├── bilibili
├── __init__.py
├── protobuf
│ ├── __init__.py
│ ├── dm_pb2.pyi
│ └── dm_pb2.py
├── utils.py
└── biliass.py
├── requirements.txt
├── README.md
├── .gitignore
├── USAGE.md
├── LICENSE
└── main.py
/bilibili/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bilibili/protobuf/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | protobuf
3 | tqdm
4 | danmakuC
5 | rsa
--------------------------------------------------------------------------------
/bilibili/protobuf/dm_pb2.pyi:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | DmSegMobileReply: Any
4 | DmWebViewReply: Any
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LBCC
2 |
3 | ## 介绍
4 |
5 | Laosun Bilibili Console Client (LBCC) 是一个跨平台B站命令行客户端.
6 |
7 | 
8 |
9 | 
10 |
11 | (上列图片不代表任何立场)
12 |
13 | 项目暂缓新功能更新, 如有 feature request 请提交 issues.
14 |
15 | ## 使用
16 |
17 | [教程](USAGE.md)
18 |
19 | ## 致谢
20 |
21 | [yutto-dev/biliass](https://github.com/yutto-dev/biliass/) 和
22 | [HFrost0/danmakuC](https://github.com/HFrost0/danmakuC/) 提供弹幕转换
23 |
24 | [mpv-player/mpv](https://github.com/mpv-player/mpv/) 提供播放器
25 |
26 | [SocialSisterYi/bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect/) 提供部分b站API
27 |
28 | ## 注意
29 |
30 | 使用 LBCC 播放的视频并不会涨播放量 !!!
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 | # Distribution / packaging
9 | .Python
10 | build/
11 | develop-eggs/
12 | dist/
13 | downloads/
14 | cookie
15 | user
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | .idea/
25 | pip-wheel-metadata/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | cookie.txt
33 |
34 | *.json
35 |
36 | # PyInstaller
37 | # Usually these files are written by a python script from a template
38 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
39 | *.manifest
40 | *.spec
41 |
42 | # Installer logs
43 | pip-log.txt
44 | pip-delete-this-directory.txt
45 |
46 | # Unit test / coverage reports
47 | htmlcov/
48 | .tox/
49 | .nox/
50 | .coverage
51 | .coverage.*
52 | .cache
53 | nosetests.xml
54 | coverage.xml
55 | *.cover
56 | *.py,cover
57 | .hypothesis/
58 | .pytest_cache/
59 | cached/
60 |
61 | # Translations
62 | *.mo
63 | *.pot
64 |
65 | # Django stuff:
66 | *.log
67 | local_settings.py
68 | db.sqlite3
69 | db.sqlite3-journal
70 |
71 | # Flask stuff:
72 | instance/
73 | .webassets-cache
74 |
75 | # Scrapy stuff:
76 | .scrapy
77 |
78 | # Sphinx documentation
79 | docs/_build/
80 |
81 | # PyBuilder
82 | target/
83 |
84 | # Jupyter Notebook
85 | .ipynb_checkpoints
86 |
87 | # IPython
88 | profile_default/
89 | ipython_config.py
90 |
91 | # pyenv
92 | .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
102 | __pypackages__/
103 |
104 | # Celery stuff
105 | celerybeat-schedule
106 | celerybeat.pid
107 |
108 | # SageMath parsed files
109 | *.sage.py
110 |
111 | # Environments
112 | .env
113 | .venv
114 | env/
115 | venv/
116 | ENV/
117 | env.bak/
118 | venv.bak/
119 | download/
120 | download/*.mp4
121 |
122 | # Spyder project settings
123 | .spyderproject
124 | .spyproject
125 |
126 | # Rope project settings
127 | .ropeproject
128 |
129 | # mkdocs documentation
130 | /site
131 |
132 | # mypy
133 | .mypy_cache/
134 | .dmypy.json
135 | dmypy.json
136 |
137 | # Pyre type checker
138 | .pyre/
139 | /old
--------------------------------------------------------------------------------
/bilibili/utils.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import json
3 | import os
4 | import re
5 | import time
6 | import urllib.parse
7 |
8 | import requests
9 | from google.protobuf.json_format import MessageToJson
10 |
11 | from bilibili.biliass import Proto2ASS
12 | from bilibili.protobuf.dm_pb2 import DmSegMobileReply, DmWebViewReply
13 |
14 | XOR_CODE = 23442827791579
15 | MASK_CODE = 2251799813685247
16 | MAX_AID = 1 << 51
17 |
18 | data = [b"F", b"c", b"w", b"A", b"P", b"N", b"K", b"T", b"M", b"u", b"g", b"3", b"G", b"V", b"5", b"L", b"j", b"7",
19 | b"E", b"J", b"n", b"H", b"p", b"W", b"s", b"x", b"4", b"t", b"b", b"8", b"h", b"a", b"Y", b"e", b"v", b"i",
20 | b"q", b"B", b"z", b"6", b"r", b"k", b"C", b"y", b"1", b"2", b"m", b"U", b"S", b"D", b"Q", b"X", b"9", b"R",
21 | b"d", b"o", b"Z", b"f"]
22 |
23 | BASE = 58
24 | BV_LEN = 12
25 | PREFIX = "BV1"
26 |
27 | cached_response: dict[str, str] = {}
28 |
29 |
30 | class UserManager:
31 | def __init__(self, cookie=""):
32 | self.cached_response: dict[str, requests.Response] = {}
33 | self.session = requests.session()
34 | self.session.headers.update(
35 | {
36 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
37 | "referer": "https://www.bilibili.com",
38 | }
39 | )
40 | self.session.headers.update({"cookie": cookie})
41 | self.mid = 0
42 | self.csrf = clean_cookie(
43 | convert_cookies_to_dict(self.session.headers.get("cookie"))
44 | ).get("bili_jct", "")
45 | self.is_login = False
46 |
47 | # 这个函数不可能返回 None, 因为 if 已经对 cached_response 进行了验空
48 | def get(self, url: str, params=None, cache=False, **kwargs) -> requests.Response:
49 | if self.cached_response.get(url):
50 | return self.cached_response.get(url) # type: ignore
51 | else:
52 | count = 5
53 | while True:
54 | try:
55 | request = self.session.get(url, params=params, timeout=5, **kwargs)
56 | break
57 | except requests.exceptions.RequestException as request_error:
58 | print("\n")
59 | print(f"{url}请求错误! 将会重试{count}次! ")
60 | count -= 1
61 | if count <= 0:
62 | raise request_error
63 | if cache:
64 | self.cached_response[url] = request
65 | return request
66 |
67 | def post(self, url: str, params=None, **kwargs) -> requests.Response:
68 | count = 5
69 | while True:
70 | try:
71 | request = self.session.post(url, params=params, timeout=5, **kwargs)
72 | break
73 | except requests.exceptions.RequestException as request_error:
74 | print("\n")
75 | print(f"{url}请求错误! 将会重试{count}次! ")
76 | count -= 1
77 | if count <= 0:
78 | raise request_error
79 | return request
80 |
81 | def refresh_login(self):
82 | if os.path.exists("cookie.txt"):
83 | with open("cookie.txt") as file:
84 | cookie = file.read()
85 | self.session.headers["cookie"] = cookie
86 | print()
87 | print("刷新登录状态成功.")
88 | print()
89 | return self.login()
90 |
91 | def login(self):
92 | request = self.get("https://api.bilibili.com/x/member/web/account")
93 | if request.json()["code"] == -101:
94 | print("账号尚未登录.")
95 | print()
96 | elif request.json()["code"] == 0:
97 | print("账号已登录.")
98 | print(f"欢迎{request.json()['data']['uname']}登录.")
99 | print()
100 | self.mid = request.json()["data"]["mid"]
101 | self.is_login = True
102 | self.csrf = clean_cookie(
103 | convert_cookies_to_dict(self.session.headers.get("cookie"))
104 | ).get("bili_jct", "")
105 | else:
106 | raise Exception("Invalid login code: " + str(request.json()["code"]))
107 |
108 |
109 | def convert_cookies_to_dict(cookies) -> dict[str, str]:
110 | if not cookies:
111 | return {}
112 | return dict([li.split("=", 1) for li in cookies.split(";")])
113 |
114 |
115 | def clean_cookie(dict_cookie: dict[str, str]) -> dict[str, str]:
116 | cleaned = {}
117 | for i, j in dict_cookie.items():
118 | cleaned[i.strip()] = j.strip()
119 | return cleaned
120 |
121 |
122 | def remove(remove_str: str, want_remove: str):
123 | return remove_str.replace(want_remove, "")
124 |
125 |
126 | def format_time(timestamp: int) -> str:
127 | if timestamp > 60 * 60:
128 | fmt = "{}:{}:{}"
129 | hour = timestamp // (60 * 60)
130 | minute = (timestamp - (hour * 60 * 60)) // 60
131 | sec = timestamp - (hour * 60 * 60) - minute * 60
132 | if minute < 10:
133 | fmt = "{}:0{}:{}"
134 | if sec < 10:
135 | fmt = "{}:{}:0{}"
136 | if sec < 10 and minute < 10:
137 | fmt = "{}:0{}:0{}"
138 | if hour > 10:
139 | fmt = "1" + fmt
140 | return fmt.format(hour, minute, sec)
141 | else:
142 | fmt = "{}:{}"
143 | minute = timestamp // 60
144 | if minute < 10:
145 | fmt = "0{}:{}"
146 | sec = timestamp - minute * 60
147 | if sec < 10:
148 | fmt = "{}:0{}"
149 | if sec < 10 and minute < 10:
150 | fmt = "0{}:0{}"
151 | return fmt.format(minute, sec)
152 |
153 |
154 | def validate_title(title) -> str:
155 | rstr = r"[\/\\\:\*\?\"\<\>\|]"
156 | new_title = re.sub(rstr, "_", title)
157 | return new_title
158 |
159 |
160 | # av bv互转算法
161 | # https://www.zhihu.com/question/381784377/answer/1099438784
162 |
163 |
164 | def av2bv(aid):
165 | bytes = [b"B", b"V", b"1", b"0", b"0", b"0", b"0", b"0", b"0", b"0", b"0", b"0"]
166 | bv_idx = BV_LEN - 1
167 | tmp = (MAX_AID | aid) ^ XOR_CODE
168 | while int(tmp) != 0:
169 | bytes[bv_idx] = data[int(tmp % BASE)]
170 | tmp /= BASE
171 | bv_idx -= 1
172 | bytes[3], bytes[9] = bytes[9], bytes[3]
173 | bytes[4], bytes[7] = bytes[7], bytes[4]
174 | return "".join([i.decode() for i in bytes])
175 |
176 |
177 | def bv2av(bvid: str):
178 | bvid_list = list(bvid)
179 | bvid_list[3], bvid_list[9] = bvid_list[9], bvid_list[3]
180 | bvid_list[4], bvid_list[7] = bvid_list[7], bvid_list[4]
181 | bvid_list = bvid_list[3:]
182 | tmp = 0
183 | for i in bvid_list:
184 | idx = data.index(i.encode())
185 | tmp = tmp * BASE + idx
186 | return (tmp & MASK_CODE) ^ XOR_CODE
187 |
188 |
189 | def read_cookie():
190 | if os.path.exists("cookie.txt"):
191 | with open("cookie.txt") as f:
192 | cookie = f.read().strip()
193 | return cookie
194 | else:
195 | with open("cookie.txt", "w") as f:
196 | f.write("")
197 |
198 |
199 | def encrypt_wbi(request_params: str):
200 | params = {i.split("=")[0]: i.split("=")[1] for i in request_params.split("&")}
201 | r = user_manager.get("https://api.bilibili.com/x/web-interface/nav", cache=True)
202 | wbi_img_url = r.json()["data"]["wbi_img"]["img_url"]
203 | wbi_sub_url = r.json()["data"]["wbi_img"]["sub_url"]
204 | oe = [46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12,
205 | 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62,
206 | 11, 36, 20, 34, 44, 52]
207 |
208 | le = []
209 | key = (
210 | wbi_img_url.split("/")[-1].split(".")[0]
211 | + wbi_sub_url.split("/")[-1].split(".")[0]
212 | )
213 | for i in oe:
214 | le.append(key[i])
215 | key = "".join(le)[:32]
216 | params["wts"] = str(round(time.time()))
217 | wbi_sign = hashlib.md5(
218 | (urllib.parse.urlencode(dict(sorted(params.items()))) + key).encode()
219 | ).hexdigest() # 计算 w_rid
220 | params["w_rid"] = wbi_sign
221 | return urllib.parse.urlencode(params)
222 |
223 |
224 | # https://www.cnblogs.com/0506winds/p/13953600.html
225 | # python字节自适应转化单位KB、MB、GB
226 | def hum_convert(value):
227 | units = ["B", "KB", "MB", "GB", "TB", "PB"]
228 | size = 1024.0
229 | for i in range(len(units)):
230 | if (value / size) < 1:
231 | return "%.2f%s" % (value, units[i])
232 | value = value / size
233 |
234 |
235 | def get_danmaku(cid: int, index: int = 1):
236 | resp = user_manager.get(
237 | f"https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid={cid}&segment_index={index}",
238 | cache=True,
239 | )
240 | return resp.content
241 |
242 |
243 | def get_more_danmaku(cid: int):
244 | view = parse_view(cid)
245 | total = int(view['dmSge']['total'])
246 | danmaku_byte = [get_danmaku(cid, i) for i in range(1, total + 1)]
247 | return b"".join(danmaku_byte)
248 |
249 |
250 | def parse_view(cid: int):
251 | resp = user_manager.get(f"https://api.bilibili.com/x/v2/dm/web/view?oid={cid}&type=1", cache=True)
252 | dm_view = DmWebViewReply()
253 | dm_view.ParseFromString(resp.content)
254 | dm_view = json.loads(MessageToJson(dm_view))
255 | return dm_view
256 |
257 |
258 | def danmaku_provider():
259 | try:
260 | from danmakuC.bilibili import proto2ass
261 | return proto2ass
262 | except Exception:
263 | return Proto2ASS
264 |
265 |
266 | user_manager = UserManager(read_cookie())
267 |
--------------------------------------------------------------------------------
/USAGE.md:
--------------------------------------------------------------------------------
1 | # LBCC使用教程
2 |
3 | ## 1.1 下载并安装
4 |
5 | 首先安装 [python](http://www.python.org/downloads) 3.6 版本以上
6 | 和 [mpv](https://laosun-image.obs.cn-north-4.myhuaweicloud.com/mpv.exe)
7 |
8 | 然后 `git clone https://github.com/12345-mcpython/bilibili-console` 或
9 |
10 | 
11 |
12 | 并解压zip
13 |
14 | 打开命令行,切换到此文件夹或对文件夹Shift+右键,点击"在命令行打开此文件夹" (在Win11中可以直接右键打开终端)
15 |
16 | 输入 `pip install -r requirements.txt`
17 |
18 | 
19 |
20 | 登录b站网页端, 手动拷贝cookie一次(按F12, 到Network 网络 标签页, 按F5, 翻到最上面找到第一个www.bilibili.com点击,
21 | 找到Response Header 请求标头 找到cookie选择并复杂这堆cookie) 并新建文件cookie.txt, 把拷贝内容复制到这个文件.
22 |
23 | 如果懒得复制可以通过删除 localStorage 的 ac_time_value 延长时间
24 |
25 | 
26 |
27 | 输入 `python main.py`
28 |
29 | 
30 |
31 | ## 1.2 主界面指令集
32 |
33 | 1. `recommend/r` 根据你账号的个性化需求推荐视频.
34 | 2. `address/a` 根据地址播放视频. 支持 b23.tv 短链接.
35 | 3. `bangumi/b` 番剧界面.
36 | 4. `favorite/f` 预览当前已登录账号的收藏夹.
37 | 5. `view_self` 预览当前已登录账号的个人空间.
38 | 6. `view_user` 根据mid预览账号的个人空间.
39 | 7. `export_favorite` 导出收藏夹为JSON. JSON格式见下.
40 | 8. `download_favorite` 下载收藏夹内全部视频.
41 | 9. `refresh_login` 重载登录状态, 重新加载cookie.txt文件内cookie.
42 | 10. `clean_cache` 清空缓存的弹幕文件.
43 | 11. `enable_online_watching` 与 `disable_online_watching` 启用或禁用正在观看的人数.
44 | 12. `login` 登录.
45 | 13. `logout` 登出.
46 | 14. `history` 查看该账号历史记录.
47 |
48 | ## 1.3 视频选项指令集
49 |
50 | 1. `play/p` 播放视频.
51 | 2. `like/l` 与 `unlike/ul` 点赞或取消点赞视频.
52 | 3. `unlike/ul` 取消点赞.
53 | 4. `coin/c` 投币视频.
54 | 5. `triple/t` 三连视频.
55 | 6. `download/d` 下载视频.
56 | 7. `download_video_list/da` 下载全部视频.
57 | 8. `view_user` 预览视频作者的用户空间.
58 | 9. `follow/fo` 关注视频作者.
59 | 10. `unfollow/ufo` 取关视频作者.
60 |
61 | ## 1.4 个人空间指令
62 |
63 | 1. `list_video` 列出所有个人空间的视频.
64 |
65 | ## 附录 导出的收藏夹JSON格式
66 |
67 |
68 |
69 | ```json
70 | {
71 | "cover": "https://i1.hdslb.com/bfs/archive/903a7e34a064a8b4b0a4cf5d72c32a9344c6d30c.jpg",
72 | "id": 1872753922,
73 | "media_count": 19,
74 | "medias": [
75 | {
76 | "attr": 0,
77 | "bvid": "BV1R54y1N7nu",
78 | "cnt_info": {
79 | "collect": 11918,
80 | "danmaku": 33,
81 | "play": 190535
82 | },
83 | "cover": "https://i1.hdslb.com/bfs/archive/903a7e34a064a8b4b0a4cf5d72c32a9344c6d30c.jpg",
84 | "duration": 645,
85 | "fav_time": 1676978493,
86 | "id": 864204502,
87 | "intro": "1w摩拉换80原石!老米你真能藏!1w摩拉是本派蒙的工资!",
88 | "page": 1,
89 | "publish_time": 1676088688,
90 | "title": "1w摩拉换80原石!老米你真能藏!1w摩拉是本派蒙的工资!",
91 | "upper": {
92 | "face": "https://i2.hdslb.com/bfs/face/7d874b93b83da539871f1d5df455819826d95491.jpg",
93 | "mid": 3493086890035370,
94 | "name": "游戏小驴大解密"
95 | }
96 | },
97 | {
98 | "attr": 0,
99 | "bvid": "BV1oM41147ER",
100 | "cnt_info": {
101 | "collect": 17658,
102 | "danmaku": 575,
103 | "play": 894611
104 | },
105 | "cover": "https://i0.hdslb.com/bfs/archive/7abd49be4afa4ba9fe203b3b55f300303003eba9.jpg",
106 | "duration": 63,
107 | "fav_time": 1675262946,
108 | "id": 523013198,
109 | "intro": "还是当哥哥跟美少女贴贴有代入感啊",
110 | "page": 1,
111 | "publish_time": 1674124056,
112 | "title": "【原神】我好后悔当初没选哥哥当主角呀",
113 | "upper": {
114 | "face": "https://i2.hdslb.com/bfs/face/4d81826ee190a55593a7f2ae1e20b8fbc8e6b3c0.jpg",
115 | "mid": 1540103078,
116 | "name": "不长草的树根"
117 | }
118 | },
119 | {
120 | "attr": 0,
121 | "bvid": "BV1hg41137ES",
122 | "cnt_info": {
123 | "collect": 3,
124 | "danmaku": 0,
125 | "play": 836
126 | },
127 | "cover": "https://i1.hdslb.com/bfs/archive/cabea28b765a573847a2e1e988a4d6eb08298fb1.jpg",
128 | "duration": 51,
129 | "fav_time": 1674198735,
130 | "id": 503342351,
131 | "intro": "源关卡(要科学上网):https://www.youtube.com/watch?v=SYlFCYdqjQs",
132 | "page": 1,
133 | "publish_time": 1622298603,
134 | "title": "[小鳄鱼][每周关卡还原]搭桥前进",
135 | "upper": {
136 | "face": "https://i0.hdslb.com/bfs/face/03cfc671b0c356a34dfd1614903b50a464d42735.jpg",
137 | "mid": 349112658,
138 | "name": "呵哈呵啊呵哈呵"
139 | }
140 | },
141 | {
142 | "attr": 0,
143 | "bvid": "BV1Wz411z7aG",
144 | "cnt_info": {
145 | "collect": 56,
146 | "danmaku": 18,
147 | "play": 21177
148 | },
149 | "cover": "https://i2.hdslb.com/bfs/archive/20687314f2f2ba70d4c5191df2c76ce93bb6392e.jpg",
150 | "duration": 36,
151 | "fav_time": 1674197311,
152 | "id": 200502341,
153 | "intro": "mega mega mega mega mega\n好昏呐\n等我睡会\n游戏:鳄鱼小顽皮爱洗澡",
154 | "page": 1,
155 | "publish_time": 1588425315,
156 | "title": "建议改为:群 魔 乱 舞",
157 | "upper": {
158 | "face": "https://i0.hdslb.com/bfs/face/da67a317123e8271967b2aa39291587f4b49587a.jpg",
159 | "mid": 348887631,
160 | "name": "拿着skull的老爹"
161 | }
162 | },
163 | {
164 | "attr": 0,
165 | "bvid": "BV1pi4y1x797",
166 | "cnt_info": {
167 | "collect": 3,
168 | "danmaku": 1,
169 | "play": 2140
170 | },
171 | "cover": "https://i0.hdslb.com/bfs/archive/222f412c748727e4553c396b257a223d3bd564a7.jpg",
172 | "duration": 19,
173 | "fav_time": 1674196919,
174 | "id": 541027250,
175 | "intro": "游戏:鳄鱼小顽皮爱洗澡\n如果你看到了这行字,就给我点个赞吧~",
176 | "page": 1,
177 | "publish_time": 1592112164,
178 | "title": "【挑战NO.2】零鸭挑战F1-5",
179 | "upper": {
180 | "face": "https://i0.hdslb.com/bfs/face/21a97a0109123cf59956257a56a34172bf61c192.jpg",
181 | "mid": 389116215,
182 | "name": "芝士-cheese-"
183 | }
184 | },
185 | {
186 | "attr": 0,
187 | "bvid": "BV1pe411s7rN",
188 | "cnt_info": {
189 | "collect": 240,
190 | "danmaku": 81,
191 | "play": 170968
192 | },
193 | "cover": "https://i0.hdslb.com/bfs/archive/c44a6c51293c4f715a521ed5caac93b631b4fb29.jpg",
194 | "duration": 264,
195 | "fav_time": 1674196333,
196 | "id": 242895142,
197 | "intro": "-",
198 | "page": 1,
199 | "publish_time": 1588135368,
200 | "title": "【喜羊羊】给 我 毒 水 灭 火",
201 | "upper": {
202 | "face": "https://i2.hdslb.com/bfs/face/8777c8bd3083f9f823445e84f46c3b5dd8b93380.jpg",
203 | "mid": 520011242,
204 | "name": "MED_Lucient__"
205 | }
206 | },
207 | {
208 | "attr": 0,
209 | "bvid": "BV1624y1e7JJ",
210 | "cnt_info": {
211 | "collect": 1400,
212 | "danmaku": 44,
213 | "play": 80243
214 | },
215 | "cover": "https://i2.hdslb.com/bfs/archive/35998b53f5bddbc92ea9007d97d65071208553d4.jpg",
216 | "duration": 112,
217 | "fav_time": 1674031386,
218 | "id": 692510702,
219 | "intro": "模型作者by_takoyaki.raw",
220 | "page": 1,
221 | "publish_time": 1673492400,
222 | "title": "[原神] x [莉可丽丝] “今天天气很好,我也精神满满,真好”Lycoris所属,代号LC2808,锦木千束,登陆!",
223 | "upper": {
224 | "face": "https://i1.hdslb.com/bfs/face/9d998af488de31883abb56d416c5b393f90a0415.gif",
225 | "mid": 769105,
226 | "name": "嫁人的少女"
227 | }
228 | },
229 | {
230 | "attr": 0,
231 | "bvid": "BV1yM411h74d",
232 | "cnt_info": {
233 | "collect": 119586,
234 | "danmaku": 303,
235 | "play": 932450
236 | },
237 | "cover": "https://i0.hdslb.com/bfs/archive/31eda1f16f6d6ee6cfaf7fe87bb1848b511d8faa.jpg",
238 | "duration": 70,
239 | "fav_time": 1673177354,
240 | "id": 522207629,
241 | "intro": "-",
242 | "page": 1,
243 | "publish_time": 1672631267,
244 | "title": "惊天Bug??下线重上就能刷新宝箱",
245 | "upper": {
246 | "face": "https://i0.hdslb.com/bfs/face/561d9cfadbc943aabf7f4ff777d0eafcbeda89f5.jpg",
247 | "mid": 3493089123502938,
248 | "name": "流浪者小散兵"
249 | }
250 | },
251 | {
252 | "attr": 0,
253 | "bvid": "BV1g14y1p7nH",
254 | "cnt_info": {
255 | "collect": 4945,
256 | "danmaku": 0,
257 | "play": 39473
258 | },
259 | "cover": "https://i1.hdslb.com/bfs/archive/0fca9b8e8f11ab97213dc329b851a77e2e12000e.jpg",
260 | "duration": 614,
261 | "fav_time": 1672578295,
262 | "id": 775415118,
263 | "intro": "白铁矿 高效速刷路线!",
264 | "page": 1,
265 | "publish_time": 1669208926,
266 | "title": "原神白铁矿速刷点位,7分钟111个高效便捷。",
267 | "upper": {
268 | "face": "https://i1.hdslb.com/bfs/face/dd1d66600cb3e4f24fc79c716ca106eeda0ece9f.jpg",
269 | "mid": 412276207,
270 | "name": "阿炜原神"
271 | }
272 | },
273 | {
274 | "attr": 0,
275 | "bvid": "BV1Z84y167cY",
276 | "cnt_info": {
277 | "collect": 55347,
278 | "danmaku": 916,
279 | "play": 764598
280 | },
281 | "cover": "https://i2.hdslb.com/bfs/archive/0baf4deb80d9bda50180a0deb817504cf1958461.jpg",
282 | "duration": 191,
283 | "fav_time": 1672573323,
284 | "id": 605959769,
285 | "intro": "",
286 | "page": 1,
287 | "publish_time": 1670342559,
288 | "title": "原神:从欧皇那里偷学的玄学抽卡技巧!",
289 | "upper": {
290 | "face": "https://i0.hdslb.com/bfs/face/8f5b0cca8c28d0e0c2ad8d19d96c19de64b14d6c.jpg",
291 | "mid": 26705184,
292 | "name": "拾柒酱紫吖"
293 | }
294 | },
295 | {
296 | "attr": 0,
297 | "bvid": "BV1Qe411j7xa",
298 | "cnt_info": {
299 | "collect": 183560,
300 | "danmaku": 1282,
301 | "play": 3077608
302 | },
303 | "cover": "https://i0.hdslb.com/bfs/archive/a3b09e634451961e11d47c19024a0dcdce3ceca4.jpg",
304 | "duration": 673,
305 | "fav_time": 1672569723,
306 | "id": 261352890,
307 | "intro": "1 须弥哈哈镜\n2须弥花样跳水\n3须弥悬崖双人打卡点\n4须弥滑滑梯\n5群玉阁夕阳红\n6璃月跑步机\n7比翼双飞\n8小个快乐坡\n9风龙废墟打卡点\n10须弥空气墙\n11梯子走秀\n12骑士踢\n13原魔小姐姐之舞\n14五香岩见证的爱情\n15早柚画爱心\n16流星时刻\n17踏鞴砂浪漫海滩\n18蒙德城神像\n19渊下宫打卡点\n20须弥雨幕\n21璃月滑滑梯\n22明蕴镇双人看日落\n23猫猫云\n24双人快乐菇\n25花之桥\n26-30爱心打卡点\n31-39其他打卡点\n末尾:彩蛋",
308 | "page": 1,
309 | "publish_time": 1664974912,
310 | "title": "盘点39个你不一定知道原神网红打卡点",
311 | "upper": {
312 | "face": "https://i1.hdslb.com/bfs/face/a990d058125253dc0cffa66aa27f2a298108fbb0.jpg",
313 | "mid": 1756185076,
314 | "name": "提瓦特老村长"
315 | }
316 | },
317 | {
318 | "attr": 0,
319 | "bvid": "BV1HM411z75u",
320 | "cnt_info": {
321 | "collect": 26849,
322 | "danmaku": 160,
323 | "play": 770741
324 | },
325 | "cover": "https://i0.hdslb.com/bfs/archive/f8ca72527a9fb51c8328f1cfd6a76a1ea860efa8.jpg",
326 | "duration": 179,
327 | "fav_time": 1671619135,
328 | "id": 521074242,
329 | "intro": "",
330 | "page": 1,
331 | "publish_time": 1670555842,
332 | "title": "原神1级就能从蒙德偷渡到稻妻的方法,我们好像走到了世界的尽头",
333 | "upper": {
334 | "face": "https://i1.hdslb.com/bfs/face/76a805cc27075752fdee4dd6aa35a393e0582afc.jpg",
335 | "mid": 1896554362,
336 | "name": "易神易魔啊"
337 | }
338 | },
339 | {
340 | "attr": 0,
341 | "bvid": "BV19G4y1G7fF",
342 | "cnt_info": {
343 | "collect": 34123,
344 | "danmaku": 676,
345 | "play": 408608
346 | },
347 | "cover": "https://i0.hdslb.com/bfs/archive/c9aa2bcfb8d33e3477bf588915dda50138393402.jpg",
348 | "duration": 243,
349 | "fav_time": 1671274399,
350 | "id": 818298593,
351 | "intro": "做了很长时间的男生女生都能看的散兵/流浪者声线教学来啦!大家在伪音过程中一定要注意保护嗓子哦!视频仅代表个人观点,如有错误,欢迎在评论区指出!最后喜欢记得点个关注点个赞,下期更新会更快!",
352 | "page": 1,
353 | "publish_time": 1670082311,
354 | "title": "流浪者(散兵)的声线教学——男生女生都能学!",
355 | "upper": {
356 | "face": "https://i1.hdslb.com/bfs/face/921ecd1778c9ee92698fa755dde7ffbb380aedd8.jpg",
357 | "mid": 405845641,
358 | "name": "Happy_Twins"
359 | }
360 | },
361 | {
362 | "attr": 0,
363 | "bvid": "BV14G4y1R7Ps",
364 | "cnt_info": {
365 | "collect": 38623,
366 | "danmaku": 273,
367 | "play": 494590
368 | },
369 | "cover": "https://i1.hdslb.com/bfs/archive/659bbdc24377531951356ba9b2c69fda02659e3f.jpg",
370 | "duration": 142,
371 | "fav_time": 1670845515,
372 | "id": 860843467,
373 | "intro": "-",
374 | "page": 1,
375 | "publish_time": 1670050979,
376 | "title": "一个视频让萌新旅行者赢在起跑线上",
377 | "upper": {
378 | "face": "https://i1.hdslb.com/bfs/face/a990d058125253dc0cffa66aa27f2a298108fbb0.jpg",
379 | "mid": 1756185076,
380 | "name": "提瓦特老村长"
381 | }
382 | },
383 | {
384 | "attr": 0,
385 | "bvid": "BV1SM411r7Nc",
386 | "cnt_info": {
387 | "collect": 35749,
388 | "danmaku": 421,
389 | "play": 586445
390 | },
391 | "cover": "https://i2.hdslb.com/bfs/archive/0fdf544b029894d2b07788a9c8c4a8bcd83efd6f.jpg",
392 | "duration": 63,
393 | "fav_time": 1670844684,
394 | "id": 520504184,
395 | "intro": "-",
396 | "page": 1,
397 | "publish_time": 1669450087,
398 | "title": "让旅行者记忆犹新的NPC们",
399 | "upper": {
400 | "face": "https://i1.hdslb.com/bfs/face/a990d058125253dc0cffa66aa27f2a298108fbb0.jpg",
401 | "mid": 1756185076,
402 | "name": "提瓦特老村长"
403 | }
404 | },
405 | {
406 | "attr": 0,
407 | "bvid": "BV1uP411P7KR",
408 | "cnt_info": {
409 | "collect": 30166,
410 | "danmaku": 1128,
411 | "play": 1034118
412 | },
413 | "cover": "https://i2.hdslb.com/bfs/archive/735cdfe0b2e0e566011815bede42b26be56b1ee7.jpg",
414 | "duration": 601,
415 | "fav_time": 1670815335,
416 | "id": 304505198,
417 | "intro": "相关游戏:《纪念碑谷》",
418 | "page": 1,
419 | "publish_time": 1667016284,
420 | "title": "14年爆火的解谜游戏,《纪念碑谷》竟然隐藏了如此压抑的剧情?!",
421 | "upper": {
422 | "face": "https://i1.hdslb.com/bfs/face/22d1ece56cd2a3a82dfe626469fd8837826e3552.jpg",
423 | "mid": 293846287,
424 | "name": "硬的小软"
425 | }
426 | },
427 | {
428 | "attr": 0,
429 | "bvid": "BV1RV4y1M7dY",
430 | "cnt_info": {
431 | "collect": 26028,
432 | "danmaku": 581,
433 | "play": 1622085
434 | },
435 | "cover": "https://i0.hdslb.com/bfs/archive/3958faf29152c47c8fe272caf9590087fc00ca9b.jpg",
436 | "duration": 109,
437 | "fav_time": 1670815247,
438 | "id": 858197915,
439 | "intro": "整点旧活,趁着烟花穿墙还没和谐,赶紧下来探索探索",
440 | "page": 1,
441 | "publish_time": 1663388801,
442 | "title": "【原神】离谱!风龙废墟下的隐藏彩蛋(地下还有空间?)",
443 | "upper": {
444 | "face": "https://i2.hdslb.com/bfs/face/1e100316a9fb4b2c09bdc3409b26c1b1ee25f04b.jpg",
445 | "mid": 15112013,
446 | "name": "一半月X"
447 | }
448 | },
449 | {
450 | "attr": 0,
451 | "bvid": "BV1r8411j7in",
452 | "cnt_info": {
453 | "collect": 36749,
454 | "danmaku": 236,
455 | "play": 884923
456 | },
457 | "cover": "https://i1.hdslb.com/bfs/archive/8df3790e8d8f33ff989bcb3048cf01673d545c3f.jpg",
458 | "duration": 102,
459 | "fav_time": 1670815194,
460 | "id": 220210985,
461 | "intro": "-",
462 | "page": 1,
463 | "publish_time": 1668515207,
464 | "title": "7个牺牲旅行者才能完成的成就",
465 | "upper": {
466 | "face": "https://i1.hdslb.com/bfs/face/a990d058125253dc0cffa66aa27f2a298108fbb0.jpg",
467 | "mid": 1756185076,
468 | "name": "提瓦特老村长"
469 | }
470 | },
471 | {
472 | "attr": 0,
473 | "bvid": "BV1N24y1f7ae",
474 | "cnt_info": {
475 | "collect": 121722,
476 | "danmaku": 334,
477 | "play": 995180
478 | },
479 | "cover": "https://i1.hdslb.com/bfs/archive/2c624885e2f7fd3a501c468c9b16c4b2b4e8e9ce.jpg",
480 | "duration": 456,
481 | "fav_time": 1670815194,
482 | "id": 689702440,
483 | "intro": "",
484 | "page": 1,
485 | "publish_time": 1667361128,
486 | "title": "远吕羽氏遗事任务全流程210原石一长枪图纸",
487 | "upper": {
488 | "face": "https://i2.hdslb.com/bfs/face/ad1f831bf06bdc970aae3b3ec4182234300d0701.jpg",
489 | "mid": 1662158531,
490 | "name": "小张张原神"
491 | }
492 | }
493 | ],
494 | "title": "其他游戏",
495 | "user": {
496 | "create_time": 1670815074,
497 | "mid": 450196722,
498 | "name": "grhrhd123"
499 | },
500 | "view": 0
501 | }
502 | ```
503 |
--------------------------------------------------------------------------------
/bilibili/biliass.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import math
4 | import random
5 | import re
6 | from typing import Optional, Union, List, Generator, Tuple
7 |
8 | from .protobuf.dm_pb2 import DmSegMobileReply
9 |
10 | #
11 | # ReadComments**** protocol
12 | #
13 | # Input:
14 | # text: Input XML string
15 | # fontsize: Default font size
16 | #
17 | # Output:
18 | # yield a tuple:
19 | # (timeline, timestamp, no, comment, pos, color, size, height, width)
20 | # timeline: The position when the comment is replayed
21 | # timestamp: The UNIX timestamp when the comment is submitted
22 | # no: A sequence of 1, 2, 3, ..., used for sorting
23 | # comment: The content of the comment
24 | # pos: 0 for regular moving comment,
25 | # 1 for bottom centered comment,
26 | # 2 for top centered comment,
27 | # 3 for reversed moving comment
28 | # color: Font color represented in 0xRRGGBB,
29 | # e.g. 0xffffff for white
30 | # size: Font size
31 | # height: The estimated height in pixels
32 | # i.e. (comment.count('\n')+1)*size
33 | # width: The estimated width in pixels
34 | # i.e. CalculateLength(comment)*size
35 | #
36 |
37 | __all__ = []
38 |
39 |
40 | def export(func):
41 | global __all__
42 | try:
43 | __all__.append(func.__name__)
44 | except NameError:
45 | __all__ = [func.__name__]
46 | return func
47 |
48 |
49 | Comment = Tuple[float, float, int, str, Union[int, str], int, float, float, float]
50 |
51 |
52 | @export
53 | def ReadCommentsBilibiliProtobuf(protobuf: Union[bytes, str], fontsize: float) -> Generator[Comment, None, None]:
54 | target = DmSegMobileReply()
55 | target.ParseFromString(protobuf)
56 | for i, elem in enumerate(target.elems):
57 | try:
58 | assert elem.mode in (1, 4, 5, 6, 7, 8)
59 | if elem.mode in (1, 4, 5, 6):
60 | c = elem.content.replace("/n", "\n")
61 | size = int(elem.fontsize) * fontsize / 25.0
62 | yield (
63 | elem.progress / 1000, # 视频内出现的时间
64 | elem.ctime, # 弹幕的发送时间(时间戳)
65 | i,
66 | c,
67 | {1: 0, 4: 2, 5: 1, 6: 3}[elem.mode],
68 | elem.color,
69 | size,
70 | (c.count("\n") + 1) * size,
71 | CalculateLength(c) * size,
72 | )
73 | elif elem.mode == 7: # positioned comment
74 | c = elem.content
75 | yield (elem.progress / 1000, elem.ctime, i, c, "bilipos", elem.color, elem.fontsize, 0, 0)
76 | elif elem.mode == 8:
77 | pass # ignore scripted comment
78 | except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
79 | logging.warning("Invalid comment: %s" % elem.content)
80 | continue
81 |
82 |
83 | class AssText:
84 | def __init__(self):
85 | self._text = ""
86 |
87 | def WriteCommentBilibiliPositioned(self, c, width, height, styleid):
88 | # BiliPlayerSize = (512, 384) # Bilibili player version 2010
89 | # BiliPlayerSize = (540, 384) # Bilibili player version 2012
90 | # BiliPlayerSize = (672, 438) # Bilibili player version 2014
91 | BiliPlayerSize = (891, 589) # Bilibili player version 2021 (flex)
92 | ZoomFactor = GetZoomFactor(BiliPlayerSize, (width, height))
93 |
94 | def GetPosition(InputPos, isHeight):
95 | isHeight = int(isHeight) # True -> 1
96 | if isinstance(InputPos, int):
97 | return ZoomFactor[0] * InputPos + ZoomFactor[isHeight + 1]
98 | elif isinstance(InputPos, float):
99 | if InputPos > 1:
100 | return ZoomFactor[0] * InputPos + ZoomFactor[isHeight + 1]
101 | else:
102 | return BiliPlayerSize[isHeight] * ZoomFactor[0] * InputPos + ZoomFactor[isHeight + 1]
103 | else:
104 | try:
105 | InputPos = int(InputPos)
106 | except ValueError:
107 | InputPos = float(InputPos)
108 | return GetPosition(InputPos, isHeight)
109 |
110 | try:
111 | comment_args = SafeList(json.loads(c[3]))
112 | text = ASSEscape(str(comment_args[4]).replace("/n", "\n"))
113 | from_x = comment_args.get(0, 0)
114 | from_y = comment_args.get(1, 0)
115 | to_x = comment_args.get(7, from_x)
116 | to_y = comment_args.get(8, from_y)
117 | from_x = GetPosition(from_x, False)
118 | from_y = GetPosition(from_y, True)
119 | to_x = GetPosition(to_x, False)
120 | to_y = GetPosition(to_y, True)
121 | alpha = SafeList(str(comment_args.get(2, "1")).split("-"))
122 | from_alpha = float(alpha.get(0, 1))
123 | to_alpha = float(alpha.get(1, from_alpha))
124 | from_alpha = 255 - round(from_alpha * 255)
125 | to_alpha = 255 - round(to_alpha * 255)
126 | rotate_z = int(comment_args.get(5, 0))
127 | rotate_y = int(comment_args.get(6, 0))
128 | lifetime = float(comment_args.get(3, 4500))
129 | duration = int(comment_args.get(9, lifetime * 1000))
130 | delay = int(comment_args.get(10, 0))
131 | fontface = comment_args.get(12)
132 | isborder = comment_args.get(11, "true")
133 | from_rotarg = ConvertFlashRotation(rotate_y, rotate_z, from_x, from_y, width, height)
134 | to_rotarg = ConvertFlashRotation(rotate_y, rotate_z, to_x, to_y, width, height)
135 | styles = ["\\org(%d, %d)" % (width / 2, height / 2)]
136 | if from_rotarg[0:2] == to_rotarg[0:2]:
137 | styles.append("\\pos(%.0f, %.0f)" % (from_rotarg[0:2]))
138 | else:
139 | styles.append(
140 | "\\move(%.0f, %.0f, %.0f, %.0f, %.0f, %.0f)"
141 | % (from_rotarg[0:2] + to_rotarg[0:2] + (delay, delay + duration))
142 | )
143 | styles.append("\\frx%.0f\\fry%.0f\\frz%.0f\\fscx%.0f\\fscy%.0f" % (from_rotarg[2:7]))
144 | if (from_x, from_y) != (to_x, to_y):
145 | styles.append("\\t(%d, %d, " % (delay, delay + duration))
146 | styles.append("\\frx%.0f\\fry%.0f\\frz%.0f\\fscx%.0f\\fscy%.0f" % (to_rotarg[2:7]))
147 | styles.append(")")
148 | if fontface:
149 | styles.append("\\fn%s" % ASSEscape(fontface))
150 | styles.append("\\fs%.0f" % (c[6] * ZoomFactor[0]))
151 | if c[5] != 0xFFFFFF:
152 | styles.append("\\c&H%s&" % ConvertColor(c[5]))
153 | if c[5] == 0x000000:
154 | styles.append("\\3c&HFFFFFF&")
155 | if from_alpha == to_alpha:
156 | styles.append("\\alpha&H%02X" % from_alpha)
157 | elif (from_alpha, to_alpha) == (255, 0):
158 | styles.append("\\fad(%.0f,0)" % (lifetime * 1000))
159 | elif (from_alpha, to_alpha) == (0, 255):
160 | styles.append("\\fad(0, %.0f)" % (lifetime * 1000))
161 | else:
162 | styles.append(
163 | "\\fade(%(from_alpha)d, %(to_alpha)d, %(to_alpha)d, 0, %(end_time).0f, %(end_time).0f, %(end_time).0f)"
164 | % {"from_alpha": from_alpha, "to_alpha": to_alpha, "end_time": lifetime * 1000}
165 | )
166 | if isborder == "false":
167 | styles.append("\\bord0")
168 | self._text += "Dialogue: -1,%(start)s,%(end)s,%(styleid)s,,0,0,0,,{%(styles)s}%(text)s\n" % {
169 | "start": ConvertTimestamp(c[0]),
170 | "end": ConvertTimestamp(c[0] + lifetime),
171 | "styles": "".join(styles),
172 | "text": text,
173 | "styleid": styleid,
174 | }
175 | except (IndexError, ValueError) as e:
176 | try:
177 | logging.warning("Invalid comment: %r" % c[3])
178 | except IndexError:
179 | logging.warning("Invalid comment: %r" % c)
180 |
181 | def WriteASSHead(self, width, height, fontface, fontsize, alpha, styleid):
182 | self._text += """[Script Info]
183 | Script Updated By: biliass (https://github.com/yutto-dev/biliass)
184 | ScriptType: v4.00+
185 | PlayResX: %(width)d
186 | PlayResY: %(height)d
187 | Aspect Ratio: %(width)d:%(height)d
188 | Collisions: Normal
189 | WrapStyle: 2
190 | ScaledBorderAndShadow: yes
191 | YCbCr Matrix: TV.601
192 |
193 | [V4+ Styles]
194 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
195 | Style: %(styleid)s, %(fontface)s, %(fontsize).0f, &H%(alpha)02XFFFFFF, &H%(alpha)02XFFFFFF, &H%(alpha)02X000000, &H%(alpha)02X000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, %(outline).0f, 0, 7, 0, 0, 0, 0
196 |
197 | [Events]
198 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
199 | """ % {
200 | "width": width,
201 | "height": height,
202 | "fontface": fontface,
203 | "fontsize": fontsize,
204 | "alpha": 255 - round(alpha * 255),
205 | "outline": max(fontsize / 25.0, 1),
206 | "styleid": styleid,
207 | }
208 |
209 | def WriteComment(self, c, row, width, height, bottomReserved, fontsize, duration_marquee, duration_still, styleid):
210 | text = ASSEscape(c[3])
211 | styles = []
212 | if c[4] == 1:
213 | styles.append("\\an8\\pos(%(halfwidth)d, %(row)d)" % {"halfwidth": width / 2, "row": row})
214 | duration = duration_still
215 | elif c[4] == 2:
216 | styles.append(
217 | "\\an2\\pos(%(halfwidth)d, %(row)d)"
218 | % {"halfwidth": width / 2, "row": ConvertType2(row, height, bottomReserved)}
219 | )
220 | duration = duration_still
221 | elif c[4] == 3:
222 | styles.append(
223 | "\\move(%(neglen)d, %(row)d, %(width)d, %(row)d)"
224 | % {"width": width, "row": row, "neglen": -math.ceil(c[8])}
225 | )
226 | duration = duration_marquee
227 | else:
228 | styles.append(
229 | "\\move(%(width)d, %(row)d, %(neglen)d, %(row)d)"
230 | % {"width": width, "row": row, "neglen": -math.ceil(c[8])}
231 | )
232 | duration = duration_marquee
233 | if not (-1 < c[6] - fontsize < 1):
234 | styles.append("\\fs%.0f" % c[6])
235 | if c[5] != 0xFFFFFF:
236 | styles.append("\\c&H%s&" % ConvertColor(c[5]))
237 | if c[5] == 0x000000:
238 | styles.append("\\3c&HFFFFFF&")
239 | self._text += "Dialogue: 2,%(start)s,%(end)s,%(styleid)s,,0000,0000,0000,,{%(styles)s}%(text)s\n" % {
240 | "start": ConvertTimestamp(c[0]),
241 | "end": ConvertTimestamp(c[0] + duration),
242 | "styles": "".join(styles),
243 | "text": text,
244 | "styleid": styleid,
245 | }
246 |
247 | def to_file(self, f):
248 | f.write(self._text)
249 |
250 | def to_string(self):
251 | return self._text
252 |
253 |
254 | # Result: (f, dx, dy)
255 | # To convert: NewX = f*x+dx, NewY = f*y+dy
256 | def GetZoomFactor(SourceSize, TargetSize):
257 | try:
258 | if (SourceSize, TargetSize) == GetZoomFactor.Cached_Size:
259 | return GetZoomFactor.Cached_Result
260 | except AttributeError:
261 | pass
262 | GetZoomFactor.Cached_Size = (SourceSize, TargetSize)
263 | try:
264 | SourceAspect = SourceSize[0] / SourceSize[1]
265 | TargetAspect = TargetSize[0] / TargetSize[1]
266 | if TargetAspect < SourceAspect: # narrower
267 | ScaleFactor = TargetSize[0] / SourceSize[0]
268 | GetZoomFactor.Cached_Result = (ScaleFactor, 0, (TargetSize[1] - TargetSize[0] / SourceAspect) / 2)
269 | elif TargetAspect > SourceAspect: # wider
270 | ScaleFactor = TargetSize[1] / SourceSize[1]
271 | GetZoomFactor.Cached_Result = (ScaleFactor, (TargetSize[0] - TargetSize[1] * SourceAspect) / 2, 0)
272 | else:
273 | GetZoomFactor.Cached_Result = (TargetSize[0] / SourceSize[0], 0, 0)
274 | return GetZoomFactor.Cached_Result
275 | except ZeroDivisionError:
276 | GetZoomFactor.Cached_Result = (1, 0, 0)
277 | return GetZoomFactor.Cached_Result
278 |
279 |
280 | # Calculation is based on https://github.com/jabbany/CommentCoreLibrary/issues/5#issuecomment-40087282
281 | # and https://github.com/m13253/danmaku2ass/issues/7#issuecomment-41489422
282 | # ASS FOV = width*4/3.0
283 | # But Flash FOV = width/math.tan(100*math.pi/360.0)/2 will be used instead
284 | # Result: (transX, transY, rotX, rotY, rotZ, scaleX, scaleY)
285 | def ConvertFlashRotation(rotY, rotZ, X, Y, width, height):
286 | def WrapAngle(deg):
287 | return 180 - ((180 - deg) % 360)
288 |
289 | rotY = WrapAngle(rotY)
290 | rotZ = WrapAngle(rotZ)
291 | if rotY in (90, -90):
292 | rotY -= 1
293 | if rotY == 0 or rotZ == 0:
294 | outX = 0
295 | outY = -rotY # Positive value means clockwise in Flash
296 | outZ = -rotZ
297 | rotY *= math.pi / 180.0
298 | rotZ *= math.pi / 180.0
299 | else:
300 | rotY *= math.pi / 180.0
301 | rotZ *= math.pi / 180.0
302 | outY = math.atan2(-math.sin(rotY) * math.cos(rotZ), math.cos(rotY)) * 180 / math.pi
303 | outZ = math.atan2(-math.cos(rotY) * math.sin(rotZ), math.cos(rotZ)) * 180 / math.pi
304 | outX = math.asin(math.sin(rotY) * math.sin(rotZ)) * 180 / math.pi
305 | trX = (
306 | (X * math.cos(rotZ) + Y * math.sin(rotZ)) / math.cos(rotY)
307 | + (1 - math.cos(rotZ) / math.cos(rotY)) * width / 2
308 | - math.sin(rotZ) / math.cos(rotY) * height / 2
309 | )
310 | trY = Y * math.cos(rotZ) - X * math.sin(rotZ) + math.sin(rotZ) * width / 2 + (1 - math.cos(rotZ)) * height / 2
311 | trZ = (trX - width / 2) * math.sin(rotY)
312 | FOV = width * math.tan(2 * math.pi / 9.0) / 2
313 | try:
314 | scaleXY = FOV / (FOV + trZ)
315 | except ZeroDivisionError:
316 | logging.error("Rotation makes object behind the camera: trZ == %.0f" % trZ)
317 | scaleXY = 1
318 | trX = (trX - width / 2) * scaleXY + width / 2
319 | trY = (trY - height / 2) * scaleXY + height / 2
320 | if scaleXY < 0:
321 | scaleXY = -scaleXY
322 | outX += 180
323 | outY += 180
324 | logging.error("Rotation makes object behind the camera: trZ == %.0f < %.0f" % (trZ, FOV))
325 | return (trX, trY, WrapAngle(outX), WrapAngle(outY), WrapAngle(outZ), scaleXY * 100, scaleXY * 100)
326 |
327 |
328 | def ProcessComments(
329 | comments,
330 | width,
331 | height,
332 | bottomReserved,
333 | font_face,
334 | font_size,
335 | alpha,
336 | duration_marquee,
337 | duration_still,
338 | filters_regex,
339 | reduced,
340 | ):
341 | style_id = "danmaku_%04x" % random.randint(0, 0xFFFF)
342 | ass = AssText()
343 | ass.WriteASSHead(width, height, font_face, font_size, alpha, style_id)
344 | rows = [[None] * (height - bottomReserved + 1) for i in range(4)]
345 | for idx, i in enumerate(comments):
346 | if isinstance(i[4], int):
347 | skip = False
348 | for filter_regex in filters_regex:
349 | if filter_regex and filter_regex.search(i[3]):
350 | skip = True
351 | break
352 | if skip:
353 | continue
354 | row = 0
355 | rowmax = height - bottomReserved - i[7]
356 | while row <= rowmax:
357 | freerows = TestFreeRows(rows, i, row, width, height, bottomReserved, duration_marquee, duration_still)
358 | if freerows >= i[7]:
359 | MarkCommentRow(rows, i, row)
360 | ass.WriteComment(
361 | i, row, width, height, bottomReserved, font_size, duration_marquee, duration_still, style_id
362 | )
363 | break
364 | else:
365 | row += freerows or 1
366 | else:
367 | if not reduced:
368 | row = FindAlternativeRow(rows, i, height, bottomReserved)
369 | MarkCommentRow(rows, i, row)
370 | ass.WriteComment(
371 | i, row, width, height, bottomReserved, font_size, duration_marquee, duration_still, style_id
372 | )
373 | elif i[4] == "bilipos":
374 | ass.WriteCommentBilibiliPositioned(i, width, height, style_id)
375 | else:
376 | logging.warning("Invalid comment: %r" % i[3])
377 | return ass.to_string()
378 |
379 |
380 | def TestFreeRows(rows, c, row, width, height, bottomReserved, duration_marquee, duration_still):
381 | res = 0
382 | rowmax = height - bottomReserved
383 | targetRow = None
384 | if c[4] in (1, 2):
385 | while row < rowmax and res < c[7]:
386 | if targetRow != rows[c[4]][row]:
387 | targetRow = rows[c[4]][row]
388 | if targetRow and targetRow[0] + duration_still > c[0]:
389 | break
390 | row += 1
391 | res += 1
392 | else:
393 | try:
394 | thresholdTime = c[0] - duration_marquee * (1 - width / (c[8] + width))
395 | except ZeroDivisionError:
396 | thresholdTime = c[0] - duration_marquee
397 | while row < rowmax and res < c[7]:
398 | if targetRow != rows[c[4]][row]:
399 | targetRow = rows[c[4]][row]
400 | try:
401 | if targetRow and (
402 | targetRow[0] > thresholdTime
403 | or targetRow[0] + targetRow[8] * duration_marquee / (targetRow[8] + width) > c[0]
404 | ):
405 | break
406 | except ZeroDivisionError:
407 | pass
408 | row += 1
409 | res += 1
410 | return res
411 |
412 |
413 | def FindAlternativeRow(rows, c, height, bottomReserved):
414 | res = 0
415 | for row in range(height - bottomReserved - math.ceil(c[7])):
416 | if not rows[c[4]][row]:
417 | return row
418 | elif rows[c[4]][row][0] < rows[c[4]][res][0]:
419 | res = row
420 | return res
421 |
422 |
423 | def MarkCommentRow(rows, c, row):
424 | try:
425 | for i in range(row, row + math.ceil(c[7])):
426 | rows[c[4]][i] = c
427 | except IndexError:
428 | pass
429 |
430 |
431 | def ASSEscape(s):
432 | def ReplaceLeadingSpace(s):
433 | sstrip = s.strip(" ")
434 | slen = len(s)
435 | if slen == len(sstrip):
436 | return s
437 | else:
438 | llen = slen - len(s.lstrip(" "))
439 | rlen = slen - len(s.rstrip(" "))
440 | return "".join(("\u2007" * llen, sstrip, "\u2007" * rlen))
441 |
442 | return "\\N".join(
443 | (
444 | ReplaceLeadingSpace(i) or " "
445 | for i in str(s).replace("\\", "\\\\").replace("{", "\\{").replace("}", "\\}").split("\n")
446 | )
447 | )
448 |
449 |
450 | def CalculateLength(s):
451 | return max(map(len, s.split("\n"))) # May not be accurate
452 |
453 |
454 | def ConvertTimestamp(timestamp):
455 | timestamp = round(timestamp * 100.0)
456 | hour, minute = divmod(timestamp, 360000)
457 | minute, second = divmod(minute, 6000)
458 | second, centsecond = divmod(second, 100)
459 | return "%d:%02d:%02d.%02d" % (int(hour), int(minute), int(second), int(centsecond))
460 |
461 |
462 | def ConvertColor(RGB, width=1280, height=576):
463 | if RGB == 0x000000:
464 | return "000000"
465 | elif RGB == 0xFFFFFF:
466 | return "FFFFFF"
467 | R = (RGB >> 16) & 0xFF
468 | G = (RGB >> 8) & 0xFF
469 | B = RGB & 0xFF
470 | if width < 1280 and height < 576:
471 | return "%02X%02X%02X" % (B, G, R)
472 | else: # VobSub always uses BT.601 colorspace, convert to BT.709
473 | ClipByte = lambda x: 255 if x > 255 else 0 if x < 0 else round(x)
474 | return "%02X%02X%02X" % (
475 | ClipByte(R * 0.00956384088080656 + G * 0.03217254540203729 + B * 0.95826361371715607),
476 | ClipByte(R * -0.10493933142075390 + G * 1.17231478191855154 + B * -0.06737545049779757),
477 | ClipByte(R * 0.91348912373987645 + G * 0.07858536372532510 + B * 0.00792551253479842),
478 | )
479 |
480 |
481 | def ConvertType2(row, height, bottomReserved):
482 | return height - bottomReserved - row
483 |
484 |
485 | def FilterBadChars(string: str) -> str:
486 | return re.sub("[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]", "\ufffd", string)
487 |
488 |
489 | class SafeList(list):
490 | def get(self, index, default=None):
491 | try:
492 | return self[index]
493 | except IndexError:
494 | return default
495 |
496 |
497 | @export
498 | def Proto2ASS(
499 | danmaku_list: Union[List[Union[str, bytes]], Union[str, bytes]],
500 | stage_width: int,
501 | stage_height: int,
502 | reserve_blank: float = 0,
503 | font_face: str = "sans-serif",
504 | font_size: float = 25.0,
505 | alpha: float = 1.0,
506 | duration_marquee: float = 5.0,
507 | duration_still: float = 5.0,
508 | comment_filter: Optional[str] = None,
509 | reduced: bool = False
510 | ) -> str:
511 | comment_filters: List[str] = [comment_filter] if comment_filter is not None else []
512 | filters_regex = []
513 | for comment_filter in comment_filters:
514 | try:
515 | if comment_filter:
516 | filters_regex.append(re.compile(comment_filter))
517 | except re.error:
518 | raise ValueError("Invalid regular expression: %s" % comment_filter)
519 |
520 | comments: List[Comment] = []
521 | if not isinstance(danmaku_list, list):
522 | danmaku_list = [danmaku_list]
523 | for danmaku in danmaku_list:
524 | if isinstance(danmaku, str):
525 | raise Exception("Argument 'inputs' type must be bytes.")
526 | comments.extend(ReadCommentsBilibiliProtobuf(danmaku, font_size))
527 | comments.sort()
528 | return ProcessComments(
529 | comments,
530 | stage_width,
531 | stage_height,
532 | reserve_blank,
533 | font_face,
534 | font_size,
535 | alpha,
536 | duration_marquee,
537 | duration_still,
538 | filters_regex,
539 | reduced,
540 | )
541 |
--------------------------------------------------------------------------------
/bilibili/protobuf/dm_pb2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by the protocol buffer compiler. DO NOT EDIT!
3 | # source: dm.proto (https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/grpc_api/bilibili/community/service/dm/v1/dm.proto)
4 | # Protobuf Python Version: 5.26.1
5 | """Generated protocol buffer code."""
6 | from google.protobuf import descriptor as _descriptor
7 | from google.protobuf import descriptor_pool as _descriptor_pool
8 | from google.protobuf import symbol_database as _symbol_database
9 | from google.protobuf.internal import builder as _builder
10 |
11 | # @@protoc_insertion_point(imports)
12 |
13 | _sym_db = _symbol_database.Default()
14 |
15 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
16 | b'\n\x08\x64m.proto\x12 bilibili.community.service.dm.v1\"d\n\x06\x41vatar\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x41\n\x0b\x61vatar_type\x18\x03 \x01(\x0e\x32,.bilibili.community.service.dm.v1.AvatarType\"#\n\x06\x42ubble\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\"\xc6\x01\n\x08\x42ubbleV2\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x41\n\x0b\x62ubble_type\x18\x03 \x01(\x0e\x32,.bilibili.community.service.dm.v1.BubbleType\x12\x15\n\rexposure_once\x18\x04 \x01(\x08\x12\x45\n\rexposure_type\x18\x05 \x01(\x0e\x32..bilibili.community.service.dm.v1.ExposureType\"&\n\x06\x42utton\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\x05\"X\n\x0e\x42uzzwordConfig\x12\x46\n\x08keywords\x18\x01 \x03(\x0b\x32\x34.bilibili.community.service.dm.v1.BuzzwordShowConfig\"x\n\x12\x42uzzwordShowConfig\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06schema\x18\x02 \x01(\t\x12\x0e\n\x06source\x18\x03 \x01(\x05\x12\n\n\x02id\x18\x04 \x01(\x03\x12\x13\n\x0b\x62uzzword_id\x18\x05 \x01(\x03\x12\x13\n\x0bschema_type\x18\x06 \x01(\x05\"{\n\x08\x43heckBox\x12\x0c\n\x04text\x18\x01 \x01(\t\x12<\n\x04type\x18\x02 \x01(\x0e\x32..bilibili.community.service.dm.v1.CheckboxType\x12\x15\n\rdefault_value\x18\x03 \x01(\x08\x12\x0c\n\x04show\x18\x04 \x01(\x08\"?\n\nCheckBoxV2\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\x05\x12\x15\n\rdefault_value\x18\x03 \x01(\x08\"\x82\x02\n\x0b\x43lickButton\x12\x15\n\rportrait_text\x18\x01 \x03(\t\x12\x16\n\x0elandscape_text\x18\x02 \x03(\t\x12\x1b\n\x13portrait_text_focus\x18\x03 \x03(\t\x12\x1c\n\x14landscape_text_focus\x18\x04 \x03(\t\x12\x41\n\x0brender_type\x18\x05 \x01(\x0e\x32,.bilibili.community.service.dm.v1.RenderType\x12\x0c\n\x04show\x18\x06 \x01(\x08\x12\x38\n\x06\x62ubble\x18\x07 \x01(\x0b\x32(.bilibili.community.service.dm.v1.Bubble\"\xd5\x01\n\rClickButtonV2\x12\x15\n\rportrait_text\x18\x01 \x03(\t\x12\x16\n\x0elandscape_text\x18\x02 \x03(\t\x12\x1b\n\x13portrait_text_focus\x18\x03 \x03(\t\x12\x1c\n\x14landscape_text_focus\x18\x04 \x03(\t\x12\x13\n\x0brender_type\x18\x05 \x01(\x05\x12\x17\n\x0ftext_input_post\x18\x06 \x01(\x08\x12\x15\n\rexposure_once\x18\x07 \x01(\x08\x12\x15\n\rexposure_type\x18\x08 \x01(\x05\"\xa1\x01\n\tCommandDm\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x0b\n\x03oid\x18\x02 \x01(\x03\x12\x0b\n\x03mid\x18\x03 \x01(\t\x12\x0f\n\x07\x63ommand\x18\x04 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x05 \x01(\t\x12\x10\n\x08progress\x18\x06 \x01(\x05\x12\r\n\x05\x63time\x18\x07 \x01(\t\x12\r\n\x05mtime\x18\x08 \x01(\t\x12\r\n\x05\x65xtra\x18\t \x01(\t\x12\r\n\x05idStr\x18\n \x01(\t\"P\n\rDanmakuAIFlag\x12?\n\x08\x64m_flags\x18\x01 \x03(\x0b\x32-.bilibili.community.service.dm.v1.DanmakuFlag\"\xad\x02\n\x0b\x44\x61nmakuElem\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x10\n\x08progress\x18\x02 \x01(\x05\x12\x0c\n\x04mode\x18\x03 \x01(\x05\x12\x10\n\x08\x66ontsize\x18\x04 \x01(\x05\x12\r\n\x05\x63olor\x18\x05 \x01(\r\x12\x0f\n\x07midHash\x18\x06 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x07 \x01(\t\x12\r\n\x05\x63time\x18\x08 \x01(\x03\x12\x0e\n\x06weight\x18\t \x01(\x05\x12\x0e\n\x06\x61\x63tion\x18\n \x01(\t\x12\x0c\n\x04pool\x18\x0b \x01(\x05\x12\r\n\x05idStr\x18\x0c \x01(\t\x12\x0c\n\x04\x61ttr\x18\r \x01(\x05\x12\x11\n\tanimation\x18\x16 \x01(\t\x12\x42\n\x08\x63olorful\x18\x18 \x01(\x0e\x32\x30.bilibili.community.service.dm.v1.DmColorfulType\")\n\x0b\x44\x61nmakuFlag\x12\x0c\n\x04\x64mid\x18\x01 \x01(\x03\x12\x0c\n\x04\x66lag\x18\x02 \x01(\r\"K\n\x11\x44\x61nmakuFlagConfig\x12\x10\n\x08rec_flag\x18\x01 \x01(\x05\x12\x10\n\x08rec_text\x18\x02 \x01(\t\x12\x12\n\nrec_switch\x18\x03 \x01(\x05\"\xe4\x06\n\x18\x44\x61nmuDefaultPlayerConfig\x12)\n!player_danmaku_use_default_config\x18\x01 \x01(\x08\x12,\n$player_danmaku_ai_recommended_switch\x18\x04 \x01(\x08\x12+\n#player_danmaku_ai_recommended_level\x18\x05 \x01(\x05\x12\x1f\n\x17player_danmaku_blocktop\x18\x06 \x01(\x08\x12\"\n\x1aplayer_danmaku_blockscroll\x18\x07 \x01(\x08\x12\"\n\x1aplayer_danmaku_blockbottom\x18\x08 \x01(\x08\x12$\n\x1cplayer_danmaku_blockcolorful\x18\t \x01(\x08\x12\"\n\x1aplayer_danmaku_blockrepeat\x18\n \x01(\x08\x12#\n\x1bplayer_danmaku_blockspecial\x18\x0b \x01(\x08\x12\x1e\n\x16player_danmaku_opacity\x18\x0c \x01(\x02\x12$\n\x1cplayer_danmaku_scalingfactor\x18\r \x01(\x02\x12\x1d\n\x15player_danmaku_domain\x18\x0e \x01(\x02\x12\x1c\n\x14player_danmaku_speed\x18\x0f \x01(\x05\x12$\n\x1cinline_player_danmaku_switch\x18\x10 \x01(\x08\x12)\n!player_danmaku_senior_mode_switch\x18\x11 \x01(\x05\x12.\n&player_danmaku_ai_recommended_level_v2\x18\x12 \x01(\x05\x12\x98\x01\n*player_danmaku_ai_recommended_level_v2_map\x18\x13 \x03(\x0b\x32\x64.bilibili.community.service.dm.v1.DanmuDefaultPlayerConfig.PlayerDanmakuAiRecommendedLevelV2MapEntry\x1aK\n)PlayerDanmakuAiRecommendedLevelV2MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"\x8f\x08\n\x11\x44\x61nmuPlayerConfig\x12\x1d\n\x15player_danmaku_switch\x18\x01 \x01(\x08\x12\"\n\x1aplayer_danmaku_switch_save\x18\x02 \x01(\x08\x12)\n!player_danmaku_use_default_config\x18\x03 \x01(\x08\x12,\n$player_danmaku_ai_recommended_switch\x18\x04 \x01(\x08\x12+\n#player_danmaku_ai_recommended_level\x18\x05 \x01(\x05\x12\x1f\n\x17player_danmaku_blocktop\x18\x06 \x01(\x08\x12\"\n\x1aplayer_danmaku_blockscroll\x18\x07 \x01(\x08\x12\"\n\x1aplayer_danmaku_blockbottom\x18\x08 \x01(\x08\x12$\n\x1cplayer_danmaku_blockcolorful\x18\t \x01(\x08\x12\"\n\x1aplayer_danmaku_blockrepeat\x18\n \x01(\x08\x12#\n\x1bplayer_danmaku_blockspecial\x18\x0b \x01(\x08\x12\x1e\n\x16player_danmaku_opacity\x18\x0c \x01(\x02\x12$\n\x1cplayer_danmaku_scalingfactor\x18\r \x01(\x02\x12\x1d\n\x15player_danmaku_domain\x18\x0e \x01(\x02\x12\x1c\n\x14player_danmaku_speed\x18\x0f \x01(\x05\x12&\n\x1eplayer_danmaku_enableblocklist\x18\x10 \x01(\x08\x12$\n\x1cinline_player_danmaku_switch\x18\x11 \x01(\x08\x12$\n\x1cinline_player_danmaku_config\x18\x12 \x01(\x05\x12&\n\x1eplayer_danmaku_ios_switch_save\x18\x13 \x01(\x05\x12)\n!player_danmaku_senior_mode_switch\x18\x14 \x01(\x05\x12.\n&player_danmaku_ai_recommended_level_v2\x18\x15 \x01(\x05\x12\x91\x01\n*player_danmaku_ai_recommended_level_v2_map\x18\x16 \x03(\x0b\x32].bilibili.community.service.dm.v1.DanmuPlayerConfig.PlayerDanmakuAiRecommendedLevelV2MapEntry\x1aK\n)PlayerDanmakuAiRecommendedLevelV2MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"0\n\x16\x44\x61nmuPlayerConfigPanel\x12\x16\n\x0eselection_text\x18\x01 \x01(\t\"K\n\x18\x44\x61nmuPlayerDynamicConfig\x12\x10\n\x08progress\x18\x01 \x01(\x05\x12\x1d\n\x15player_danmaku_domain\x18\x0e \x01(\x02\"\x90\x03\n\x15\x44\x61nmuPlayerViewConfig\x12\x61\n\x1d\x64\x61nmuku_default_player_config\x18\x01 \x01(\x0b\x32:.bilibili.community.service.dm.v1.DanmuDefaultPlayerConfig\x12R\n\x15\x64\x61nmuku_player_config\x18\x02 \x01(\x0b\x32\x33.bilibili.community.service.dm.v1.DanmuPlayerConfig\x12\x61\n\x1d\x64\x61nmuku_player_dynamic_config\x18\x03 \x03(\x0b\x32:.bilibili.community.service.dm.v1.DanmuPlayerDynamicConfig\x12]\n\x1b\x64\x61nmuku_player_config_panel\x18\x04 \x01(\x0b\x32\x38.bilibili.community.service.dm.v1.DanmuPlayerConfigPanel\"\xd8\x04\n\x14\x44\x61nmuWebPlayerConfig\x12\x11\n\tdm_switch\x18\x01 \x01(\x08\x12\x11\n\tai_switch\x18\x02 \x01(\x08\x12\x10\n\x08\x61i_level\x18\x03 \x01(\x05\x12\x10\n\x08\x62locktop\x18\x04 \x01(\x08\x12\x13\n\x0b\x62lockscroll\x18\x05 \x01(\x08\x12\x13\n\x0b\x62lockbottom\x18\x06 \x01(\x08\x12\x12\n\nblockcolor\x18\x07 \x01(\x08\x12\x14\n\x0c\x62lockspecial\x18\x08 \x01(\x08\x12\x14\n\x0cpreventshade\x18\t \x01(\x08\x12\r\n\x05\x64mask\x18\n \x01(\x08\x12\x0f\n\x07opacity\x18\x0b \x01(\x02\x12\x0e\n\x06\x64marea\x18\x0c \x01(\x05\x12\x11\n\tspeedplus\x18\r \x01(\x02\x12\x10\n\x08\x66ontsize\x18\x0e \x01(\x02\x12\x12\n\nscreensync\x18\x0f \x01(\x08\x12\x11\n\tspeedsync\x18\x10 \x01(\x08\x12\x12\n\nfontfamily\x18\x11 \x01(\t\x12\x0c\n\x04\x62old\x18\x12 \x01(\x08\x12\x12\n\nfontborder\x18\x13 \x01(\x05\x12\x11\n\tdraw_type\x18\x14 \x01(\t\x12\x1a\n\x12senior_mode_switch\x18\x15 \x01(\x05\x12\x13\n\x0b\x61i_level_v2\x18\x16 \x01(\x05\x12\x61\n\x0f\x61i_level_v2_map\x18\x17 \x03(\x0b\x32H.bilibili.community.service.dm.v1.DanmuWebPlayerConfig.AiLevelV2MapEntry\x1a\x33\n\x11\x41iLevelV2MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"Y\n\nDmColorful\x12>\n\x04type\x18\x01 \x01(\x0e\x32\x30.bilibili.community.service.dm.v1.DmColorfulType\x12\x0b\n\x03src\x18\x02 \x01(\t\"A\n\x0f\x44mExpoReportReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0b\n\x03oid\x18\x02 \x01(\x03\x12\r\n\x05spmid\x18\x04 \x01(\t\"\x11\n\x0f\x44mExpoReportRes\"\xe3\x0c\n\x11\x44mPlayerConfigReq\x12\n\n\x02ts\x18\x01 \x01(\x03\x12\x45\n\x06switch\x18\x02 \x01(\x0b\x32\x35.bilibili.community.service.dm.v1.PlayerDanmakuSwitch\x12N\n\x0bswitch_save\x18\x03 \x01(\x0b\x32\x39.bilibili.community.service.dm.v1.PlayerDanmakuSwitchSave\x12[\n\x12use_default_config\x18\x04 \x01(\x0b\x32?.bilibili.community.service.dm.v1.PlayerDanmakuUseDefaultConfig\x12\x61\n\x15\x61i_recommended_switch\x18\x05 \x01(\x0b\x32\x42.bilibili.community.service.dm.v1.PlayerDanmakuAiRecommendedSwitch\x12_\n\x14\x61i_recommended_level\x18\x06 \x01(\x0b\x32\x41.bilibili.community.service.dm.v1.PlayerDanmakuAiRecommendedLevel\x12I\n\x08\x62locktop\x18\x07 \x01(\x0b\x32\x37.bilibili.community.service.dm.v1.PlayerDanmakuBlocktop\x12O\n\x0b\x62lockscroll\x18\x08 \x01(\x0b\x32:.bilibili.community.service.dm.v1.PlayerDanmakuBlockscroll\x12O\n\x0b\x62lockbottom\x18\t \x01(\x0b\x32:.bilibili.community.service.dm.v1.PlayerDanmakuBlockbottom\x12S\n\rblockcolorful\x18\n \x01(\x0b\x32<.bilibili.community.service.dm.v1.PlayerDanmakuBlockcolorful\x12O\n\x0b\x62lockrepeat\x18\x0b \x01(\x0b\x32:.bilibili.community.service.dm.v1.PlayerDanmakuBlockrepeat\x12Q\n\x0c\x62lockspecial\x18\x0c \x01(\x0b\x32;.bilibili.community.service.dm.v1.PlayerDanmakuBlockspecial\x12G\n\x07opacity\x18\r \x01(\x0b\x32\x36.bilibili.community.service.dm.v1.PlayerDanmakuOpacity\x12S\n\rscalingfactor\x18\x0e \x01(\x0b\x32<.bilibili.community.service.dm.v1.PlayerDanmakuScalingfactor\x12\x45\n\x06\x64omain\x18\x0f \x01(\x0b\x32\x35.bilibili.community.service.dm.v1.PlayerDanmakuDomain\x12\x43\n\x05speed\x18\x10 \x01(\x0b\x32\x34.bilibili.community.service.dm.v1.PlayerDanmakuSpeed\x12W\n\x0f\x65nableblocklist\x18\x11 \x01(\x0b\x32>.bilibili.community.service.dm.v1.PlayerDanmakuEnableblocklist\x12^\n\x19inlinePlayerDanmakuSwitch\x18\x12 \x01(\x0b\x32;.bilibili.community.service.dm.v1.InlinePlayerDanmakuSwitch\x12[\n\x12senior_mode_switch\x18\x13 \x01(\x0b\x32?.bilibili.community.service.dm.v1.PlayerDanmakuSeniorModeSwitch\x12\x64\n\x17\x61i_recommended_level_v2\x18\x14 \x01(\x0b\x32\x43.bilibili.community.service.dm.v1.PlayerDanmakuAiRecommendedLevelV2\"/\n\x0b\x44mSegConfig\x12\x11\n\tpage_size\x18\x01 \x01(\x03\x12\r\n\x05total\x18\x02 \x01(\x03\"\xe4\x01\n\x10\x44mSegMobileReply\x12<\n\x05\x65lems\x18\x01 \x03(\x0b\x32-.bilibili.community.service.dm.v1.DanmakuElem\x12\r\n\x05state\x18\x02 \x01(\x05\x12@\n\x07\x61i_flag\x18\x03 \x01(\x0b\x32/.bilibili.community.service.dm.v1.DanmakuAIFlag\x12\x41\n\x0b\x63olorfulSrc\x18\x05 \x03(\x0b\x32,.bilibili.community.service.dm.v1.DmColorful\"\xa6\x01\n\x0e\x44mSegMobileReq\x12\x0b\n\x03pid\x18\x01 \x01(\x03\x12\x0b\n\x03oid\x18\x02 \x01(\x03\x12\x0c\n\x04type\x18\x03 \x01(\x05\x12\x15\n\rsegment_index\x18\x04 \x01(\x03\x12\x16\n\x0eteenagers_mode\x18\x05 \x01(\x05\x12\n\n\x02ps\x18\x06 \x01(\x03\x12\n\n\x02pe\x18\x07 \x01(\x03\x12\x11\n\tpull_mode\x18\x08 \x01(\x05\x12\x12\n\nfrom_scene\x18\t \x01(\x05\"]\n\rDmSegOttReply\x12\x0e\n\x06\x63losed\x18\x01 \x01(\x08\x12<\n\x05\x65lems\x18\x02 \x03(\x0b\x32-.bilibili.community.service.dm.v1.DanmakuElem\"L\n\x0b\x44mSegOttReq\x12\x0b\n\x03pid\x18\x01 \x01(\x03\x12\x0b\n\x03oid\x18\x02 \x01(\x03\x12\x0c\n\x04type\x18\x03 \x01(\x05\x12\x15\n\rsegment_index\x18\x04 \x01(\x03\"]\n\rDmSegSDKReply\x12\x0e\n\x06\x63losed\x18\x01 \x01(\x08\x12<\n\x05\x65lems\x18\x02 \x03(\x0b\x32-.bilibili.community.service.dm.v1.DanmakuElem\"L\n\x0b\x44mSegSDKReq\x12\x0b\n\x03pid\x18\x01 \x01(\x03\x12\x0b\n\x03oid\x18\x02 \x01(\x03\x12\x0c\n\x04type\x18\x03 \x01(\x05\x12\x15\n\rsegment_index\x18\x04 \x01(\x03\"\xde\x06\n\x0b\x44mViewReply\x12\x0e\n\x06\x63losed\x18\x01 \x01(\x08\x12\x39\n\x04mask\x18\x02 \x01(\x0b\x32+.bilibili.community.service.dm.v1.VideoMask\x12\x41\n\x08subtitle\x18\x03 \x01(\x0b\x32/.bilibili.community.service.dm.v1.VideoSubtitle\x12\x13\n\x0bspecial_dms\x18\x04 \x03(\t\x12\x44\n\x07\x61i_flag\x18\x05 \x01(\x0b\x32\x33.bilibili.community.service.dm.v1.DanmakuFlagConfig\x12N\n\rplayer_config\x18\x06 \x01(\x0b\x32\x37.bilibili.community.service.dm.v1.DanmuPlayerViewConfig\x12\x16\n\x0esend_box_style\x18\x07 \x01(\x05\x12\r\n\x05\x61llow\x18\x08 \x01(\x08\x12\x11\n\tcheck_box\x18\t \x01(\t\x12\x1a\n\x12\x63heck_box_show_msg\x18\n \x01(\t\x12\x18\n\x10text_placeholder\x18\x0b \x01(\t\x12\x19\n\x11input_placeholder\x18\x0c \x01(\t\x12\x1d\n\x15report_filter_content\x18\r \x03(\t\x12\x41\n\x0b\x65xpo_report\x18\x0e \x01(\x0b\x32,.bilibili.community.service.dm.v1.ExpoReport\x12I\n\x0f\x62uzzword_config\x18\x0f \x01(\x0b\x32\x30.bilibili.community.service.dm.v1.BuzzwordConfig\x12\x42\n\x0b\x65xpressions\x18\x10 \x03(\x0b\x32-.bilibili.community.service.dm.v1.Expressions\x12?\n\npost_panel\x18\x11 \x03(\x0b\x32+.bilibili.community.service.dm.v1.PostPanel\x12\x15\n\ractivity_meta\x18\x12 \x03(\t\x12\x42\n\x0bpost_panel2\x18\x13 \x03(\x0b\x32-.bilibili.community.service.dm.v1.PostPanelV2\"X\n\tDmViewReq\x12\x0b\n\x03pid\x18\x01 \x01(\x03\x12\x0b\n\x03oid\x18\x02 \x01(\x03\x12\x0c\n\x04type\x18\x03 \x01(\x05\x12\r\n\x05spmid\x18\x04 \x01(\t\x12\x14\n\x0cis_hard_boot\x18\x05 \x01(\x05\"\xc4\x04\n\x0e\x44mWebViewReply\x12\r\n\x05state\x18\x01 \x01(\x05\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x11\n\ttext_side\x18\x03 \x01(\t\x12=\n\x06\x64m_sge\x18\x04 \x01(\x0b\x32-.bilibili.community.service.dm.v1.DmSegConfig\x12\x41\n\x04\x66lag\x18\x05 \x01(\x0b\x32\x33.bilibili.community.service.dm.v1.DanmakuFlagConfig\x12\x13\n\x0bspecial_dms\x18\x06 \x03(\t\x12\x11\n\tcheck_box\x18\x07 \x01(\x08\x12\r\n\x05\x63ount\x18\x08 \x01(\x03\x12?\n\ncommandDms\x18\t \x03(\x0b\x32+.bilibili.community.service.dm.v1.CommandDm\x12M\n\rplayer_config\x18\n \x01(\x0b\x32\x36.bilibili.community.service.dm.v1.DanmuWebPlayerConfig\x12\x1d\n\x15report_filter_content\x18\x0b \x03(\t\x12\x42\n\x0b\x65xpressions\x18\x0c \x03(\x0b\x32-.bilibili.community.service.dm.v1.Expressions\x12?\n\npost_panel\x18\r \x03(\x0b\x32+.bilibili.community.service.dm.v1.PostPanel\x12\x15\n\ractivity_meta\x18\x0e \x03(\t\"*\n\nExpoReport\x12\x1c\n\x14should_report_at_end\x18\x01 \x01(\x08\"d\n\nExpression\x12\x0f\n\x07keyword\x18\x01 \x03(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x38\n\x06period\x18\x03 \x03(\x0b\x32(.bilibili.community.service.dm.v1.Period\"I\n\x0b\x45xpressions\x12:\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32,.bilibili.community.service.dm.v1.Expression\"*\n\x19InlinePlayerDanmakuSwitch\x12\r\n\x05value\x18\x01 \x01(\x08\"\'\n\x05Label\x12\r\n\x05title\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x03(\t\"W\n\x07LabelV2\x12\r\n\x05title\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x03(\t\x12\x15\n\rexposure_once\x18\x03 \x01(\x08\x12\x15\n\rexposure_type\x18\x04 \x01(\x05\"$\n\x06Period\x12\r\n\x05start\x18\x01 \x01(\x03\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x03\"0\n\x1fPlayerDanmakuAiRecommendedLevel\x12\r\n\x05value\x18\x01 \x01(\x08\"2\n!PlayerDanmakuAiRecommendedLevelV2\x12\r\n\x05value\x18\x01 \x01(\x05\"1\n PlayerDanmakuAiRecommendedSwitch\x12\r\n\x05value\x18\x01 \x01(\x08\")\n\x18PlayerDanmakuBlockbottom\x12\r\n\x05value\x18\x01 \x01(\x08\"+\n\x1aPlayerDanmakuBlockcolorful\x12\r\n\x05value\x18\x01 \x01(\x08\")\n\x18PlayerDanmakuBlockrepeat\x12\r\n\x05value\x18\x01 \x01(\x08\")\n\x18PlayerDanmakuBlockscroll\x12\r\n\x05value\x18\x01 \x01(\x08\"*\n\x19PlayerDanmakuBlockspecial\x12\r\n\x05value\x18\x01 \x01(\x08\"&\n\x15PlayerDanmakuBlocktop\x12\r\n\x05value\x18\x01 \x01(\x08\"$\n\x13PlayerDanmakuDomain\x12\r\n\x05value\x18\x01 \x01(\x02\"-\n\x1cPlayerDanmakuEnableblocklist\x12\r\n\x05value\x18\x01 \x01(\x08\"%\n\x14PlayerDanmakuOpacity\x12\r\n\x05value\x18\x01 \x01(\x02\"+\n\x1aPlayerDanmakuScalingfactor\x12\r\n\x05value\x18\x01 \x01(\x02\".\n\x1dPlayerDanmakuSeniorModeSwitch\x12\r\n\x05value\x18\x01 \x01(\x05\"#\n\x12PlayerDanmakuSpeed\x12\r\n\x05value\x18\x01 \x01(\x05\"8\n\x13PlayerDanmakuSwitch\x12\r\n\x05value\x18\x01 \x01(\x08\x12\x12\n\ncan_ignore\x18\x02 \x01(\x08\"(\n\x17PlayerDanmakuSwitchSave\x12\r\n\x05value\x18\x01 \x01(\x08\".\n\x1dPlayerDanmakuUseDefaultConfig\x12\r\n\x05value\x18\x01 \x01(\x08\"\x8c\x03\n\tPostPanel\x12\r\n\x05start\x18\x01 \x01(\x03\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x03\x12\x10\n\x08priority\x18\x03 \x01(\x03\x12\x0e\n\x06\x62iz_id\x18\x04 \x01(\x03\x12\x44\n\x08\x62iz_type\x18\x05 \x01(\x0e\x32\x32.bilibili.community.service.dm.v1.PostPanelBizType\x12\x43\n\x0c\x63lick_button\x18\x06 \x01(\x0b\x32-.bilibili.community.service.dm.v1.ClickButton\x12?\n\ntext_input\x18\x07 \x01(\x0b\x32+.bilibili.community.service.dm.v1.TextInput\x12=\n\tcheck_box\x18\x08 \x01(\x0b\x32*.bilibili.community.service.dm.v1.CheckBox\x12\x36\n\x05toast\x18\t \x01(\x0b\x32\'.bilibili.community.service.dm.v1.Toast\"\xcb\x03\n\x0bPostPanelV2\x12\r\n\x05start\x18\x01 \x01(\x03\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x03\x12\x10\n\x08\x62iz_type\x18\x03 \x01(\x05\x12\x45\n\x0c\x63lick_button\x18\x04 \x01(\x0b\x32/.bilibili.community.service.dm.v1.ClickButtonV2\x12\x41\n\ntext_input\x18\x05 \x01(\x0b\x32-.bilibili.community.service.dm.v1.TextInputV2\x12?\n\tcheck_box\x18\x06 \x01(\x0b\x32,.bilibili.community.service.dm.v1.CheckBoxV2\x12\x38\n\x05toast\x18\x07 \x01(\x0b\x32).bilibili.community.service.dm.v1.ToastV2\x12:\n\x06\x62ubble\x18\x08 \x01(\x0b\x32*.bilibili.community.service.dm.v1.BubbleV2\x12\x38\n\x05label\x18\t \x01(\x0b\x32).bilibili.community.service.dm.v1.LabelV2\x12\x13\n\x0bpost_status\x18\n \x01(\x05\")\n\x08Response\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xf9\x02\n\x0cSubtitleItem\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x0e\n\x06id_str\x18\x02 \x01(\t\x12\x0b\n\x03lan\x18\x03 \x01(\t\x12\x0f\n\x07lan_doc\x18\x04 \x01(\t\x12\x14\n\x0csubtitle_url\x18\x05 \x01(\t\x12:\n\x06\x61uthor\x18\x06 \x01(\x0b\x32*.bilibili.community.service.dm.v1.UserInfo\x12<\n\x04type\x18\x07 \x01(\x0e\x32..bilibili.community.service.dm.v1.SubtitleType\x12\x15\n\rlan_doc_brief\x18\x08 \x01(\t\x12\x41\n\x07\x61i_type\x18\t \x01(\x0e\x32\x30.bilibili.community.service.dm.v1.SubtitleAiType\x12\x45\n\tai_status\x18\n \x01(\x0e\x32\x32.bilibili.community.service.dm.v1.SubtitleAiStatus\"\xe8\x02\n\tTextInput\x12\x1c\n\x14portrait_placeholder\x18\x01 \x03(\t\x12\x1d\n\x15landscape_placeholder\x18\x02 \x03(\t\x12\x41\n\x0brender_type\x18\x03 \x01(\x0e\x32,.bilibili.community.service.dm.v1.RenderType\x12\x18\n\x10placeholder_post\x18\x04 \x01(\x08\x12\x0c\n\x04show\x18\x05 \x01(\x08\x12\x38\n\x06\x61vatar\x18\x06 \x03(\x0b\x32(.bilibili.community.service.dm.v1.Avatar\x12\x41\n\x0bpost_status\x18\x07 \x01(\x0e\x32,.bilibili.community.service.dm.v1.PostStatus\x12\x36\n\x05label\x18\x08 \x01(\x0b\x32\'.bilibili.community.service.dm.v1.Label\"\xfb\x01\n\x0bTextInputV2\x12\x1c\n\x14portrait_placeholder\x18\x01 \x03(\t\x12\x1d\n\x15landscape_placeholder\x18\x02 \x03(\t\x12\x41\n\x0brender_type\x18\x03 \x01(\x0e\x32,.bilibili.community.service.dm.v1.RenderType\x12\x18\n\x10placeholder_post\x18\x04 \x01(\x08\x12\x38\n\x06\x61vatar\x18\x05 \x03(\x0b\x32(.bilibili.community.service.dm.v1.Avatar\x12\x18\n\x10text_input_limit\x18\x06 \x01(\x05\"o\n\x05Toast\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x10\n\x08\x64uration\x18\x02 \x01(\x05\x12\x0c\n\x04show\x18\x03 \x01(\x08\x12\x38\n\x06\x62utton\x18\x04 \x01(\x0b\x32(.bilibili.community.service.dm.v1.Button\"-\n\rToastButtonV2\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\x05\"s\n\x07ToastV2\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x10\n\x08\x64uration\x18\x02 \x01(\x05\x12H\n\x0ftoast_button_v2\x18\x03 \x01(\x0b\x32/.bilibili.community.service.dm.v1.ToastButtonV2\"\\\n\x08UserInfo\x12\x0b\n\x03mid\x18\x01 \x01(\x03\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0b\n\x03sex\x18\x03 \x01(\t\x12\x0c\n\x04\x66\x61\x63\x65\x18\x04 \x01(\t\x12\x0c\n\x04sign\x18\x05 \x01(\t\x12\x0c\n\x04rank\x18\x06 \x01(\x05\"S\n\tVideoMask\x12\x0b\n\x03\x63id\x18\x01 \x01(\x03\x12\x0c\n\x04plat\x18\x02 \x01(\x05\x12\x0b\n\x03\x66ps\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x03\x12\x10\n\x08mask_url\x18\x05 \x01(\t\"o\n\rVideoSubtitle\x12\x0b\n\x03lan\x18\x01 \x01(\t\x12\x0e\n\x06lanDoc\x18\x02 \x01(\t\x12\x41\n\tsubtitles\x18\x03 \x03(\x0b\x32..bilibili.community.service.dm.v1.SubtitleItem*3\n\nAvatarType\x12\x12\n\x0e\x41vatarTypeNone\x10\x00\x12\x11\n\rAvatarTypeNFT\x10\x01*Y\n\nBubbleType\x12\x12\n\x0e\x42ubbleTypeNone\x10\x00\x12\x19\n\x15\x42ubbleTypeClickButton\x10\x01\x12\x1c\n\x18\x42ubbleTypeDmSettingPanel\x10\x02*X\n\x0c\x43heckboxType\x12\x14\n\x10\x43heckboxTypeNone\x10\x00\x12\x19\n\x15\x43heckboxTypeEncourage\x10\x01\x12\x17\n\x13\x43heckboxTypeColorDM\x10\x02*L\n\tDMAttrBit\x12\x14\n\x10\x44MAttrBitProtect\x10\x00\x12\x15\n\x11\x44MAttrBitFromLive\x10\x01\x12\x12\n\x0e\x44MAttrHighLike\x10\x02*5\n\x0e\x44mColorfulType\x12\x0c\n\x08NoneType\x10\x00\x12\x15\n\x0fVipGradualColor\x10\xe1\xd4\x03*<\n\x0c\x45xposureType\x12\x14\n\x10\x45xposureTypeNone\x10\x00\x12\x16\n\x12\x45xposureTypeDMSend\x10\x01*\xc1\x01\n\x10PostPanelBizType\x12\x18\n\x14PostPanelBizTypeNone\x10\x00\x12\x1d\n\x19PostPanelBizTypeEncourage\x10\x01\x12\x1b\n\x17PostPanelBizTypeColorDM\x10\x02\x12\x19\n\x15PostPanelBizTypeNFTDM\x10\x03\x12\x1d\n\x19PostPanelBizTypeFragClose\x10\x04\x12\x1d\n\x19PostPanelBizTypeRecommend\x10\x05*8\n\nPostStatus\x12\x14\n\x10PostStatusNormal\x10\x00\x12\x14\n\x10PostStatusClosed\x10\x01*N\n\nRenderType\x12\x12\n\x0eRenderTypeNone\x10\x00\x12\x14\n\x10RenderTypeSingle\x10\x01\x12\x16\n\x12RenderTypeRotation\x10\x02*6\n\x10SubtitleAiStatus\x12\x08\n\x04None\x10\x00\x12\x0c\n\x08\x45xposure\x10\x01\x12\n\n\x06\x41ssist\x10\x02*+\n\x0eSubtitleAiType\x12\n\n\x06Normal\x10\x00\x12\r\n\tTranslate\x10\x01*\x1e\n\x0cSubtitleType\x12\x06\n\x02\x43\x43\x10\x00\x12\x06\n\x02\x41I\x10\x01*N\n\x11ToastFunctionType\x12\x19\n\x15ToastFunctionTypeNone\x10\x00\x12\x1e\n\x1aToastFunctionTypePostPanel\x10\x01\x32\xa0\x05\n\x02\x44M\x12s\n\x0b\x44mSegMobile\x12\x30.bilibili.community.service.dm.v1.DmSegMobileReq\x1a\x32.bilibili.community.service.dm.v1.DmSegMobileReply\x12\x64\n\x06\x44mView\x12+.bilibili.community.service.dm.v1.DmViewReq\x1a-.bilibili.community.service.dm.v1.DmViewReply\x12q\n\x0e\x44mPlayerConfig\x12\x33.bilibili.community.service.dm.v1.DmPlayerConfigReq\x1a*.bilibili.community.service.dm.v1.Response\x12j\n\x08\x44mSegOtt\x12-.bilibili.community.service.dm.v1.DmSegOttReq\x1a/.bilibili.community.service.dm.v1.DmSegOttReply\x12j\n\x08\x44mSegSDK\x12-.bilibili.community.service.dm.v1.DmSegSDKReq\x1a/.bilibili.community.service.dm.v1.DmSegSDKReply\x12t\n\x0c\x44mExpoReport\x12\x31.bilibili.community.service.dm.v1.DmExpoReportReq\x1a\x31.bilibili.community.service.dm.v1.DmExpoReportResb\x06proto3')
17 |
18 | _globals = globals()
19 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
20 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dm_pb2', _globals)
21 | if not _descriptor._USE_C_DESCRIPTORS:
22 | DESCRIPTOR._loaded_options = None
23 | _globals['_DANMUDEFAULTPLAYERCONFIG_PLAYERDANMAKUAIRECOMMENDEDLEVELV2MAPENTRY']._loaded_options = None
24 | _globals['_DANMUDEFAULTPLAYERCONFIG_PLAYERDANMAKUAIRECOMMENDEDLEVELV2MAPENTRY']._serialized_options = b'8\001'
25 | _globals['_DANMUPLAYERCONFIG_PLAYERDANMAKUAIRECOMMENDEDLEVELV2MAPENTRY']._loaded_options = None
26 | _globals['_DANMUPLAYERCONFIG_PLAYERDANMAKUAIRECOMMENDEDLEVELV2MAPENTRY']._serialized_options = b'8\001'
27 | _globals['_DANMUWEBPLAYERCONFIG_AILEVELV2MAPENTRY']._loaded_options = None
28 | _globals['_DANMUWEBPLAYERCONFIG_AILEVELV2MAPENTRY']._serialized_options = b'8\001'
29 | _globals['_AVATARTYPE']._serialized_start = 12885
30 | _globals['_AVATARTYPE']._serialized_end = 12936
31 | _globals['_BUBBLETYPE']._serialized_start = 12938
32 | _globals['_BUBBLETYPE']._serialized_end = 13027
33 | _globals['_CHECKBOXTYPE']._serialized_start = 13029
34 | _globals['_CHECKBOXTYPE']._serialized_end = 13117
35 | _globals['_DMATTRBIT']._serialized_start = 13119
36 | _globals['_DMATTRBIT']._serialized_end = 13195
37 | _globals['_DMCOLORFULTYPE']._serialized_start = 13197
38 | _globals['_DMCOLORFULTYPE']._serialized_end = 13250
39 | _globals['_EXPOSURETYPE']._serialized_start = 13252
40 | _globals['_EXPOSURETYPE']._serialized_end = 13312
41 | _globals['_POSTPANELBIZTYPE']._serialized_start = 13315
42 | _globals['_POSTPANELBIZTYPE']._serialized_end = 13508
43 | _globals['_POSTSTATUS']._serialized_start = 13510
44 | _globals['_POSTSTATUS']._serialized_end = 13566
45 | _globals['_RENDERTYPE']._serialized_start = 13568
46 | _globals['_RENDERTYPE']._serialized_end = 13646
47 | _globals['_SUBTITLEAISTATUS']._serialized_start = 13648
48 | _globals['_SUBTITLEAISTATUS']._serialized_end = 13702
49 | _globals['_SUBTITLEAITYPE']._serialized_start = 13704
50 | _globals['_SUBTITLEAITYPE']._serialized_end = 13747
51 | _globals['_SUBTITLETYPE']._serialized_start = 13749
52 | _globals['_SUBTITLETYPE']._serialized_end = 13779
53 | _globals['_TOASTFUNCTIONTYPE']._serialized_start = 13781
54 | _globals['_TOASTFUNCTIONTYPE']._serialized_end = 13859
55 | _globals['_AVATAR']._serialized_start = 46
56 | _globals['_AVATAR']._serialized_end = 146
57 | _globals['_BUBBLE']._serialized_start = 148
58 | _globals['_BUBBLE']._serialized_end = 183
59 | _globals['_BUBBLEV2']._serialized_start = 186
60 | _globals['_BUBBLEV2']._serialized_end = 384
61 | _globals['_BUTTON']._serialized_start = 386
62 | _globals['_BUTTON']._serialized_end = 424
63 | _globals['_BUZZWORDCONFIG']._serialized_start = 426
64 | _globals['_BUZZWORDCONFIG']._serialized_end = 514
65 | _globals['_BUZZWORDSHOWCONFIG']._serialized_start = 516
66 | _globals['_BUZZWORDSHOWCONFIG']._serialized_end = 636
67 | _globals['_CHECKBOX']._serialized_start = 638
68 | _globals['_CHECKBOX']._serialized_end = 761
69 | _globals['_CHECKBOXV2']._serialized_start = 763
70 | _globals['_CHECKBOXV2']._serialized_end = 826
71 | _globals['_CLICKBUTTON']._serialized_start = 829
72 | _globals['_CLICKBUTTON']._serialized_end = 1087
73 | _globals['_CLICKBUTTONV2']._serialized_start = 1090
74 | _globals['_CLICKBUTTONV2']._serialized_end = 1303
75 | _globals['_COMMANDDM']._serialized_start = 1306
76 | _globals['_COMMANDDM']._serialized_end = 1467
77 | _globals['_DANMAKUAIFLAG']._serialized_start = 1469
78 | _globals['_DANMAKUAIFLAG']._serialized_end = 1549
79 | _globals['_DANMAKUELEM']._serialized_start = 1552
80 | _globals['_DANMAKUELEM']._serialized_end = 1853
81 | _globals['_DANMAKUFLAG']._serialized_start = 1855
82 | _globals['_DANMAKUFLAG']._serialized_end = 1896
83 | _globals['_DANMAKUFLAGCONFIG']._serialized_start = 1898
84 | _globals['_DANMAKUFLAGCONFIG']._serialized_end = 1973
85 | _globals['_DANMUDEFAULTPLAYERCONFIG']._serialized_start = 1976
86 | _globals['_DANMUDEFAULTPLAYERCONFIG']._serialized_end = 2844
87 | _globals['_DANMUDEFAULTPLAYERCONFIG_PLAYERDANMAKUAIRECOMMENDEDLEVELV2MAPENTRY']._serialized_start = 2769
88 | _globals['_DANMUDEFAULTPLAYERCONFIG_PLAYERDANMAKUAIRECOMMENDEDLEVELV2MAPENTRY']._serialized_end = 2844
89 | _globals['_DANMUPLAYERCONFIG']._serialized_start = 2847
90 | _globals['_DANMUPLAYERCONFIG']._serialized_end = 3886
91 | _globals['_DANMUPLAYERCONFIG_PLAYERDANMAKUAIRECOMMENDEDLEVELV2MAPENTRY']._serialized_start = 2769
92 | _globals['_DANMUPLAYERCONFIG_PLAYERDANMAKUAIRECOMMENDEDLEVELV2MAPENTRY']._serialized_end = 2844
93 | _globals['_DANMUPLAYERCONFIGPANEL']._serialized_start = 3888
94 | _globals['_DANMUPLAYERCONFIGPANEL']._serialized_end = 3936
95 | _globals['_DANMUPLAYERDYNAMICCONFIG']._serialized_start = 3938
96 | _globals['_DANMUPLAYERDYNAMICCONFIG']._serialized_end = 4013
97 | _globals['_DANMUPLAYERVIEWCONFIG']._serialized_start = 4016
98 | _globals['_DANMUPLAYERVIEWCONFIG']._serialized_end = 4416
99 | _globals['_DANMUWEBPLAYERCONFIG']._serialized_start = 4419
100 | _globals['_DANMUWEBPLAYERCONFIG']._serialized_end = 5019
101 | _globals['_DANMUWEBPLAYERCONFIG_AILEVELV2MAPENTRY']._serialized_start = 4968
102 | _globals['_DANMUWEBPLAYERCONFIG_AILEVELV2MAPENTRY']._serialized_end = 5019
103 | _globals['_DMCOLORFUL']._serialized_start = 5021
104 | _globals['_DMCOLORFUL']._serialized_end = 5110
105 | _globals['_DMEXPOREPORTREQ']._serialized_start = 5112
106 | _globals['_DMEXPOREPORTREQ']._serialized_end = 5177
107 | _globals['_DMEXPOREPORTRES']._serialized_start = 5179
108 | _globals['_DMEXPOREPORTRES']._serialized_end = 5196
109 | _globals['_DMPLAYERCONFIGREQ']._serialized_start = 5199
110 | _globals['_DMPLAYERCONFIGREQ']._serialized_end = 6834
111 | _globals['_DMSEGCONFIG']._serialized_start = 6836
112 | _globals['_DMSEGCONFIG']._serialized_end = 6883
113 | _globals['_DMSEGMOBILEREPLY']._serialized_start = 6886
114 | _globals['_DMSEGMOBILEREPLY']._serialized_end = 7114
115 | _globals['_DMSEGMOBILEREQ']._serialized_start = 7117
116 | _globals['_DMSEGMOBILEREQ']._serialized_end = 7283
117 | _globals['_DMSEGOTTREPLY']._serialized_start = 7285
118 | _globals['_DMSEGOTTREPLY']._serialized_end = 7378
119 | _globals['_DMSEGOTTREQ']._serialized_start = 7380
120 | _globals['_DMSEGOTTREQ']._serialized_end = 7456
121 | _globals['_DMSEGSDKREPLY']._serialized_start = 7458
122 | _globals['_DMSEGSDKREPLY']._serialized_end = 7551
123 | _globals['_DMSEGSDKREQ']._serialized_start = 7553
124 | _globals['_DMSEGSDKREQ']._serialized_end = 7629
125 | _globals['_DMVIEWREPLY']._serialized_start = 7632
126 | _globals['_DMVIEWREPLY']._serialized_end = 8494
127 | _globals['_DMVIEWREQ']._serialized_start = 8496
128 | _globals['_DMVIEWREQ']._serialized_end = 8584
129 | _globals['_DMWEBVIEWREPLY']._serialized_start = 8587
130 | _globals['_DMWEBVIEWREPLY']._serialized_end = 9167
131 | _globals['_EXPOREPORT']._serialized_start = 9169
132 | _globals['_EXPOREPORT']._serialized_end = 9211
133 | _globals['_EXPRESSION']._serialized_start = 9213
134 | _globals['_EXPRESSION']._serialized_end = 9313
135 | _globals['_EXPRESSIONS']._serialized_start = 9315
136 | _globals['_EXPRESSIONS']._serialized_end = 9388
137 | _globals['_INLINEPLAYERDANMAKUSWITCH']._serialized_start = 9390
138 | _globals['_INLINEPLAYERDANMAKUSWITCH']._serialized_end = 9432
139 | _globals['_LABEL']._serialized_start = 9434
140 | _globals['_LABEL']._serialized_end = 9473
141 | _globals['_LABELV2']._serialized_start = 9475
142 | _globals['_LABELV2']._serialized_end = 9562
143 | _globals['_PERIOD']._serialized_start = 9564
144 | _globals['_PERIOD']._serialized_end = 9600
145 | _globals['_PLAYERDANMAKUAIRECOMMENDEDLEVEL']._serialized_start = 9602
146 | _globals['_PLAYERDANMAKUAIRECOMMENDEDLEVEL']._serialized_end = 9650
147 | _globals['_PLAYERDANMAKUAIRECOMMENDEDLEVELV2']._serialized_start = 9652
148 | _globals['_PLAYERDANMAKUAIRECOMMENDEDLEVELV2']._serialized_end = 9702
149 | _globals['_PLAYERDANMAKUAIRECOMMENDEDSWITCH']._serialized_start = 9704
150 | _globals['_PLAYERDANMAKUAIRECOMMENDEDSWITCH']._serialized_end = 9753
151 | _globals['_PLAYERDANMAKUBLOCKBOTTOM']._serialized_start = 9755
152 | _globals['_PLAYERDANMAKUBLOCKBOTTOM']._serialized_end = 9796
153 | _globals['_PLAYERDANMAKUBLOCKCOLORFUL']._serialized_start = 9798
154 | _globals['_PLAYERDANMAKUBLOCKCOLORFUL']._serialized_end = 9841
155 | _globals['_PLAYERDANMAKUBLOCKREPEAT']._serialized_start = 9843
156 | _globals['_PLAYERDANMAKUBLOCKREPEAT']._serialized_end = 9884
157 | _globals['_PLAYERDANMAKUBLOCKSCROLL']._serialized_start = 9886
158 | _globals['_PLAYERDANMAKUBLOCKSCROLL']._serialized_end = 9927
159 | _globals['_PLAYERDANMAKUBLOCKSPECIAL']._serialized_start = 9929
160 | _globals['_PLAYERDANMAKUBLOCKSPECIAL']._serialized_end = 9971
161 | _globals['_PLAYERDANMAKUBLOCKTOP']._serialized_start = 9973
162 | _globals['_PLAYERDANMAKUBLOCKTOP']._serialized_end = 10011
163 | _globals['_PLAYERDANMAKUDOMAIN']._serialized_start = 10013
164 | _globals['_PLAYERDANMAKUDOMAIN']._serialized_end = 10049
165 | _globals['_PLAYERDANMAKUENABLEBLOCKLIST']._serialized_start = 10051
166 | _globals['_PLAYERDANMAKUENABLEBLOCKLIST']._serialized_end = 10096
167 | _globals['_PLAYERDANMAKUOPACITY']._serialized_start = 10098
168 | _globals['_PLAYERDANMAKUOPACITY']._serialized_end = 10135
169 | _globals['_PLAYERDANMAKUSCALINGFACTOR']._serialized_start = 10137
170 | _globals['_PLAYERDANMAKUSCALINGFACTOR']._serialized_end = 10180
171 | _globals['_PLAYERDANMAKUSENIORMODESWITCH']._serialized_start = 10182
172 | _globals['_PLAYERDANMAKUSENIORMODESWITCH']._serialized_end = 10228
173 | _globals['_PLAYERDANMAKUSPEED']._serialized_start = 10230
174 | _globals['_PLAYERDANMAKUSPEED']._serialized_end = 10265
175 | _globals['_PLAYERDANMAKUSWITCH']._serialized_start = 10267
176 | _globals['_PLAYERDANMAKUSWITCH']._serialized_end = 10323
177 | _globals['_PLAYERDANMAKUSWITCHSAVE']._serialized_start = 10325
178 | _globals['_PLAYERDANMAKUSWITCHSAVE']._serialized_end = 10365
179 | _globals['_PLAYERDANMAKUUSEDEFAULTCONFIG']._serialized_start = 10367
180 | _globals['_PLAYERDANMAKUUSEDEFAULTCONFIG']._serialized_end = 10413
181 | _globals['_POSTPANEL']._serialized_start = 10416
182 | _globals['_POSTPANEL']._serialized_end = 10812
183 | _globals['_POSTPANELV2']._serialized_start = 10815
184 | _globals['_POSTPANELV2']._serialized_end = 11274
185 | _globals['_RESPONSE']._serialized_start = 11276
186 | _globals['_RESPONSE']._serialized_end = 11317
187 | _globals['_SUBTITLEITEM']._serialized_start = 11320
188 | _globals['_SUBTITLEITEM']._serialized_end = 11697
189 | _globals['_TEXTINPUT']._serialized_start = 11700
190 | _globals['_TEXTINPUT']._serialized_end = 12060
191 | _globals['_TEXTINPUTV2']._serialized_start = 12063
192 | _globals['_TEXTINPUTV2']._serialized_end = 12314
193 | _globals['_TOAST']._serialized_start = 12316
194 | _globals['_TOAST']._serialized_end = 12427
195 | _globals['_TOASTBUTTONV2']._serialized_start = 12429
196 | _globals['_TOASTBUTTONV2']._serialized_end = 12474
197 | _globals['_TOASTV2']._serialized_start = 12476
198 | _globals['_TOASTV2']._serialized_end = 12591
199 | _globals['_USERINFO']._serialized_start = 12593
200 | _globals['_USERINFO']._serialized_end = 12685
201 | _globals['_VIDEOMASK']._serialized_start = 12687
202 | _globals['_VIDEOMASK']._serialized_end = 12770
203 | _globals['_VIDEOSUBTITLE']._serialized_start = 12772
204 | _globals['_VIDEOSUBTITLE']._serialized_end = 12883
205 | _globals['_DM']._serialized_start = 13862
206 | _globals['_DM']._serialized_end = 14534
207 | # @@protoc_insertion_point(module_scope)
208 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Copyright (c) 2025 Laosun Studios.
4 |
5 | Distributed under GPL-3.0 License.
6 |
7 | The product is developing. Effect currently
8 | displayed is for reference only. Not indicative
9 | of final product.
10 |
11 | This program is free software: you can redistribute it and/or modify
12 | it under the terms of the GNU General Public License as published by
13 | the Free Software Foundation, either version 3 of the License, or
14 | (at your option) any later version.
15 |
16 | This program is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU General Public License for more details.
20 |
21 | You should have received a copy of the GNU General Public License
22 | along with this program. If not, see .
23 |
24 | The old version also use the GPL-3.0 license, not MIT License.
25 | """
26 | import base64
27 | import datetime
28 | import getpass
29 | import json
30 | import os
31 | import reprlib
32 | import shutil
33 | import subprocess
34 | import sys
35 | import time
36 | import traceback
37 | from typing import Generator, List
38 |
39 | import requests
40 | import rsa
41 | from google.protobuf.json_format import MessageToJson
42 | from tqdm import tqdm
43 |
44 | from bilibili.protobuf.dm_pb2 import DmSegMobileReply
45 | # from bilibili.biliass.protobuf.danmaku_pb2 import DmSegMobileReply
46 | from bilibili.utils import (
47 | av2bv,
48 | bv2av,
49 | format_time,
50 | validate_title,
51 | encrypt_wbi,
52 | user_manager,
53 | hum_convert,
54 | get_danmaku,
55 | remove, parse_view, danmaku_provider,
56 | )
57 |
58 | __version__ = "1.0.0-dev"
59 |
60 | __year__ = 2025
61 |
62 | __author__ = "Laosun Studios"
63 |
64 | saw = False
65 |
66 | quality_mapping = {6: "240P 极速", 16: "360P 流畅", 32: "480P 清晰", 64: "720P 高清", 74: "720P60 高帧率",
67 | 80: "1080P 高清", 112: "1080P+ 高码率", 116: "1080P60 高帧率", 120: "4K 超清"}
68 |
69 |
70 | def view_short_video_info(bvid):
71 | video = user_manager.get(
72 | "https://api.bilibili.com/x/web-interface/view/detail?bvid=" + bvid
73 | )
74 | item = video.json()["data"]["View"]
75 | print("封面: ", item["pic"])
76 | print("标题: ", item["title"])
77 | print(
78 | "作者: ",
79 | item["owner"]["name"],
80 | " bvid: ",
81 | item["bvid"],
82 | " 日期: ",
83 | datetime.datetime.fromtimestamp(
84 | item["pubdate"]).strftime("%Y-%m-%d %H:%M:%S"),
85 | " 视频时长:",
86 | format_time(item["duration"]),
87 | " 观看量: ",
88 | item["stat"]["view"],
89 | )
90 |
91 |
92 | def show_help():
93 | print(
94 | """帮助菜单:
95 | recommend/r: 推荐
96 | login/l: 登录
97 | logout/lo: 登出
98 | address/a: 按地址播放
99 | bangumi/b: 按地址播放番剧
100 | favorite/f: 查看收藏夹
101 | search/s: 搜索
102 | quit/q: 退出
103 | enable_online_watching: 开启在线观看
104 | disable_online_watching: 关闭在线观看
105 | clean_cache: 清除缓存
106 | refresh_login_state: 刷新登录状态
107 | export_favorite: 导出收藏夹
108 | export_all_favorite: 导出所有收藏夹
109 | download_favorite: 下载收藏夹视频
110 | history: 查看历史记录
111 | view_self: 查看自己的空间
112 | view_user: 查看用户空间
113 | """
114 | )
115 |
116 |
117 | class BilibiliDynamic():
118 | @staticmethod
119 | def get_dynamic():
120 | r = user_manager.get("https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all")
121 | return r.json()
122 |
123 |
124 | class BilibiliLogin:
125 | temp_header = {
126 | "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
127 | "(KHTML, like Gecko) "
128 | "Chrome/103.0.5060.134 Safari/537.36 Edg/103.0.1264.77",
129 | "Referer": "https://www.bilibili.com",
130 | }
131 |
132 | temp_login_header = temp_header.copy()
133 |
134 | @staticmethod
135 | def generate_cookie():
136 | r = requests.get("https://www.bilibili.com",
137 | headers=BilibiliLogin.temp_header)
138 | cookie = ""
139 | for i, j in r.cookies.items():
140 | cookie += f"{i}={j}; "
141 | BilibiliLogin.temp_login_header["Cookie"] = cookie[:-2]
142 |
143 | @staticmethod
144 | def logout():
145 | r = user_manager.post(
146 | "https://passport.bilibili.com/login/exit/v2", data={"biliCSRF": user_manager.csrf})
147 | rsp_json = r.json()
148 | if rsp_json["code"] == 0:
149 | print("退出登录成功.")
150 | with open("cookie.txt", "w") as f:
151 | f.write("")
152 | user_manager.refresh_login()
153 |
154 | @staticmethod
155 | def login_by_password(username: str, password: str):
156 | token, challenge, validate = BilibiliLogin.generate_captcha()
157 | hash_, key = BilibiliLogin.get_key()
158 | pk = rsa.PublicKey.load_pkcs1_openssl_pem(key.encode())
159 | password_hash = rsa.encrypt((hash_ + password).encode(), pk)
160 | password_base64 = base64.b64encode(password_hash)
161 | data = {"username": username, "password": password_base64, "keep": 0, "token": token, "challenge": challenge,
162 | "validate": validate, "seccode": validate + "|jordan"}
163 | r = requests.post("https://passport.bilibili.com/x/passport-login/web/login", data=data,
164 | headers=BilibiliLogin.temp_login_header)
165 | if r.json()["code"] != 0 or r.json()["data"]["message"].startswith("本次登录"):
166 | print("登录失败!")
167 | print(r.json()["message"])
168 | print(r.json()["data"]["message"])
169 | return False
170 | cookie = ""
171 | for i, j in r.cookies.items():
172 | cookie += f"{i}={j}; "
173 | return cookie[:-2] + "; " + BilibiliLogin.temp_login_header["Cookie"]
174 |
175 | @staticmethod
176 | def send_sms(phone_number: str) -> str | bool:
177 | token, challenge, validate = BilibiliLogin.generate_captcha()
178 | data = {"cid": 86, "tel": phone_number, "challenge": challenge, "seccode": validate + "|jordan",
179 | "validate": validate, "token": token,
180 | "source": "main-fe-header"}
181 | r = requests.post("https://passport.bilibili.com/x/passport-login/web/sms/send", data=data,
182 | headers=BilibiliLogin.temp_login_header)
183 | if r.json()["code"] != 0:
184 | print("发送短信认证码失败! ")
185 | print(r.json()["code"])
186 | print(r.json()["message"])
187 | return False
188 | return r.json()["data"]["captcha_key"]
189 |
190 | @staticmethod
191 | def login_by_sms(phone_number: str, captcha_key: str, sms_code: str) -> str | bool:
192 | data = {"cid": 86, "tel": phone_number, "captcha_key": captcha_key, "code": sms_code, "keep": True,
193 | "source": "main_mini"}
194 | r = requests.post("https://passport.bilibili.com/x/passport-login/web/login/sms", data=data,
195 | headers=BilibiliLogin.temp_login_header)
196 | if r.json()["code"] != 0:
197 | print("登录失败! ")
198 | print(r.json()["message"])
199 | return False
200 | cookie = ""
201 | for i, j in r.cookies.items():
202 | cookie += f"{i}={j}; "
203 | return cookie[:-2] + "; " + BilibiliLogin.temp_login_header["Cookie"]
204 |
205 | @staticmethod
206 | def generate_captcha():
207 | r = requests.get("https://passport.bilibili.com/x/passport-login/captcha",
208 | headers=BilibiliLogin.temp_login_header)
209 | data = r.json()
210 | challenge = data["data"]["geetest"]["challenge"]
211 | gt = data["data"]["geetest"]["gt"]
212 | print("gt: ", gt, "challenge: ", challenge)
213 | print("请到 https://kuresaru.github.io/geetest-validator/ 进行认证.")
214 | while True:
215 | validate = input("请输入得到的 validate: ")
216 | if len(validate.strip()) != 32:
217 | print("validate 长度错误! ")
218 | continue
219 | break
220 | return data["data"]["token"], challenge, validate
221 |
222 | @staticmethod
223 | def get_key() -> tuple[str, str]:
224 | r = requests.get("https://passport.bilibili.com/x/passport-login/web/key",
225 | headers=BilibiliLogin.temp_login_header)
226 | return r.json()["data"]["hash"], r.json()["data"]["key"]
227 |
228 |
229 | # https://github.com/SocialSisterYi/bilibili-API-collect/issues/1168
230 | #
231 | # class BilibiliManga:
232 | # @staticmethod
233 | # def get_manga_detail(manga_id: int) -> dict:
234 | # detail_request = user_manager.post(
235 | # "https://manga.bilibili.com/twirp/comic.v1.Comic/ComicDetail?device=pc&platform=web",
236 | # data={"comic_id": manga_id},
237 | # )
238 | # return detail_request.json()
239 | #
240 | # @staticmethod
241 | # def list_history() -> dict:
242 | # history = user_manager.post(
243 | # "https://manga.bilibili.com/twirp/bookshelf.v1.Bookshelf/ListHistory?device=pc&platform=web",
244 | # data={"page_num": 1, "page_size": 50},
245 | # )
246 | # return history.json()
247 | #
248 | # @staticmethod
249 | # def get_image_list(epid) -> dict:
250 | # images = user_manager.post(
251 | # "https://manga.bilibili.com/twirp/comic.v1.Comic/GetImageIndex?device=pc&platform=web",
252 | # data={"ep_id": epid},
253 | # )
254 | # return images.json()
255 | #
256 | # @staticmethod
257 | # def get_token(image: str) -> dict:
258 | # token = user_manager.post(
259 | # "https://manga.bilibili.com/twirp/comic.v1.Comic/ImageToken?device=pc&platform=web",
260 | # data={"urls": '["{}"]'.format(image)},
261 | # )
262 | # return token.json()
263 | #
264 | # @classmethod
265 | # def download_manga(cls, manga_id: int) -> bool:
266 | # manga_info = cls.get_manga_detail(manga_id)
267 | # ep_info = manga_info["data"]["ep_list"]
268 | # name = manga_info["data"]["title"]
269 | # if not os.path.exists("download/manga"):
270 | # os.mkdir("download/manga")
271 | # if not os.path.exists("download/manga/" + validate_title(name)):
272 | # os.mkdir("download/manga/" + validate_title(name))
273 | # first, end = input("选择回目范围 (1-{}): ".format(len(ep_info))).split("-")
274 | # try:
275 | # # first, end str值如果不可转换为 int 会直接跳出函数, 故对其忽略类型检查
276 | # first = int(first) # type: ignore
277 | # end = int(end) # type: ignore
278 | # except ValueError:
279 | # print("输入回目范围错误!")
280 | # return False
281 | # download_manga_epid = []
282 | # download_manga_name = []
283 | # locked = 0
284 | # for i in list(reversed(ep_info)):
285 | # if first <= i["ord"] <= end:
286 | # if i["is_locked"]:
287 | # locked += 1
288 | # continue
289 | # download_manga_epid.append(i["id"])
290 | # download_manga_name.append(i["title"])
291 | # print(f"有{locked}篇被上锁, 需要购买" if locked else "")
292 | # download_image = {}
293 | # cursor = 0
294 | # picture_count = 0
295 | # print("获取图片信息中.")
296 | # # 忽略原因同上
297 | # with tqdm(total=end) as progress_bar: # type: ignore
298 | # for i in download_manga_epid:
299 | # download_image_prefix = []
300 | # image_list = cls.get_image_list(i)
301 | # for j in image_list["data"]["images"]:
302 | # download_image_prefix.append(j["path"])
303 | # picture_count += 1
304 | # download_image[download_manga_name[cursor]
305 | # ] = download_image_prefix
306 | # progress_bar.update(1)
307 | # cursor += 1
308 | # download_image_url = {}
309 | # print("获取图片token中.")
310 | # with tqdm(total=picture_count) as progress_bar:
311 | # for i, j in download_image.items():
312 | # download_image_url_local = []
313 | # for k in j:
314 | # token = cls.get_token(k)["data"][0]
315 | # download_image_url_local.append(
316 | # "{}?token={}".format(token["url"], token["token"])
317 | # )
318 | # progress_bar.update(1)
319 | # download_image_url[i] = download_image_url_local
320 | # print("下载图片中.")
321 | # byte = 0
322 | # with tqdm(total=picture_count) as progress_bar:
323 | # for i, j in download_image_url.items():
324 | # filename = 0
325 | # for k in j:
326 | # path = (
327 | # "download/manga/"
328 | # + validate_title(name)
329 | # + "/"
330 | # + validate_title(i)
331 | # + "/"
332 | # )
333 | # file = path + f"{filename}.jpg"
334 | # if not os.path.exists(path):
335 | # os.mkdir(path)
336 | # with open(file, "wb") as f:
337 | # byte += f.write(user_manager.get(k).content)
338 | # progress_bar.update(1)
339 | # filename += 1
340 | # print("下载完成. 总计下载了 {} 字节 ({})".format(byte, hum_convert(byte)))
341 | # return True
342 |
343 |
344 | class BilibiliUserSpace:
345 | @staticmethod
346 | def get_following_list(mid: int):
347 | following_list = []
348 | pre_page = 20
349 | r = user_manager.get(
350 | f"https://api.bilibili.com/x/relation/fans?vmid={mid}&pn=1&ps={pre_page}"
351 | )
352 | total = r.json()["data"]["total"]
353 | for i in range(1, total // pre_page + 2):
354 | if i == 5:
355 | break
356 | r = user_manager.get(
357 | f"https://api.bilibili.com/x/relation/fans?vmid={mid}&pn={i}&ps={pre_page}"
358 | )
359 | following_list += r.json()["data"]["list"]
360 | return following_list
361 |
362 | @staticmethod
363 | def get_followed_list(mid: int):
364 | followed_list = []
365 | pre_page = 20
366 | r = user_manager.get(
367 | f"https://api.bilibili.com/x/relation/followings?vmid={mid}&pn=1&ps={pre_page}"
368 | )
369 | total = r.json()["data"]["total"]
370 | for i in range(1, total // pre_page + 2):
371 | if i == 5:
372 | break
373 | r = user_manager.get(
374 | f"https://api.bilibili.com/x/relation/followings?vmid={mid}&pn={i}&ps={pre_page}"
375 | )
376 | followed_list += r.json()["data"]["list"]
377 | return followed_list
378 |
379 | # follow_type 1 关注 2 取关
380 | @staticmethod
381 | def modify_relation(mid: int, modify_type: int = 1):
382 | data = {"fid": mid, "act": modify_type, "csrf": user_manager.csrf}
383 | r = user_manager.post(
384 | "https://api.bilibili.com/x/relation/modify", data=data)
385 | if r.json()["code"] == 0:
386 | print("更改用户关系成功.")
387 | else:
388 | print("更改用户关系失败!")
389 | print(r.json()["message"])
390 |
391 | @staticmethod
392 | def get_user_data(mid: int):
393 | user_info = user_manager.get(
394 | "https://api.bilibili.com/x/space/wbi/acc/info?"
395 | + encrypt_wbi("mid=" + str(mid))
396 | )
397 | return user_info.json()["data"]
398 |
399 | @staticmethod
400 | def get_user_video(mid: int):
401 | pre_page = 5
402 | cursor = 1
403 | request = user_manager.get(
404 | "https://api.bilibili.com/x/space/wbi/arc/search?"
405 | + encrypt_wbi(f"mid={mid}&ps={pre_page}"),
406 | cache=True,
407 | )
408 | total = request.json()["data"]["page"]["count"] // pre_page + 1
409 | while True:
410 | ls = user_manager.get(
411 | "https://api.bilibili.com/x/space/wbi/arc/search?"
412 | + encrypt_wbi(f"mid={mid}&ps={pre_page}&pn={cursor}"),
413 | cache=True,
414 | )
415 | if total < cursor:
416 | break
417 | yield ls.json()["data"]["list"]["vlist"]
418 | cursor += 1
419 |
420 |
421 | class BilibiliBangumi:
422 | def __init__(self, quality: int):
423 | self.quality = quality
424 |
425 | @staticmethod
426 | def get_follow_bangumi(mid) -> list:
427 | r = user_manager.get(
428 | f"https://api.bilibili.com/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15"
429 | + f"&vmid={mid}",
430 | cache=True,
431 | )
432 | if r.json()["code"] != 0:
433 | raise Exception(r.json()["message"])
434 | datas = []
435 | for i in r.json()["data"]["list"]:
436 | datas.append(
437 | {
438 | "watch_progress": i["progress"],
439 | "img": i["cover"],
440 | "title": i["title"],
441 | "bangumi_type": i["season_type_name"],
442 | "areas": i["areas"][0]["name"],
443 | "update_progress": i["new_ep"]["index_show"],
444 | }
445 | )
446 | return datas
447 |
448 | @staticmethod
449 | def get_self_follow_bangumi():
450 | return BilibiliBangumi.get_follow_bangumi(user_manager.mid)
451 |
452 | @staticmethod
453 | def follow_bangumi(season_id):
454 | data = {"season_id": season_id, "csrf": user_manager.csrf}
455 | r = user_manager.post(
456 | "https://api.bilibili.com/pgc/web/follow/add", data=data)
457 | if r.json()["code"] == 0:
458 | print("追番成功.")
459 | else:
460 | print("追番失败!")
461 | print(f"失败信息: {r.json()['message']}")
462 |
463 | @staticmethod
464 | def cancel_follow_bangumi(season_id):
465 | data = {"season_id": season_id, "csrf": user_manager.csrf}
466 | r = user_manager.post(
467 | "https://api.bilibili.com/pgc/web/follow/del", data=data)
468 | if r.json()["code"] == 0:
469 | print("取消追番成功.")
470 | else:
471 | print("取消追番失败!")
472 | print(f"失败信息: {r.json()['message']}")
473 |
474 | def select_bangumi(self, ssid="", epid=""):
475 | if not any([ssid, epid]):
476 | return
477 | if ssid:
478 | url = "https://api.bilibili.com/pgc/view/web/season?season_id=" + ssid
479 | else:
480 | url = "https://api.bilibili.com/pgc/view/web/season?ep_id=" + epid
481 | bangumi_url = user_manager.get(url)
482 | bangumi_page = bangumi_url.json()["result"]["episodes"]
483 | for i, j in enumerate(bangumi_page):
484 | print(f"{i + 1}: {j['share_copy']} ({j['badge']})")
485 | print("请以冒号前面的数字为准选择视频.")
486 | while True:
487 | page = input("选择视频: ")
488 | if page == "quit" or page == "q":
489 | break
490 | if not page:
491 | continue
492 | if not page.isdigit():
493 | continue
494 | if int(page) > len(bangumi_page) or int(page) <= 0:
495 | print("选视频错误!")
496 | continue
497 | cid = bangumi_page[int(page) - 1]["cid"]
498 | bvid = bangumi_page[int(page) - 1]["bvid"]
499 | epid = bangumi_page[int(page) - 1]["id"]
500 | title = bangumi_page[int(page) - 1]["share_copy"]
501 | video = BilibiliVideo(
502 | bvid=bvid, epid=epid, bangumi=True, quality=self.quality
503 | )
504 | video.play(cid, title=title)
505 |
506 |
507 | # search_type
508 |
509 | # article 专栏
510 | # video 视频
511 | # bili_user 用户
512 | # live 直播
513 | # media_ft 影视
514 | # media_bangumi 番剧
515 |
516 | # order
517 | # click 播放量
518 | # pubdate 最新
519 | # dm 弹幕
520 | # stow 收藏
521 | # 空 综合
522 |
523 |
524 | class BilibiliSearch:
525 | @staticmethod
526 | def search(keyword, search_type="video", order=""):
527 | """
528 | 搜索
529 | :param keyword: 搜索关键词
530 | :param search_type: 搜索类型 article 专栏 video 视频 bili_user 用户 live 直播 media_ft 影视 media_bangumi 番剧
531 | :param order: 搜索排序 click 播放量 pubdate 最新 dm 弹幕 stow 收藏 空 综合
532 | :return: 搜索结果
533 | """
534 | pre_page = 5
535 | cursor = 1
536 | while True:
537 | ls = user_manager.get(
538 | f"https://api.bilibili.com/x/web-interface/wbi/search/type?page={cursor}"
539 | f"&page_size={pre_page}&keyword={keyword}&search_type={search_type}"
540 | + (f"&order={order}" if order else ""),
541 | cache=True,
542 | )
543 | if len(ls.json()["data"]["result"]) == 0:
544 | break
545 | result = ls.json()["data"]["result"]
546 | for i in result:
547 | i["title"] = remove(i["title"], '')
548 | i["title"] = remove(i["title"], "")
549 | yield result
550 | cursor += 1
551 |
552 |
553 | class BilibiliHistory:
554 | def __init__(self, csrf):
555 | self.csrf = csrf
556 |
557 | @staticmethod
558 | def get_history():
559 | url = "https://api.bilibili.com/x/web-interface/history/cursor?max={}&view_at={}&business={}"
560 | max_ = 0
561 | view_at = 0
562 | business = ""
563 | history = user_manager.get(url.format(max_, view_at, business))
564 | while history.json()["data"]["cursor"]["max"] != 0:
565 | data = history.json()["data"]["list"]
566 | for cursor in range(0, len(data), 5):
567 | yield data[cursor:cursor + 5]
568 | max_ = history.json()["data"]["cursor"]["max"]
569 | view_at = history.json()["data"]["cursor"]["view_at"]
570 | business = history.json()["data"]["cursor"]["business"]
571 | history = user_manager.get(url.format(max_, view_at, business))
572 |
573 | def set_record_history(self, stop=True):
574 | req = user_manager.post(
575 | "https://api.bilibili.com/x/v2/history/shadow/set",
576 | data={"jsonp": "jsonp", "csrf": self.csrf, "switch": stop},
577 | )
578 | if req.json()["code"] == 0:
579 | print(("停止" if stop else "开启") + "记录历史成功.")
580 | else:
581 | print(("停止" if stop else "开启") + "记录历史失败!")
582 | print("错误代码: ", req.json()["code"])
583 | print("错误信息: ", req.json()["message"])
584 |
585 | @staticmethod
586 | def search_history(search=""):
587 | url = "https://api.bilibili.com/x/web-goblin/history/search?pn={}&keyword={}&business=all"
588 | cursor = 1
589 | req = user_manager.get(url.format(cursor, search))
590 | print("搜索数量: ", req.json()["data"]["page"]["total"])
591 | while req.json()["data"]["has_more"]:
592 | yield req.json()["data"]["list"]
593 | cursor += 1
594 | req = user_manager.get(url.format(cursor, search))
595 |
596 | @staticmethod
597 | def dump_history():
598 | return [j for i in BilibiliHistory.get_history() for j in i]
599 |
600 |
601 | class BilibiliFavorite:
602 | @staticmethod
603 | def select_favorite(mid: int, aid: int = 0) -> list[int] | int:
604 | """
605 | 选择收藏夹
606 | :param mid: 用户mid
607 | :param aid: 视频aid
608 | :return: 收藏夹id list or int
609 | """
610 | request = user_manager.get(
611 | f"https://api.bilibili.com/x/v3/fav/folder/created/list-all?type=2&rid={aid}&up_mid={mid}",
612 | cache=True,
613 | )
614 | print("\n")
615 | print("选择收藏夹")
616 | for index, item in enumerate(request.json()["data"]["list"]):
617 | print(
618 | f"{index + 1}: {item['title']} ({item['media_count']}) {'(已收藏)' if item['fav_state'] else ''}"
619 | )
620 | fail = False
621 | ids = []
622 | command = input("选择收藏夹(以逗号为分隔): ")
623 | if command == "quit" or command == "q":
624 | return 0
625 | for index, item in enumerate(command.split(",")):
626 | if not item.replace(" ", "").isdecimal():
627 | print(f"索引{index + 1} 错误: 输入的必须为数字!")
628 | fail = True
629 | break
630 | if int(item) - 1 < 0:
631 | print(f"索引{index + 1} 错误: 输入的必须为正数!")
632 | fail = True
633 | break
634 | try:
635 | if request.json()["data"]["list"][int(item) - 1]["fav_state"]:
636 | print(f"索引{index + 1} 警告: 此收藏夹已收藏过该视频, 将不会重复收藏.")
637 | continue
638 | ids.append(request.json()["data"]["list"][int(item) - 1]["id"])
639 | except IndexError:
640 | print(f"索引{index + 1} 错误: 索引超出收藏夹范围!")
641 | fail = True
642 | if fail:
643 | print("收藏失败!")
644 | return ids
645 |
646 | @staticmethod
647 | def select_one_favorite(mid: int, aid: int = 0):
648 | request = user_manager.get(
649 | f"https://api.bilibili.com/x/v3/fav/folder/created/list-all?type=2&rid={aid}&up_mid={mid}",
650 | cache=True,
651 | )
652 | for index, item in enumerate(request.json()["data"]["list"]):
653 | print(
654 | f"{index + 1}: {item['title']} ({item['media_count']}) {'(已收藏)' if item['fav_state'] else ''}"
655 | )
656 | command = input("选择收藏夹: ")
657 | if command == "quit" or command == "q":
658 | return 0
659 | if not command.isdecimal():
660 | print(f"错误: 输入的必须为数字!")
661 | return 0
662 | try:
663 | return request.json()["data"]["list"][int(command) - 1]["id"]
664 | except IndexError:
665 | print("错误: 索引超出收藏夹范围!")
666 | return 0
667 | except TypeError as e:
668 | print("错误: 收藏夹可能未开放")
669 | traceback.print_exc()
670 |
671 | @staticmethod
672 | def get_favorite(fav_id: int) -> Generator:
673 | """
674 | 获取收藏夹
675 | :param fav_id: 收藏夹id
676 | :return: 收藏夹内容
677 | """
678 | pre_page = 5
679 | cursor = 1
680 | request = user_manager.get(
681 | f"https://api.bilibili.com/x/v3/fav/resource/list?ps=20&media_id={fav_id}",
682 | cache=True,
683 | )
684 | total = request.json()["data"]["info"]["media_count"] // pre_page + 1
685 | while True:
686 | ls = user_manager.get(
687 | f"https://api.bilibili.com/x/v3/fav/resource/list?ps=5&media_id={fav_id}&pn={cursor}",
688 | cache=True,
689 | )
690 | if total < cursor:
691 | break
692 | yield ls.json()["data"]["medias"]
693 | cursor += 1
694 |
695 | @staticmethod
696 | def get_favorite_information(fav_id: int) -> dict:
697 | """
698 | 获取收藏夹信息
699 | :param fav_id:
700 | :return:
701 | """
702 | request = user_manager.get(
703 | f"https://api.bilibili.com/x/v3/fav/resource/list?ps=20&media_id={fav_id}"
704 | )
705 | return request.json()["data"]["info"]
706 |
707 | @staticmethod
708 | def export_favorite(fav_id: int):
709 | """
710 | 导出收藏夹
711 | :param fav_id: 收藏夹id
712 | :return:
713 | """
714 | pre_page = 5
715 | cursor = 1
716 | r = user_manager.get(
717 | "https://api.bilibili.com/x/v3/fav/resource/list?ps=20&media_id="
718 | + str(fav_id)
719 | )
720 | total = r.json()["data"]["info"]["media_count"] // pre_page + (
721 | 1 if r.json()["data"]["info"]["media_count"] % pre_page != 0 else 0
722 | )
723 | print(f"正在导出收藏夹\"{r.json()['data']['info']['title']}\".")
724 | # 导出格式
725 | export = {
726 | "id": r.json()["data"]["info"]["id"],
727 | "title": r.json()["data"]["info"]["title"],
728 | "cover": r.json()["data"]["info"]["cover"].replace("http", "https"),
729 | "media_count": r.json()["data"]["info"]["media_count"],
730 | "view": r.json()["data"]["info"]["cnt_info"]["play"],
731 | "user": {
732 | "name": r.json()["data"]["info"]["upper"]["name"],
733 | "mid": r.json()["data"]["info"]["upper"]["mid"],
734 | "create_time": r.json()["data"]["info"]["mtime"],
735 | },
736 | "medias": [],
737 | }
738 | with tqdm(total=total, desc=r.json()["data"]["info"]["title"]) as progress_bar:
739 | while True:
740 | if total < cursor:
741 | break
742 | medias: List[dict] = user_manager.get(
743 | f"https://api.bilibili.com/x/v3/fav/resource/list?ps=5&media_id={fav_id}&pn={cursor}"
744 | ).json()["data"]["medias"]
745 | for i in medias:
746 | # 清理数据
747 | del i["type"]
748 | del i["bv_id"]
749 | del i["ugc"]
750 | del i["season"]
751 | del i["ogv"]
752 | del i["link"]
753 | i["publish_time"] = i["pubtime"]
754 | del i["pubtime"]
755 | del i["ctime"]
756 | i["cover"] = i["cover"].replace("http", "https")
757 | export["medias"] += medias
758 | cursor += 1
759 | progress_bar.update(1)
760 | with open(f"favorite_{str(fav_id)}_{str(round(time.time()))}.json", "w", encoding="utf-8") as f:
761 | json.dump(export, f, indent=4, ensure_ascii=False)
762 | print(f"导出收藏夹\"{r.json()['data']['info']['title']}\"成功.")
763 |
764 | @staticmethod
765 | def list_favorite(mid):
766 | ls = []
767 | request = user_manager.get(
768 | f"https://api.bilibili.com/x/v3/fav/folder/created/list-all?type=2&up_mid={mid}",
769 | cache=True,
770 | )
771 | for i in request.json()["data"]["list"]:
772 | ls.append(i["id"])
773 | return ls
774 |
775 |
776 | class BilibiliInteraction:
777 | @staticmethod
778 | def like(bvid: str, unlike=False):
779 | r = user_manager.post(
780 | "https://api.bilibili.com/x/web-interface/archive/like",
781 | data={"bvid": bvid, "like": 2 if unlike else 1,
782 | "csrf": user_manager.csrf},
783 | )
784 | if r.json()["code"] != 0:
785 | print("点赞或取消点赞失败!")
786 | print(f"错误信息: {r.json()['message']}")
787 | else:
788 | if unlike:
789 | print("取消点赞成功!")
790 | else:
791 | print("点赞成功!")
792 |
793 | @staticmethod
794 | def coin(bvid: str, count: int):
795 | r = user_manager.post(
796 | "https://api.bilibili.com/x/web-interface/coin/add",
797 | data={"bvid": bvid, "csrf": user_manager.csrf, "multiply": count},
798 | )
799 | if r.json()["code"] == 0:
800 | print("投币成功!")
801 | else:
802 | print("投币失败!")
803 | print(f"错误信息: {r.json()['message']}")
804 |
805 | @staticmethod
806 | def triple(bvid: str):
807 | r = user_manager.post(
808 | "https://api.bilibili.com/x/web-interface/archive/like/triple",
809 | data={"bvid": bvid, "csrf": user_manager.csrf},
810 | )
811 | if r.json()["code"] == 0:
812 | print("三联成功!")
813 | else:
814 | print("三联失败!")
815 | print(f"错误信息: {r.json()['message']}")
816 |
817 | @staticmethod
818 | def mark_interact_video(bvid: str, score: int):
819 | r = user_manager.post(
820 | "https://api.bilibili.com/x/stein/mark",
821 | data={"bvid": bvid, "csrf": user_manager.csrf, "mark": score},
822 | )
823 | if r.json()["code"] == 0:
824 | print("评分成功!")
825 | else:
826 | print("评分失败!")
827 | print(f"错误信息: {r.json()['message']}")
828 |
829 | @staticmethod
830 | def favorite(aid: int, favorite_list: list):
831 | if not favorite_list:
832 | print("收藏列表为空!")
833 | return
834 | r = user_manager.post(
835 | "https://api.bilibili.com/x/v3/fav/resource/deal",
836 | data={
837 | "rid": aid,
838 | "type": 2,
839 | "add_media_ids": ",".join("%s" % fav_id for fav_id in favorite_list),
840 | "csrf": user_manager.csrf,
841 | },
842 | )
843 | if r.json()["code"] == 0:
844 | print("收藏成功!")
845 | else:
846 | print("收藏失败!")
847 | print(f"错误信息: {r.json()['message']}")
848 |
849 |
850 | # type
851 | # 1 视频稿件 稿件 aid
852 | # 2 话题 话题 id
853 | # 4 活动 活动 id
854 | # 5 小视频 小视频 id
855 | # 6 小黑屋封禁信息 封禁公示 id
856 | # 7 公告信息 公告 id
857 | # 8 直播活动 直播间 id
858 | # 9 活动稿件 (?)
859 | # 10 直播公告 (?)
860 | # 11 相簿(图片动态) 相簿 id
861 | # 12 专栏 专栏 cvid
862 | # 13 票务 (?)
863 | # 14 音频 音频 auid
864 | # 15 风纪委员会 众裁项目 id
865 | # 16 点评 (?)
866 | # 17 动态(纯文字动态&分享) 动态 id
867 | # 18 播单 (?)
868 | # 19 音乐播单 (?)
869 | # 20 漫画 (?)
870 | # 21 漫画 (?)
871 | # 22 漫画 漫画 mcid
872 | # 33 课程 课程 epid
873 |
874 | # sort_type
875 | # 默认为0
876 | # 0:按时间
877 | # 1:按点赞数
878 | # 2:按回复数
879 | # https://api.bilibili.com/x/v2/reply/reply?oid=113858106693946&type=1&root=252881092688&ps=10&pn=1&web_location=333.788
880 | class BilibiliComment:
881 | @staticmethod
882 | def get_comment(content_type: int, content_id: int, sort_type: int = 0):
883 | pre_page = 10
884 | cursor = 1
885 | while True:
886 | datas = []
887 | try:
888 | ls = user_manager.get(
889 | f"https://api.bilibili.com/x/v2/reply?type={content_type}&oid={content_id}&sort={sort_type}&ps={pre_page}"
890 | f"&pn={cursor}",
891 | cache=True,
892 | )
893 | except json.decoder.JSONDecodeError:
894 | time.sleep(1200)
895 | continue
896 | if not ls.json()["data"]["replies"]:
897 | break
898 | for i in ls.json()["data"]["replies"]:
899 | data = {"content": i["content"], "rpid": i["rpid"], "reply_count": i["rcount"], "like": i["like"],
900 | "send_time": i["ctime"],
901 | "user": {"mid": i["mid"], "uname": i["member"]["uname"],
902 | "level": i["member"]["level_info"]["current_level"]}}
903 | datas.append(data)
904 | yield datas
905 | cursor += 1
906 |
907 | @staticmethod
908 | def like_comment():
909 | pass
910 |
911 | @staticmethod
912 | def get_comment_reply(content_type: int, oid: int, comment_id: int, sort_type: int = 0):
913 | pre_page = 10
914 | cursor = 1
915 | while True:
916 | datas = []
917 | try:
918 | ls = user_manager.get(
919 | f"https://api.bilibili.com/x/v2/reply/reply?type={content_type}&oid={oid}&sort={sort_type}&root={comment_id}&ps={pre_page}"
920 | f"&pn={cursor}",
921 | cache=True,
922 | )
923 | except json.decoder.JSONDecodeError:
924 | time.sleep(1200)
925 | continue
926 | if not ls.json()["data"]["replies"]:
927 | break
928 | for i in ls.json()["data"]["replies"]:
929 | data = {"content": i["content"], "rpid": i["rpid"], "like": i["like"],
930 | "send_time": i["ctime"],
931 | "user": {"mid": i["mid"], "uname": i["member"]["uname"],
932 | "level": i["member"]["level_info"]["current_level"]}}
933 | datas.append(data)
934 | yield datas
935 | cursor += 1
936 |
937 |
938 | class BilibiliVideo:
939 | def __init__(
940 | self,
941 | bvid: str = "",
942 | aid: int = 0,
943 | epid: str = "",
944 | season_id: str = "",
945 | quality=80,
946 | view_online_watch=True,
947 | audio_quality=30280,
948 | bangumi=False,
949 | source="backup"
950 | ):
951 | if not any([bvid, aid, epid, season_id]):
952 | raise Exception("Video id can't be null.")
953 | self.bvid = bvid if bvid else av2bv(aid)
954 | self.aid = aid if aid else bv2av(bvid)
955 | self.epid = epid
956 | self.season_id = season_id
957 | self.bangumi = bangumi
958 | self.quality = quality
959 | self.audio_quality = audio_quality
960 | self.view_online_watch = view_online_watch
961 | self.author_mid = self.get_author_mid()
962 | self.source = source
963 | self.see_message = False
964 |
965 | def select_video(self, return_information=False):
966 | r = user_manager.get(
967 | "https://api.bilibili.com/x/web-interface/view/detail?bvid=" + self.bvid,
968 | cache=True,
969 | )
970 | if r.json()["code"] != 0:
971 | print("获取视频信息错误!")
972 | print(r.json()["code"])
973 | print(r.json()["message"])
974 | return
975 | # if r.json()['data']["View"]['stat']['evaluation']:
976 | # print("你播放的视频是一个互动视频.")
977 | # base_cid = r.json()['data']["View"]['cid']
978 | # self.play_interact_video(bvid, base_cid)
979 | # return
980 | video = r.json()["data"]["View"]["pages"]
981 | title = r.json()["data"]["View"]["title"]
982 | pic = r.json()["data"]["View"]["pic"]
983 | if len(video) == 1:
984 | if not return_information:
985 | self.play(video[0]["cid"], title)
986 | return
987 | else:
988 | return (
989 | video[0]["cid"],
990 | title,
991 | video[0]["part"],
992 | pic,
993 | r.json()["data"]["View"]["stat"]["evaluation"],
994 | )
995 | print("\n")
996 | print("视频选集")
997 | for i in video:
998 | print(f"{i['page']}: {i['part']}")
999 | print("\n")
1000 | while True:
1001 | page = input("选择视频: ")
1002 | if page == "quit" or page == "q":
1003 | break
1004 | elif not page:
1005 | continue
1006 | elif not page.isdigit():
1007 | print("输入的并不是数字!")
1008 | continue
1009 | elif int(page) > len(video) or int(page) <= 0:
1010 | print("选视频超出范围!")
1011 | continue
1012 | if not return_information:
1013 | self.play(video[int(page) - 1]["cid"], self.bvid)
1014 | else:
1015 | return (
1016 | video[int(page) - 1]["cid"],
1017 | title,
1018 | video[int(page) - 1]["part"],
1019 | pic,
1020 | True if r.json()[
1021 | "data"]["View"]["stat"]["evaluation"] else False,
1022 | )
1023 | break
1024 |
1025 | def get_author_mid(self):
1026 | return user_manager.get(
1027 | "https://api.bilibili.com/x/web-interface/view/detail?bvid=" + self.bvid,
1028 | cache=True,
1029 | ).json()["data"]["Card"]["card"]["mid"]
1030 |
1031 | def select_video_collection(self):
1032 | r = user_manager.get(
1033 | f"https://api.bilibili.com/x/web-interface/view/detail?bvid={self.bvid}", cache=True)
1034 | if r.json()["code"] != 0:
1035 | print("获取视频信息错误!")
1036 | print(r.json()["code"])
1037 | print(r.json()["message"])
1038 | return
1039 | if not r.json()["data"]["View"].get("ugc_season"):
1040 | print("视频并没有合集!")
1041 | return
1042 | video = r.json()["data"]["View"]["ugc_season"]["sections"]
1043 | videos = []
1044 | for i in video:
1045 | videos += i["episodes"]
1046 | print("\n")
1047 | print("视频合集选集")
1048 | for i, j in enumerate(videos):
1049 | print(f"{i + 1}: {j['title']}")
1050 | while True:
1051 | page = input("选择视频: ")
1052 | if page == "quit" or page == "q":
1053 | break
1054 | elif not page:
1055 | continue
1056 | elif not page.isdigit():
1057 | print("输入的并不是数字!")
1058 | continue
1059 | elif int(page) > len(video) or int(page) <= 0:
1060 | print("选视频超出范围!")
1061 | continue
1062 | view_short_video_info(videos[int(page) - 1]["bvid"])
1063 | selected_video = BilibiliVideo(
1064 | bvid=videos[int(page) - 1]["bvid"],
1065 | quality=self.quality,
1066 | view_online_watch=self.view_online_watch,
1067 | )
1068 | selected_video.select_video()
1069 | break
1070 |
1071 | def switch_quality(self, cid):
1072 | if self.bangumi:
1073 | url = f"https://api.bilibili.com/pgc/player/web/playurl?cid={cid}&fnval=16&qn={self.quality}"
1074 | else:
1075 | url = f"https://api.bilibili.com/x/player/playurl?cid={cid}&bvid={self.bvid}&fnval=16"
1076 | play_url_request = user_manager.get(url, cache=True)
1077 | can_use_quality = {v["id"] for v in play_url_request.json()["data"]["dash"]["video"]}
1078 | can_use_quality_description = [quality_mapping[v] for v in can_use_quality]
1079 | quality_description_number_mapping = list(zip(can_use_quality_description, can_use_quality))
1080 | for index, description in enumerate(quality_description_number_mapping):
1081 | print(f"{index + 1}: {description[0]}")
1082 | quality = input("选择画质: ")
1083 | if quality.isdigit() and int(quality) > 0 and int(quality) < len(quality_description_number_mapping) + 1:
1084 | self.quality = quality_description_number_mapping[int(quality) - 1][1]
1085 | print("更改成功! ")
1086 | else:
1087 | print("输入不正确! ")
1088 |
1089 | def get_video_and_audio_url(self, cid):
1090 | if self.bangumi:
1091 | url = f"https://api.bilibili.com/pgc/player/web/playurl?cid={cid}&fnval=16&qn={self.quality}"
1092 | else:
1093 | url = f"https://api.bilibili.com/x/player/playurl?cid={cid}&bvid={self.bvid}&fnval=16"
1094 | play_url_request = user_manager.get(url, cache=True)
1095 |
1096 | videos = play_url_request.json()["data" if not self.bangumi else "result"][
1097 | "dash"
1098 | ]["video"]
1099 | audios = play_url_request.json()["data" if not self.bangumi else "result"][
1100 | "dash"
1101 | ]["audio"]
1102 | video_mapping = {}
1103 | audio_mapping = {}
1104 |
1105 | for i in videos:
1106 | if i["codecs"].startswith("avc"):
1107 | video_mapping[i["id"]] = {
1108 | "id": i["id"],
1109 | "url": i["backup_url"][0] if self.source == "backup" else i['base_url'],
1110 | "width": i["width"],
1111 | "height": i["height"],
1112 | }
1113 |
1114 | for i in audios:
1115 | audio_mapping[i["id"]] = i["backup_url"][0]
1116 |
1117 | default_audio = sorted(list(audio_mapping.keys()), reverse=True)[0]
1118 | default_video = sorted(list(video_mapping.keys()), reverse=True)[0]
1119 |
1120 | try:
1121 | audio_url = audio_mapping[self.audio_quality]
1122 | except KeyError:
1123 | audio_url = audio_mapping[default_audio]
1124 | try:
1125 | video_url = video_mapping[self.quality]["url"]
1126 | width = video_mapping[self.quality]["width"]
1127 | height = video_mapping[self.quality]["height"]
1128 | except KeyError:
1129 | video_url = video_mapping[default_video]["url"]
1130 | width = video_mapping[default_video]["width"]
1131 | height = video_mapping[default_video]["height"]
1132 | return video_url, width, height, audio_url
1133 |
1134 | def play(self, cid, title=""):
1135 | global saw
1136 | if not os.path.exists("cached"):
1137 | os.mkdir("cached")
1138 | video_url, width, height, audio_url = self.get_video_and_audio_url(cid)
1139 |
1140 | if not os.path.exists(f"cached/{cid}.ass"):
1141 | a = danmaku_provider()(
1142 | get_danmaku(cid),
1143 | width,
1144 | height,
1145 | reserve_blank=0,
1146 | font_face="SimHei",
1147 | font_size=25,
1148 | alpha=0.8,
1149 | duration_marquee=15.0,
1150 | duration_still=10.0,
1151 | comment_filter="",
1152 | reduced=False
1153 | )
1154 | with open(f"cached/{cid}.ass", "w", encoding="utf-8") as f:
1155 | f.write(a)
1156 | command = (
1157 | f"mpv "
1158 | f'--sub-file="cached/{cid}.ass" '
1159 | f'--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
1160 | f'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.53" '
1161 | f'--referrer="https://www.bilibili.com" '
1162 | f'--audio-file="{audio_url}" '
1163 | f'--title="{title}" '
1164 | f'"{video_url}"'
1165 | )
1166 | if not saw:
1167 | print("如有未显示视频或加载过卡可以在主选项中输入 switch_source 以换视频源. 该信息仅会在第一次播放时显示.")
1168 | saw = True
1169 | with subprocess.Popen(command, shell=True) as p:
1170 | if self.view_online_watch:
1171 | try:
1172 | while p.poll() is None:
1173 | people_watching = user_manager.get(
1174 | f"https://api.bilibili.com/x/player/online/total?cid={cid}&bvid="
1175 | f"{self.bvid}"
1176 | )
1177 | people = f"\r{people_watching.json()['data']['total']} 人正在看"
1178 | print(people, end="", flush=True)
1179 | time.sleep(3)
1180 | except (TypeError, requests.exceptions.RequestException):
1181 | print("获取观看人数时发生错误!\n")
1182 | traceback.print_exc()
1183 | except KeyboardInterrupt:
1184 | return
1185 | print("\n")
1186 |
1187 | def download_one(
1188 | self,
1189 | cid: int,
1190 | pic_url: str,
1191 | title: str = "",
1192 | part_title: str = "",
1193 | base_dir: str = "",
1194 | ):
1195 | if not self.bangumi:
1196 | url = f"https://api.bilibili.com/x/player/playurl?cid={cid}&qn={self.quality}&bvid={self.bvid}"
1197 | else:
1198 | url = f"https://api.bilibili.com/pgc/player/web/playurl?qn={self.quality}&cid={cid}&ep_id={self.bvid}"
1199 |
1200 | req = user_manager.get(url)
1201 | download_url = req.json()["data" if not self.bangumi else "result"]["durl"][0][
1202 | "url"
1203 | ]
1204 | # width = req.json()["data" if not self.bangumi else "result"]["durl"][0]["width"]
1205 | # height = req.json()["data" if not self.bangumi else "result"]["durl"][0]["height"]
1206 | if base_dir:
1207 | download_dir = "download/" + base_dir + \
1208 | "/" + validate_title(title) + "/"
1209 | else:
1210 | download_dir = "download/" + validate_title(title) + "/"
1211 | res = user_manager.get(download_url, stream=True)
1212 | length = float(res.headers["content-length"])
1213 | if not os.path.exists("download"):
1214 | os.mkdir("download")
1215 | if not os.path.exists(download_dir):
1216 | os.makedirs(download_dir)
1217 | dts = download_dir + validate_title(part_title) + ".mp4"
1218 | if os.path.exists(dts):
1219 | c = input("文件已存在, 是否覆盖(y/n)? ")
1220 | if c != "y":
1221 | print("停止操作.")
1222 | return -100
1223 | file = open(dts, "wb")
1224 | progress = tqdm(
1225 | total=length,
1226 | initial=os.path.getsize(dts),
1227 | unit_scale=True,
1228 | desc=reprlib.repr(validate_title(part_title)
1229 | ).replace("'", "") + ".mp4",
1230 | unit="B",
1231 | )
1232 | try:
1233 | for chuck in res.iter_content(chunk_size=1024):
1234 | file.write(chuck)
1235 | progress.update(1024)
1236 | except KeyboardInterrupt:
1237 | file.close()
1238 | os.remove(dts)
1239 | if len(os.listdir(download_dir)) == 0:
1240 | os.rmdir(download_dir)
1241 | print("取消下载.")
1242 | return False
1243 | if not file.closed:
1244 | file.close()
1245 | if not os.path.exists(download_dir + validate_title(title) + ".jpg"):
1246 | print("下载封面中...")
1247 | with open(download_dir + validate_title(title) + ".jpg", "wb") as file:
1248 | file.write(user_manager.get(pic_url).content)
1249 | if not os.path.exists(download_dir + validate_title(part_title) + ".xml"):
1250 | print("下载弹幕中...")
1251 | with open(
1252 | download_dir + validate_title(part_title) + ".xml",
1253 | "w",
1254 | encoding="utf-8",
1255 | ) as danmaku:
1256 | danmaku.write(
1257 | user_manager.get(
1258 | f"https://comment.bilibili.com/{cid}.xml"
1259 | ).content.decode("utf-8")
1260 | )
1261 | with open(download_dir + validate_title(part_title) + ".proto", "wb") as danmaku:
1262 | view = parse_view(cid)
1263 | total = int(view['dmSge']['total'])
1264 | danmaku_byte = [get_danmaku(cid, i)
1265 | for i in range(1, total + 1)]
1266 | # a = danmaku_provider()(
1267 | # b"".join(danmaku_byte),
1268 | # width,
1269 | # height,
1270 | # reserve_blank=0,
1271 | # font_face="SimHei",
1272 | # font_size=25,
1273 | # alpha=0.8,
1274 | # duration_marquee=15.0,
1275 | # duration_still=10.0,
1276 | # comment_filter=None,
1277 | # reduced=False
1278 | # )
1279 | danmaku.write(b"".join(danmaku_byte))
1280 | return True
1281 |
1282 | def download_video_list(self, base_dir=""):
1283 | url = "https://api.bilibili.com/x/web-interface/view/detail?bvid=" + self.bvid
1284 | request = user_manager.get(url, cache=True)
1285 | video = request.json()["data"]["View"]["pages"]
1286 | title = request.json()["data"]["View"]["title"]
1287 | pic = request.json()["data"]["View"]["pic"]
1288 | total = len(video)
1289 | count = 0
1290 | for i in video:
1291 | count += 1
1292 | print(f"{count} / {total}")
1293 | cid = i["cid"]
1294 | part_title = i["part"]
1295 | if not self.download_one(
1296 | cid, pic, title=title, part_title=part_title, base_dir=base_dir
1297 | ):
1298 | return False
1299 | return True
1300 |
1301 |
1302 | class BilibiliMain:
1303 | @staticmethod
1304 | def recommend() -> list:
1305 | r = user_manager.get(
1306 | "https://api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd?"
1307 | + encrypt_wbi("ps=5")
1308 | )
1309 | return r.json()["data"]["item"]
1310 |
1311 | @staticmethod
1312 | def get_media_list(media_id: int):
1313 | r = user_manager.get(
1314 | f"https://api.bilibili.com/x/v3/fav/resource/ids?media_id={media_id}&platform=web"
1315 | )
1316 | return r.json()["data"]
1317 |
1318 | @staticmethod
1319 | def media_list_info(media_id: int):
1320 | r = user_manager.get(
1321 | f"https://api.bilibili.com/x/v3/fav/folder/info?media_id={media_id}"
1322 | )
1323 | return r.json()["data"]
1324 |
1325 |
1326 | class BilibiliInterface:
1327 | def __init__(self):
1328 | self.audio = 30280
1329 | self.quality: int = 32 if not user_manager.mid else 80
1330 | self.view_online_watch = True
1331 | self.source = "main"
1332 | self.bilibili_favorite = BilibiliFavorite()
1333 | self.interaction: BilibiliInteraction = BilibiliInteraction()
1334 | # self.manga = BilibiliManga()
1335 | self.history = BilibiliHistory(user_manager.csrf)
1336 | self.bangumi = BilibiliBangumi(self.quality)
1337 | self.delay = 1
1338 |
1339 | def favorite(self, mid=0):
1340 | if not user_manager.is_login:
1341 | print("请先登录!")
1342 | return
1343 | fav_id = self.bilibili_favorite.select_one_favorite(
1344 | user_manager.mid if not mid else mid)
1345 | if not fav_id:
1346 | return
1347 | all_request = self.bilibili_favorite.get_favorite(fav_id)
1348 | for i in all_request:
1349 | for num, item in enumerate(i):
1350 | print(num + 1, ":")
1351 | print("封面: ", item["cover"])
1352 | print("标题: ", item["title"])
1353 | print(
1354 | "作者: ",
1355 | item["upper"]["name"],
1356 | " bvid: ",
1357 | item["bvid"],
1358 | " 日期: ",
1359 | datetime.datetime.fromtimestamp(item["pubtime"]).strftime(
1360 | "%Y-%m-%d %H:%M:%S"
1361 | ),
1362 | " 视频时长:",
1363 | format_time(item["duration"]),
1364 | " 观看量: ",
1365 | item["cnt_info"]["play"],
1366 | )
1367 | while True:
1368 | command = input("选择视频: ")
1369 | if command == "quit" or command == "q":
1370 | return
1371 | elif not command:
1372 | break
1373 | elif not command.isdecimal():
1374 | print("输入的不是整数!")
1375 | continue
1376 | elif int(command) > len(i) or int(command) <= 0:
1377 | print("选视频超出范围!")
1378 | continue
1379 | bvid = i[int(command) - 1]["bvid"]
1380 | self.view_video(bvid, no_favorite=True)
1381 |
1382 | def recommend(self):
1383 | print("推荐界面")
1384 | while True:
1385 | recommend_content = BilibiliMain.recommend()
1386 | for num, item in enumerate(recommend_content):
1387 | print(num + 1, ":")
1388 | print("封面: ", item["pic"])
1389 | print("标题: ", item["title"])
1390 | print(
1391 | "作者: ",
1392 | item["owner"]["name"],
1393 | " bvid: ",
1394 | item["bvid"],
1395 | " 日期: ",
1396 | datetime.datetime.fromtimestamp(item["pubdate"]).strftime(
1397 | "%Y-%m-%d %H:%M:%S"
1398 | ),
1399 | " 视频时长:",
1400 | format_time(item["duration"]),
1401 | " 观看量: ",
1402 | item["stat"]["view"],
1403 | )
1404 | while True:
1405 | command = input("选择视频: ")
1406 | if command == "quit" or command == "q":
1407 | return
1408 | elif not command:
1409 | break
1410 | elif not command.isdecimal():
1411 | print("输入的不是整数!")
1412 | continue
1413 | elif int(command) > len(recommend_content) or int(command) <= 0:
1414 | print("选视频超出范围!")
1415 | continue
1416 | bvid = recommend_content[int(command) - 1]["bvid"]
1417 | # title = recommend_request.json()['data']['item'][int(command) - 1]['title']
1418 | self.view_video(bvid)
1419 |
1420 | def address(self):
1421 | video_address = input("输入地址: ")
1422 | if "b23.tv" in video_address:
1423 | video_address = user_manager.get(video_address).url
1424 |
1425 | url_split = video_address.split("/")
1426 | if url_split[-1].startswith("?") or not url_split[-1]:
1427 | video_id = url_split[-2]
1428 | else:
1429 | video_id = url_split[-1].split("?")[0]
1430 |
1431 | if video_id.startswith("BV"):
1432 | view_short_video_info(video_id)
1433 | self.view_video(bvid=video_id)
1434 | else:
1435 | try:
1436 | view_short_video_info(av2bv(int(video_id.strip("av"))))
1437 | except (KeyError, ValueError):
1438 | traceback.print_exc()
1439 | print("视频解析错误, 请确保你输入的视频地址正确.")
1440 | return
1441 | self.view_video(bvid=av2bv(int(video_id.strip("av"))))
1442 |
1443 | # def play_interact_video(self, bvid: str, cid: int):
1444 | # self.play(bvid, cid, view_online_watch=False)
1445 | #
1446 | # graph_version = request_manager.get(f"https://api.bilibili.com/x/player/v2?bvid={bvid}&cid={cid}")
1447 | # graph_version = graph_version.json()['data']['interaction']['graph_version']
1448 | #
1449 | # edge_id = ""
1450 | #
1451 | # while True:
1452 | # edge_info = f"https://api.bilibili.com/x/stein/edgeinfo_v2" \
1453 | # f"?graph_version={graph_version}" \
1454 | # f"&bvid={bvid}" \
1455 | # f"&edge_id={edge_id}"
1456 | # r = request_manager.get(edge_info)
1457 | # if not r.json()['data']['edges'].get('questions'):
1458 | # print("互动视频已到达末尾.")
1459 | # score = input("是否评分? (y/n): ")
1460 | # if score == "y":
1461 | # score = input("评几分? (1-5): ")
1462 | # if not score.isdecimal():
1463 | # print("输入错误! 将停止评分")
1464 | # return
1465 | # self.interaction.mark_interact_video(bvid, int(score))
1466 | # break
1467 | # for i, j in enumerate(r.json()['data']['edges']['questions'][0]['choices']):
1468 | # print(f"{i + 1}: {j['option']}")
1469 | # while True:
1470 | # index = input("选择选项: ")
1471 | # if index.isdecimal():
1472 | # break
1473 | # else:
1474 | # print("请输入数字!")
1475 | # edge_id = r.json()['data']['edges']['questions'][0]['choices'][int(index) + 1]['id']
1476 | # cid = r.json()['data']['edges']['questions'][0]['choices'][int(index) + 1]['cid']
1477 | # self.play(bvid, cid, view_online_watch=False)
1478 |
1479 | def like(self, bvid, unlike=False):
1480 | if not user_manager.is_login:
1481 | print("请先登录!")
1482 | return
1483 | self.interaction.like(bvid, unlike=unlike)
1484 |
1485 | def coin(self, bvid):
1486 | if not user_manager.is_login:
1487 | print("请先登录!")
1488 | return
1489 | coin_count = input("输入币数(1-2): ")
1490 | if coin_count != "1" and coin_count != "2":
1491 | print("币数错误!")
1492 | return
1493 | self.interaction.coin(bvid, int(coin_count))
1494 |
1495 | def triple(self, bvid):
1496 | if not user_manager.is_login:
1497 | print("请先登录!")
1498 | return
1499 | self.interaction.triple(bvid)
1500 |
1501 | def add_favorite(self, aid):
1502 | if not user_manager.is_login:
1503 | print("请先登录!")
1504 | return
1505 | fav_id = self.bilibili_favorite.select_favorite(user_manager.mid, aid)
1506 | if fav_id == 0:
1507 | return
1508 | self.interaction.favorite(aid, fav_id) # type: ignore
1509 |
1510 | def download_favorite_video(self):
1511 | if not user_manager.is_login:
1512 | print("请先登录!")
1513 | return
1514 | fav_id = self.bilibili_favorite.select_one_favorite(user_manager.mid)
1515 | if fav_id == 0:
1516 | return
1517 | info = self.bilibili_favorite.get_favorite_information(fav_id)
1518 | count = 0
1519 | total = info["media_count"]
1520 | for i in self.bilibili_favorite.get_favorite(fav_id):
1521 | for j in i:
1522 | count += 1
1523 | print(f"收藏夹进度: {count} / {total}")
1524 | video = BilibiliVideo(bvid=j["bvid"], quality=80)
1525 | if not video.download_video_list(
1526 | base_dir=validate_title(info["title"])
1527 | ):
1528 | return
1529 |
1530 | # def download_manga(self):
1531 | # if not user_manager.is_login:
1532 | # print("请先登录!")
1533 | # return
1534 | # print("漫画id: 即 https://manga.bilibili.com/detail/mc29410 中的 29410")
1535 | # try:
1536 | # comic_id = input("请输入漫画id或url: ")
1537 | # if comic_id.startswith("https"):
1538 | # comic_id = comic_id.split("mc")[1]
1539 | # self.manga.download_manga(int(comic_id))
1540 | # except (ValueError, IndexError):
1541 | # print("id输入错误.")
1542 | # except KeyboardInterrupt:
1543 | # print("停止下载.")
1544 |
1545 | def export_favorite(self):
1546 | if not user_manager.is_login:
1547 | print("请先登录!")
1548 | return
1549 | fav_id = self.bilibili_favorite.select_one_favorite(user_manager.mid)
1550 | if fav_id == 0:
1551 | return
1552 | self.bilibili_favorite.export_favorite(fav_id)
1553 |
1554 | def export_history(self):
1555 | if not user_manager.is_login:
1556 | print("请先登录!")
1557 | return
1558 | with open(f"history_{str(round(time.time()))}.json", "w", encoding="utf-8") as f:
1559 | json.dump(self.history.dump_history(), f,
1560 | ensure_ascii=False, indent=4)
1561 |
1562 | def export_all_favorite(self):
1563 | if not user_manager.is_login:
1564 | print("请先登录!")
1565 | return
1566 | fav_id = self.bilibili_favorite.list_favorite(user_manager.mid)
1567 | for i in fav_id:
1568 | self.bilibili_favorite.export_favorite(i)
1569 |
1570 | def view_history(self):
1571 | if not user_manager.is_login:
1572 | print("请先登录!")
1573 | return
1574 | print("历史界面")
1575 | print()
1576 | for history in BilibiliHistory.get_history():
1577 | flag = True
1578 | while flag:
1579 | for num, item in enumerate(history):
1580 | if item["history"]["business"] != "archive":
1581 | print("该类型的历史记录不支持播放.")
1582 | continue
1583 | print(num + 1, ":")
1584 | print("封面: ", item["cover"])
1585 | print("标题: ", item["title"])
1586 | print(
1587 | "作者: ",
1588 | item["author_name"],
1589 | " bvid: ",
1590 | item["history"]["bvid"],
1591 | " 视频时长:",
1592 | format_time(item["progress"]),
1593 | "/",
1594 | format_time(item["duration"]),
1595 | )
1596 | print(
1597 | "观看时间: ",
1598 | datetime.datetime.fromtimestamp(item["view_at"]).strftime(
1599 | "%Y-%m-%d %H:%M:%S"
1600 | ),
1601 | )
1602 | while True:
1603 | command = input("选择视频: ")
1604 | if command == "quit" or command == "q":
1605 | return
1606 | elif not command:
1607 | flag = False
1608 | break
1609 | elif not command.isdecimal():
1610 | print("输入的不是整数!")
1611 | continue
1612 | elif int(command) > len(history) or int(command) <= 0:
1613 | print("选视频超出范围!")
1614 | continue
1615 | bvid = history[int(command) - 1]["history"]["bvid"]
1616 | if not bvid:
1617 | print("该类型的历史记录不支持播放.")
1618 | continue
1619 | # title = recommend_request.json()['data']['item'][int(command) - 1]['title']
1620 | self.view_video(bvid)
1621 |
1622 | def user_space(self, mid: int):
1623 | user_data = BilibiliUserSpace.get_user_data(mid)
1624 | print("用户空间")
1625 | print("")
1626 | print("用户名: " + user_data["name"])
1627 | print("头像: " + user_data["face"])
1628 | print(
1629 | "Level: "
1630 | + str(user_data["level"])
1631 | + (" 硬核会员" if user_data["is_senior_member"] == 1 else "")
1632 | )
1633 | print("个性签名: " + user_data["sign"])
1634 | print("")
1635 | while True:
1636 | command = input("用户空间选项: ")
1637 | if command == "list_video":
1638 | self.list_user_video(mid)
1639 | elif command == "get_follow_bangumi":
1640 | for i, j in enumerate(BilibiliBangumi.get_follow_bangumi(mid)):
1641 | print(f"{i + 1}: ")
1642 | print(f"封面: {j['img']}")
1643 | print(
1644 | f"名称: {j['title']} 更新进度: {j['update_progress']} 观看进度: {j['watch_progress']}"
1645 | )
1646 | elif command == "quit" or command == "q":
1647 | return
1648 | elif command == "list_fans":
1649 | self.list_fans(mid)
1650 | elif command == "list_followed":
1651 | self.list_followed(mid)
1652 | elif command == "list_favorite":
1653 | self.favorite(mid)
1654 | elif command:
1655 | print("未知命令!")
1656 |
1657 | def list_fans(self, mid: int):
1658 | fans_list = BilibiliUserSpace.get_following_list(mid)
1659 | print("粉丝数: " + str(len(fans_list)))
1660 | if len(fans_list) > 100:
1661 | print("注意: 粉丝数超过100, 根据b站系统限制默认只能获取100条信息.")
1662 | for i, j in enumerate(fans_list):
1663 | print(f"{i + 1}:")
1664 | print(f"头像: {j['face']}")
1665 | print(f"昵称: {j['uname']} mid: {j['mid']}")
1666 | print(f"签名: {j['sign']}")
1667 | while True:
1668 | select = input("选择用户: ")
1669 | if select == "quit" or select == "q":
1670 | return
1671 | elif not select.isdecimal():
1672 | print("输入的不是整数!")
1673 | continue
1674 | elif int(select) > len(fans_list) or int(select) <= 0:
1675 | print("选择用户超出范围!")
1676 | continue
1677 | while True:
1678 | command = input("粉丝选项: ")
1679 | if command == "user_space":
1680 | self.user_space(fans_list[int(select) - 1]['mid'])
1681 | elif command == "quit" or command == "q":
1682 | break
1683 | else:
1684 | print("未知命令! ")
1685 |
1686 | def list_followed(self, mid: int):
1687 | fans_list = BilibiliUserSpace.get_followed_list(mid)
1688 | print("关注数: " + str(len(fans_list)))
1689 | if len(fans_list) > 100:
1690 | print("注意: 关注数超过100, 根据b站系统限制默认只能获取100条信息.")
1691 | for i, j in enumerate(fans_list):
1692 | print(f"{i + 1}:")
1693 | print(f"头像: {j['face']}")
1694 | print(f"昵称: {j['uname']} mid: {j['mid']}")
1695 | print(f"签名: {j['sign']}")
1696 | while True:
1697 | select = input("选择用户: ")
1698 | if select == "quit" or select == "q":
1699 | return
1700 | elif not select.isdecimal():
1701 | print("输入的不是整数!")
1702 | continue
1703 | elif int(select) > len(fans_list) or int(select) <= 0:
1704 | print("选择用户超出范围!")
1705 | continue
1706 | while True:
1707 | command = input("关注选项: ")
1708 | if command == "user_space":
1709 | self.user_space(fans_list[int(select) - 1]['mid'])
1710 | elif command == "quit" or command == "q":
1711 | break
1712 | else:
1713 | print("未知命令! ")
1714 |
1715 | def list_user_video(self, mid: int):
1716 | for i in BilibiliUserSpace.get_user_video(mid):
1717 | if not i:
1718 | print("该UP主未发送过视频.")
1719 | return
1720 | for num, item in enumerate(i):
1721 | print(num + 1, ":")
1722 | print("封面: ", item["pic"])
1723 | print("标题: ", item["title"])
1724 | print(
1725 | "作者: ",
1726 | item["author"],
1727 | " bvid: ",
1728 | item["bvid"],
1729 | " 日期: ",
1730 | datetime.datetime.fromtimestamp(item["created"]).strftime(
1731 | "%Y-%m-%d %H:%M:%S"
1732 | ),
1733 | " 视频时长:",
1734 | item["length"],
1735 | " 观看量: ",
1736 | item["play"],
1737 | )
1738 | while True:
1739 | command = input("选择视频: ")
1740 | if command == "quit" or command == "q":
1741 | return
1742 | if not command:
1743 | break
1744 | elif not command.isdecimal():
1745 | print("输入的不是整数!")
1746 | continue
1747 | elif int(command) > len(i) or int(command) <= 0:
1748 | print("选视频超出范围!")
1749 | continue
1750 | bvid = i[int(command) - 1]["bvid"]
1751 | self.view_video(bvid)
1752 |
1753 | def search(self):
1754 | keyword = input("输入关键词: ")
1755 | if keyword == "quit" or keyword == "q":
1756 | return
1757 | for i in BilibiliSearch.search(keyword):
1758 | for index, result in enumerate(i):
1759 | print(index + 1, ":")
1760 | print("封面: ", "https:" + result["pic"])
1761 | print("标题: ", result["title"])
1762 | print(
1763 | "作者: ",
1764 | result["author"],
1765 | " bvid: ",
1766 | result["bvid"],
1767 | " 日期: ",
1768 | datetime.datetime.fromtimestamp(result["pubdate"]).strftime(
1769 | "%Y-%m-%d %H:%M:%S"
1770 | ),
1771 | " 视频时长:",
1772 | result["duration"],
1773 | " 观看量: ",
1774 | result["play"],
1775 | )
1776 | while True:
1777 | command = input("选择视频: ")
1778 | if command == "quit" or command == "q":
1779 | return
1780 | if not command:
1781 | break
1782 | elif not command.isdecimal():
1783 | print("输入的不是整数!")
1784 | continue
1785 | elif int(command) > len(i) or int(command) <= 0:
1786 | print("选视频超出范围!")
1787 | continue
1788 | bvid = i[int(command) - 1]["bvid"]
1789 | self.view_video(bvid)
1790 |
1791 | def view_comment(self, bvid):
1792 | comment_generator = BilibiliComment.get_comment(1, bv2av(bvid))
1793 | for i in comment_generator:
1794 | for index, result in enumerate(i):
1795 | print(index + 1, ":")
1796 | print(
1797 | "作者: ",
1798 | result["member"]["uname"],
1799 | " 日期: ",
1800 | datetime.datetime.fromtimestamp(result["ctime"]).strftime(
1801 | "%Y-%m-%d %H:%M:%S"
1802 | ),
1803 | " 点赞量: ",
1804 | result["like"],
1805 | )
1806 | print("内容: ", result["content"]["message"])
1807 | while True:
1808 | command = input("评论选项: ")
1809 | if command == "quit" or command == "q":
1810 | return
1811 | if command == "next":
1812 | break
1813 | else:
1814 | print("未知命令!")
1815 |
1816 | def view_video(self, bvid, no_favorite=False):
1817 | video = BilibiliVideo(
1818 | bvid=bvid, quality=self.quality, view_online_watch=self.view_online_watch, source=self.source
1819 | )
1820 | while True:
1821 | command = input("视频选项(p/l/ul/c/t/f/d/da/q/fo/ufo/cm): ")
1822 | if command == "quit" or command == "q":
1823 | return
1824 | if command == "play" or command == "p":
1825 | video.select_video()
1826 | elif command == "switch_quality":
1827 | cid, title, _, _, _ = video.select_video(
1828 | return_information=True
1829 | )
1830 | video.switch_quality(cid)
1831 | elif command == "download" or command == "d":
1832 | cid, title, part_title, pic, is_dynamic = video.select_video(
1833 | return_information=True
1834 | )
1835 | print(is_dynamic)
1836 | if is_dynamic:
1837 | print("互动视频无法下载! ")
1838 | return
1839 | video.download_one(
1840 | cid, pic_url=pic, title=title, part_title=part_title)
1841 | elif command == "download_video_list" or command == "da":
1842 | video.download_video_list(bvid)
1843 | elif command == "like" or command == "l":
1844 | self.like(bvid)
1845 | elif command == "unlike" or command == "ul":
1846 | self.like(bvid, unlike=True)
1847 | elif command == "coin" or command == "c":
1848 | self.coin(bvid)
1849 | elif command == "triple" or command == "t":
1850 | self.triple(bvid)
1851 | elif (command == "favorite" or command == "f") and not no_favorite:
1852 | self.add_favorite(bv2av(bvid))
1853 | user_manager.cached_response = {}
1854 | elif command == "follow" or command == "fo":
1855 | BilibiliUserSpace.modify_relation(
1856 | video.get_author_mid(), modify_type=1)
1857 | elif command == "unfollow" or command == "ufo":
1858 | BilibiliUserSpace.modify_relation(
1859 | video.get_author_mid(), modify_type=2)
1860 | elif command == "comment" or command == "cm":
1861 | self.view_comment(bvid)
1862 | elif command == "export_comment":
1863 | data = []
1864 | for i in BilibiliComment.get_comment(1, bv2av(bvid)):
1865 | for j in i:
1866 | data.append(j)
1867 | time.sleep(self.delay)
1868 | for i in data:
1869 | if i["reply_count"] > 0:
1870 | replies = BilibiliComment.get_comment_reply(1, bv2av(bvid), i["rpid"])
1871 | replies_data = []
1872 | for reply in replies:
1873 | replies_data.append(reply)
1874 | time.sleep(self.delay)
1875 | i["reply"] = replies_data
1876 | with open(f"comment_{bvid}.json", "w", encoding="utf-8") as f:
1877 | json.dump(data, f, indent=4, ensure_ascii=False)
1878 | elif command == "export_danmaku":
1879 | cid, title, _, _, _ = video.select_video(
1880 | return_information=True
1881 | )
1882 | view = parse_view(cid)
1883 | total = int(view['dmSge']['total'])
1884 | danmaku_byte_list = [get_danmaku(
1885 | cid, i) for i in range(1, total + 1)]
1886 | danmaku_byte = b"".join(danmaku_byte_list)
1887 | DM = DmSegMobileReply()
1888 | DM.ParseFromString(danmaku_byte)
1889 | with open(f"{cid}.json", "w", encoding="utf-8") as f:
1890 | json.dump(MessageToJson(DM), f,
1891 | indent=4, ensure_ascii=False)
1892 | elif command == "view_user":
1893 | self.user_space(video.get_author_mid())
1894 | elif command == "view_video_collection":
1895 | video.select_video_collection()
1896 | else:
1897 | print("未知命令!")
1898 |
1899 | def dynamic(self):
1900 | dynamics = BilibiliDynamic.get_dynamic()["data"]["items"]
1901 | for i in dynamics:
1902 | print(i)
1903 |
1904 | def login(self):
1905 | if user_manager.is_login:
1906 | print("已经登录!")
1907 | else:
1908 | cookies = ""
1909 | BilibiliLogin.generate_cookie()
1910 | login_method = input("登录方式 (sms/password): ")
1911 | if login_method == "password":
1912 | username = input("输入用户名: ")
1913 | password = getpass.getpass("输入密码: ")
1914 | cookies = BilibiliLogin.login_by_password(
1915 | username, password)
1916 | elif login_method == "sms":
1917 | print("默认区号为 +86 (中国).")
1918 | tel = input("输入电话号码: ")
1919 | captcha_key = BilibiliLogin.send_sms(tel)
1920 | if captcha_key:
1921 | sms_code = input("输入认证码: ")
1922 | cookies = BilibiliLogin.login_by_sms(tel, captcha_key, sms_code)
1923 | if cookies:
1924 | print("登录成功!")
1925 | with open("cookie.txt", "w") as f:
1926 | f.write(cookies)
1927 | user_manager.refresh_login()
1928 |
1929 | def main(self):
1930 | while True:
1931 | command = input("主选项(r/a/b/f/s/q/l): ")
1932 | command = command.lower().strip()
1933 | if command == "recommend" or command == "r":
1934 | self.recommend()
1935 | elif command == "address" or command == "a":
1936 | self.address()
1937 | elif command == "help" or command == "h":
1938 | show_help()
1939 | elif command == "bangumi" or command == "b":
1940 | bangumi_address = input("输入地址: ")
1941 | if bangumi_address.split("/")[-1].startswith("ep"):
1942 | self.bangumi.select_bangumi(
1943 | epid=bangumi_address.split("/")[-1].strip("ep")
1944 | )
1945 | else:
1946 | self.bangumi.select_bangumi(
1947 | ssid=bangumi_address.split("/")[-1].strip("ss")
1948 | )
1949 | elif command == "favorite" or command == "f":
1950 | self.favorite()
1951 | elif command == "quit" or command == "q":
1952 | sys.exit(0)
1953 | elif command == "search" or command == "s":
1954 | self.search()
1955 | elif command == "enable_online_watching":
1956 | self.view_online_watch = True
1957 | elif command == "disable_online_watching":
1958 | self.view_online_watch = False
1959 | elif command == "clean_cache":
1960 | shutil.rmtree("cached")
1961 | os.mkdir("cached")
1962 | elif command == "refresh_login":
1963 | user_manager.refresh_login()
1964 | elif command == "export_favorite":
1965 | self.export_favorite()
1966 | elif command == "export_history":
1967 | self.export_history()
1968 | elif command == "export_all_favorite":
1969 | self.export_all_favorite()
1970 | elif command == "download_favorite":
1971 | self.download_favorite_video()
1972 | elif command == "history":
1973 | self.view_history()
1974 | elif command == "dynamic":
1975 | self.dynamic()
1976 | elif command == "view_self":
1977 | if user_manager.is_login:
1978 | self.user_space(user_manager.mid)
1979 | else:
1980 | print("用户未登录!")
1981 | elif command == "view_user":
1982 | self.user_space(int(input("请输入用户mid: ")))
1983 | # elif command == "download_manga":
1984 | # self.download_manga()
1985 | elif command == "switch_source":
1986 | print("切换播放源成功")
1987 | if self.source == "backup":
1988 | self.source = ""
1989 | else:
1990 | self.source = "backup"
1991 | elif command == "login" or command == "l":
1992 | self.login()
1993 | elif command == "logout" or command == "lo":
1994 | if input("确定退出? (y/n)").lower() == "y":
1995 | BilibiliLogin.logout()
1996 | elif command == "set_export_delay":
1997 | self.delay = int(input("输入导出延迟: "))
1998 | else:
1999 | print("未知命令!")
2000 |
2001 |
2002 | print(f"LBCC v{__version__}.")
2003 | print()
2004 | print("Type \"help\" for more information.")
2005 | print()
2006 |
2007 | if __name__ == "__main__":
2008 | user_manager.login()
2009 | bilibili = BilibiliInterface()
2010 | bilibili.main()
2011 |
--------------------------------------------------------------------------------