├── 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 | ![](https://laosun-image.obs.cn-north-4.myhuaweicloud.com/2025-1.png) 8 | 9 | ![](https://laosun-image.obs.cn-north-4.myhuaweicloud.com/2025-2.png) 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 | ![](https://laosun-image.obs.cn-north-4.myhuaweicloud.com/20221011121641.png) 11 | 12 | 并解压zip 13 | 14 | 打开命令行,切换到此文件夹或对文件夹Shift+右键,点击"在命令行打开此文件夹" (在Win11中可以直接右键打开终端) 15 | 16 | 输入 `pip install -r requirements.txt` 17 | 18 | ![](https://laosun-image.obs.cn-north-4.myhuaweicloud.com/20221011122441.png) 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 | ![](https://laosun-image.obs.cn-north-4.myhuaweicloud.com/20230225201606.png) 26 | 27 | 输入 `python main.py` 28 | 29 | ![](https://laosun-image.obs.cn-north-4.myhuaweicloud.com/20221011205705.png) 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 | --------------------------------------------------------------------------------