├── .dockerignore ├── .github └── workflows │ └── main.yml ├── README.md ├── action.yml ├── bilibili.py ├── dockerfile └── manga_sign.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | README.md 3 | .idea/ 4 | __pycache__/ 5 | LICENSE -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | schedule: 6 | - cron: 00 00 * * * 7 | 8 | jobs: 9 | Bilibili-Manga: 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - name: Checkout master 13 | uses: actions/checkout@v2 14 | - name: Main 15 | uses: ./ 16 | env: 17 | account: ${{ secrets.ACCOUNT }} 18 | password: ${{ secrets.PASSWORD }} 19 | push_key: ${{ secrets.PUSH_KEY }} 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bilibili-Manga 漫画自动签到 2 | 3 | ### 这个程序有什么用? 4 | 5 | 可以帮助你自动登陆签到 B 站 \"哔哩哔哩漫画\" 6 | 7 | 8 | ### 如何使用? 9 | 10 | #### 方法一 使用 Github Actions(最简单, 推荐) 11 | 12 | B 站[视频教程](https://www.bilibili.com/video/av10000/). 13 | 14 | **本项目需要设置的 Secrets:** 15 | 16 | | 名称 | 内容 | 类型 | 17 | | -------- | ------------- | ---- | 18 | | ACCOUNT | 你的B站用户名 | 必写 | 19 | | PASSWORD | 你的B站密码 | 必写 | 20 | | PUSH_KEY | Server酱SCKEY值 | 可写 | 21 | 22 | [Server酱-微信推送](https://sc.ftqq.com/) 可直接使用你所注册的Github账号登陆,登录成功后点击'发送信息' 复制您的SCKEY值即可,具体微信绑定方式请查看Server酱 微信推送 23 | 24 | **本项目默认设置**: 每天早上 8 点打卡签到. 25 | 26 | #### 方法二 直接运行 Python(不推荐) 27 | 28 | 1. 找任意一台安装好 Python 3.6 或以上版本的服务器 29 | 2. 在任意目录下执行 `git clone https://github.com/BlueskyClouds/Bilibili-Manga && cd Bilibili-Manga` 30 | 3. 使用 pip 安装依赖库, 参考命令`pip3 install requests rsa chardet` 31 | 4. 使用 Cron 每日执行 `python3 manga_sign.py` 32 | 33 | 设置好账号密码后,编辑README.md文件, 随意填写一个字符然后提交即可。 34 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Bilibili-Manga' 2 | description: 'Bilibili 漫画自动签到.' 3 | #inputs: 4 | # account: # id of input 5 | # description: 'B站账号' 6 | # required: true 7 | # password: 8 | # description: 'B站密码' 9 | # required: true 10 | # push_key: 11 | # description: 'Server酱 SCKEY' 12 | # required: true 13 | 14 | runs: 15 | using: 'docker' 16 | image: 'dockerfile' 17 | -------------------------------------------------------------------------------- /bilibili.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.6 2 | # -*- coding: utf-8 -*- 3 | 4 | """Bilibili Toolkit 哔哩哔哩工具箱 5 | https://github.com/Hsury/Bilibili-Toolkit""" 6 | 7 | banner = r""" 8 | \\ // 9 | \\ // 10 | ##################### ________ ___ ___ ___ ________ ___ ___ ___ 11 | ## ## |\ __ \ |\ \ |\ \ |\ \ |\ __ \ |\ \ |\ \ |\ \ 12 | ## // \\ ## \ \ \|\ /_\ \ \\ \ \ \ \ \\ \ \|\ /_\ \ \\ \ \ \ \ \ 13 | ## // \\ ## \ \ __ \\ \ \\ \ \ \ \ \\ \ __ \\ \ \\ \ \ \ \ \ 14 | ## ## \ \ \|\ \\ \ \\ \ \____ \ \ \\ \ \|\ \\ \ \\ \ \____ \ \ \ 15 | ## www ## \ \_______\\ \__\\ \_______\\ \__\\ \_______\\ \__\\ \_______\\ \__\ 16 | ## ## \|_______| \|__| \|_______| \|__| \|_______| \|__| \|_______| \|__| 17 | ##################### 18 | \/ \/ 哔哩哔哩 (゜-゜)つロ 干杯~ 19 | """ 20 | 21 | import base64 22 | import chardet 23 | import functools 24 | import hashlib 25 | import json 26 | import os 27 | import platform 28 | import random 29 | import requests 30 | import rsa 31 | import shutil 32 | import subprocess 33 | import sys 34 | import threading 35 | import time 36 | from multiprocessing import freeze_support, Manager, Pool, Process 37 | from urllib import parse 38 | 39 | __author__ = "Hsury" 40 | __email__ = "i@hsury.com" 41 | __license__ = "SATA" 42 | __version__ = "2020.7.20" 43 | 44 | class Bilibili: 45 | app_key = "bca7e84c2d947ac6" 46 | patterns = { 47 | 'video': { 48 | 'id': 1, 49 | 'prefix': "https://www.bilibili.com/video/av", 50 | }, 51 | 'activity': { 52 | 'id': 4, 53 | 'prefix': "https://www.bilibili.com/blackboard/", 54 | }, 55 | 'gallery': { 56 | 'id': 11, 57 | 'prefix': "https://h.bilibili.com/", 58 | }, 59 | 'article': { 60 | 'id': 12, 61 | 'prefix': "https://www.bilibili.com/read/cv", 62 | }, 63 | } 64 | 65 | def __init__(self, https=True, queue=None): 66 | self._session = requests.Session() 67 | self._session.headers.update({'User-Agent': "Mozilla/5.0 BiliDroid/6.4.0 (bbcallen@gmail.com) os/android model/M1903F11I mobi_app/android build/6040500 channel/bili innerVer/6040500 osVer/9.0.0 network/2"}) 68 | self.__queue = queue 69 | self.get_cookies = lambda: self._session.cookies.get_dict(domain=".bilibili.com") 70 | self.get_csrf = lambda: self.get_cookies().get("bili_jct", "") 71 | self.get_sid = lambda: self.get_cookies().get("sid", "") 72 | self.get_uid = lambda: self.get_cookies().get("DedeUserID", "") 73 | self.access_token = "" 74 | self.refresh_token = "" 75 | self.username = "" 76 | self.password = "" 77 | self.info = { 78 | 'ban': False, 79 | 'coins': 0, 80 | 'experience': { 81 | 'current': 0, 82 | 'next': 0, 83 | }, 84 | 'face': "", 85 | 'level': 0, 86 | 'nickname': "", 87 | } 88 | self.protocol = "https" if https else "http" 89 | self.proxy = None 90 | self.proxy_pool = set() 91 | 92 | def _log(self, message): 93 | log = f"[{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}][{self.username if self.username else '#' + self.get_uid() if self.get_uid() else ''}] {message}" 94 | print(log) 95 | self.__push_to_queue("log", log) 96 | 97 | def _requests(self, method, url, decode_level=2, enable_proxy=True, retry=10, timeout=15, **kwargs): 98 | if method in ["get", "post"]: 99 | for _ in range(retry + 1): 100 | try: 101 | response = getattr(self._session, method)(url, timeout=timeout, proxies=self.proxy if enable_proxy else None, **kwargs) 102 | return response.json() if decode_level == 2 else response.content if decode_level == 1 else response 103 | except: 104 | if enable_proxy: 105 | self.set_proxy() 106 | return None 107 | 108 | def _solve_captcha(self, image): 109 | url = "https://bili.dev:2233/captcha" 110 | payload = {'image': base64.b64encode(image).decode("utf-8")} 111 | response = self._requests("post", url, json=payload) 112 | return response['message'] if response and response.get("code") == 0 else None 113 | 114 | def __bvid_handle(args_index=None, kwargs_key="aid"): 115 | def decorator(func): 116 | @functools.wraps(func) 117 | def wrapper(*args, **kwargs): 118 | self = args[0] 119 | if args_index is not None and args_index < len(args): 120 | result = Bilibili.bvid_to_aid(args[args_index]) 121 | if result: 122 | args = list(args) 123 | self._log(f"{args[args_index]}被自动转换为av{result}") 124 | args[args_index] = result 125 | if kwargs_key is not None and kwargs_key in kwargs: 126 | result = Bilibili.bvid_to_aid(kwargs[kwargs_key]) 127 | if result: 128 | self._log(f"{kwargs[kwargs_key]}被自动转换为av{result}") 129 | kwargs[kwargs_key] = result 130 | return func(*args, **kwargs) 131 | return wrapper 132 | return decorator 133 | 134 | def __push_to_queue(self, manufacturer, data): 135 | if self.__queue: 136 | self.__queue.put({ 137 | 'uid': self.get_uid(), 138 | 'time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())), 139 | 'manufacturer': manufacturer, 140 | 'data': data, 141 | }) 142 | 143 | @staticmethod 144 | def bvid_to_aid(bvid="BV17x411w7KC"): 145 | # Snippet source: https://www.zhihu.com/question/381784377/answer/1099438784 146 | table = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF" 147 | tr = {} 148 | for i in range(58): 149 | tr[table[i]] = i 150 | s = [11, 10, 3, 8, 4, 6] 151 | xor = 177451812 152 | add = 8728348608 153 | r = 0 154 | try: 155 | for i in range(6): 156 | r += tr[bvid[s[i]]] * 58 ** i 157 | return (r - add) ^ xor 158 | except: 159 | return None 160 | 161 | @staticmethod 162 | def calc_sign(param): 163 | salt = "60698ba2f68e01ce44738920a0ffe768" 164 | sign_hash = hashlib.md5() 165 | sign_hash.update(f"{param}{salt}".encode()) 166 | return sign_hash.hexdigest() 167 | 168 | def set_proxy(self, add=None): 169 | if isinstance(add, str): 170 | self.proxy_pool.add(add) 171 | elif isinstance(add, list): 172 | self.proxy_pool.update(add) 173 | if self.proxy_pool: 174 | proxy = random.sample(self.proxy_pool, 1)[0] 175 | self.proxy = {self.protocol: f"{self.protocol}://{proxy}"} 176 | # self._log(f"使用{self.protocol.upper()}代理: {proxy}") 177 | else: 178 | self.proxy = None 179 | return self.proxy 180 | 181 | # 登录 182 | def login(self, **kwargs): 183 | def by_cookie(): 184 | url = f"{self.protocol}://api.bilibili.com/x/space/myinfo" 185 | headers = {'Host': "api.bilibili.com"} 186 | response = self._requests("get", url, headers=headers) 187 | if response and response.get("code") != -101: 188 | self._log("Cookie仍有效") 189 | return True 190 | else: 191 | self._log("Cookie已失效") 192 | return False 193 | 194 | def by_token(force_refresh=False): 195 | if not force_refresh: 196 | param = f"access_key={self.access_token}&appkey={Bilibili.app_key}&ts={int(time.time())}" 197 | url = f"{self.protocol}://passport.bilibili.com/api/v2/oauth2/info?{param}&sign={self.calc_sign(param)}" 198 | response = self._requests("get", url) 199 | if response and response.get("code") == 0: 200 | self._session.cookies.set('DedeUserID', str(response['data']['mid']), domain=".bilibili.com") 201 | self._log(f"Token仍有效, 有效期至{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time() + int(response['data']['expires_in'])))}") 202 | param = f"access_key={self.access_token}&appkey={Bilibili.app_key}&gourl={self.protocol}%3A%2F%2Faccount.bilibili.com%2Faccount%2Fhome&ts={int(time.time())}" 203 | url = f"{self.protocol}://passport.bilibili.com/api/login/sso?{param}&sign={self.calc_sign(param)}" 204 | self._requests("get", url, decode_level=0) 205 | if all(key in self.get_cookies() for key in ["bili_jct", "DedeUserID", "DedeUserID__ckMd5", "sid", "SESSDATA"]): 206 | self._log("Cookie获取成功") 207 | return True 208 | else: 209 | self._log("Cookie获取失败") 210 | url = f"{self.protocol}://passport.bilibili.com/api/v2/oauth2/refresh_token" 211 | param = f"access_key={self.access_token}&appkey={Bilibili.app_key}&refresh_token={self.refresh_token}&ts={int(time.time())}" 212 | payload = f"{param}&sign={self.calc_sign(param)}" 213 | headers = {'Content-type': "application/x-www-form-urlencoded"} 214 | response = self._requests("post", url, data=payload, headers=headers) 215 | if response and response.get("code") == 0: 216 | for cookie in response['data']['cookie_info']['cookies']: 217 | self._session.cookies.set(cookie['name'], cookie['value'], domain=".bilibili.com") 218 | self.access_token = response['data']['token_info']['access_token'] 219 | self.refresh_token = response['data']['token_info']['refresh_token'] 220 | self._log(f"Token刷新成功, 有效期至{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time() + int(response['data']['token_info']['expires_in'])))}") 221 | return True 222 | else: 223 | self.access_token = "" 224 | self.refresh_token = "" 225 | self._log("Token刷新失败") 226 | return False 227 | 228 | def by_password(): 229 | def get_key(): 230 | url = f"{self.protocol}://passport.bilibili.com/api/oauth2/getKey" 231 | payload = { 232 | 'appkey': Bilibili.app_key, 233 | 'sign': self.calc_sign(f"appkey={Bilibili.app_key}"), 234 | } 235 | while True: 236 | response = self._requests("post", url, data=payload) 237 | if response and response.get("code") == 0: 238 | return { 239 | 'key_hash': response['data']['hash'], 240 | 'pub_key': rsa.PublicKey.load_pkcs1_openssl_pem(response['data']['key'].encode()), 241 | } 242 | else: 243 | time.sleep(1) 244 | 245 | while True: 246 | key = get_key() 247 | key_hash, pub_key = key['key_hash'], key['pub_key'] 248 | url = f"{self.protocol}://passport.bilibili.com/api/v2/oauth2/login" 249 | param = f"appkey={Bilibili.app_key}&password={parse.quote_plus(base64.b64encode(rsa.encrypt(f'{key_hash}{self.password}'.encode(), pub_key)))}&username={parse.quote_plus(self.username)}" 250 | payload = f"{param}&sign={self.calc_sign(param)}" 251 | headers = {'Content-type': "application/x-www-form-urlencoded"} 252 | response = self._requests("post", url, data=payload, headers=headers) 253 | while True: 254 | if response and response.get("code") is not None: 255 | if response['code'] == -105: 256 | url = f"{self.protocol}://passport.bilibili.com/captcha" 257 | headers = {'Host': "passport.bilibili.com"} 258 | response = self._requests("get", url, headers=headers, decode_level=1) 259 | captcha = self._solve_captcha(response) 260 | if captcha: 261 | self._log(f"登录验证码识别结果: {captcha}") 262 | key = get_key() 263 | key_hash, pub_key = key['key_hash'], key['pub_key'] 264 | url = f"{self.protocol}://passport.bilibili.com/api/v2/oauth2/login" 265 | param = f"appkey={Bilibili.app_key}&captcha={captcha}&password={parse.quote_plus(base64.b64encode(rsa.encrypt(f'{key_hash}{self.password}'.encode(), pub_key)))}&username={parse.quote_plus(self.username)}" 266 | payload = f"{param}&sign={self.calc_sign(param)}" 267 | headers = {'Content-type': "application/x-www-form-urlencoded"} 268 | response = self._requests("post", url, data=payload, headers=headers) 269 | else: 270 | self._log(f"登录验证码识别服务暂时不可用, {'尝试更换代理' if self.proxy else '10秒后重试'}") 271 | if not self.set_proxy(): 272 | time.sleep(10) 273 | break 274 | elif response['code'] == -449: 275 | self._log("服务繁忙, 尝试使用V3接口登录") 276 | url = f"{self.protocol}://passport.bilibili.com/api/v3/oauth2/login" 277 | param = f"access_key=&actionKey=appkey&appkey={Bilibili.app_key}&build=6040500&captcha=&challenge=&channel=bili&cookies=&device=phone&mobi_app=android&password={parse.quote_plus(base64.b64encode(rsa.encrypt(f'{key_hash}{self.password}'.encode(), pub_key)))}&permission=ALL&platform=android&seccode=&subid=1&ts={int(time.time())}&username={parse.quote_plus(self.username)}&validate=" 278 | payload = f"{param}&sign={self.calc_sign(param)}" 279 | headers = {'Content-type': "application/x-www-form-urlencoded"} 280 | response = self._requests("post", url, data=payload, headers=headers) 281 | elif response['code'] == 0 and response['data']['status'] == 0: 282 | for cookie in response['data']['cookie_info']['cookies']: 283 | self._session.cookies.set(cookie['name'], cookie['value'], domain=".bilibili.com") 284 | self.access_token = response['data']['token_info']['access_token'] 285 | self.refresh_token = response['data']['token_info']['refresh_token'] 286 | self._log("登录成功") 287 | return True 288 | else: 289 | self._log(f"登录失败 {response}") 290 | return False 291 | else: 292 | self._log(f"当前IP登录过于频繁, {'尝试更换代理' if self.proxy else '1分钟后重试'}") 293 | if not self.set_proxy(): 294 | time.sleep(60) 295 | break 296 | 297 | self._session.cookies.clear() 298 | for name in ["bili_jct", "DedeUserID", "DedeUserID__ckMd5", "sid", "SESSDATA"]: 299 | value = kwargs.get(name) 300 | if value: 301 | self._session.cookies.set(name, value, domain=".bilibili.com") 302 | self.access_token = kwargs.get("access_token", "") 303 | self.refresh_token = kwargs.get("refresh_token", "") 304 | self.username = kwargs.get("username", "") 305 | self.password = kwargs.get("password", "") 306 | force_refresh_token = kwargs.get("force_refresh_token", False) 307 | if (not force_refresh_token or not self.access_token or not self.refresh_token) and all(key in self.get_cookies() for key in ["bili_jct", "DedeUserID", "DedeUserID__ckMd5", "sid", "SESSDATA"]) and by_cookie(): 308 | return True 309 | elif self.access_token and self.refresh_token and by_token(force_refresh_token): 310 | return True 311 | elif self.username and self.password and by_password(): 312 | return True 313 | else: 314 | self._session.cookies.clear() 315 | return False 316 | 317 | # 获取用户信息 318 | def get_user_info(self): 319 | url = f"{self.protocol}://api.bilibili.com/x/space/myinfo?jsonp=jsonp" 320 | headers = { 321 | 'Host': "api.bilibili.com", 322 | 'Referer': f"https://space.bilibili.com/{self.get_uid()}/", 323 | } 324 | response = self._requests("get", url, headers=headers) 325 | if response and response.get("code") == 0: 326 | self.info['ban'] = bool(response['data']['silence']) 327 | self.info['coins'] = response['data']['coins'] 328 | self.info['experience']['current'] = response['data']['level_exp']['current_exp'] 329 | self.info['experience']['next'] = response['data']['level_exp']['next_exp'] 330 | self.info['face'] = response['data']['face'] 331 | self.info['level'] = response['data']['level'] 332 | self.info['nickname'] = response['data']['name'] 333 | self._log(f"{self.info['nickname']}(UID={self.get_uid()}), Lv.{self.info['level']}({self.info['experience']['current']}/{self.info['experience']['next']}), 拥有{self.info['coins']}枚硬币, 账号{'状态正常' if not self.info['ban'] else '被封禁'}") 334 | return True 335 | else: 336 | self._log("用户信息获取失败") 337 | return False 338 | 339 | # 修改隐私设置 340 | def set_privacy(self, show_favourite=None, show_bangumi=None, show_tag=None, show_reward=None, show_info=None, show_game=None): 341 | # show_favourite = 展示[我的收藏夹] 342 | # show_bangumi = 展示[订阅番剧] 343 | # show_tag = 展示[订阅标签] 344 | # show_reward = 展示[最近投币的视频] 345 | # show_info = 展示[个人资料] 346 | # show_game = 展示[最近玩过的游戏] 347 | privacy = { 348 | 'fav_video': show_favourite, 349 | 'bangumi': show_bangumi, 350 | 'tags': show_tag, 351 | 'coins_video': show_reward, 352 | 'user_info': show_info, 353 | 'played_game': show_game, 354 | } 355 | url = f"{self.protocol}://space.bilibili.com/ajax/settings/getSettings?mid={self.get_uid()}" 356 | headers = { 357 | 'Host': "space.bilibili.com", 358 | 'Referer': f"https://space.bilibili.com/{self.get_uid()}/", 359 | } 360 | response = self._requests("get", url, headers=headers) 361 | if response and response.get("status") == True: 362 | for key, value in privacy.items(): 363 | if response['data']['privacy'][key] == value: 364 | privacy[key] = None 365 | else: 366 | self._log(f"隐私设置获取失败 {response}") 367 | return False 368 | url = f"{self.protocol}://space.bilibili.com/ajax/settings/setPrivacy" 369 | headers = { 370 | 'Host': "space.bilibili.com", 371 | 'Origin': "https://space.bilibili.com", 372 | 'Referer': f"https://space.bilibili.com/{self.get_uid()}/", 373 | } 374 | fail = [] 375 | for key, value in privacy.items(): 376 | if value is not None: 377 | payload = { 378 | key: 1 if value else 0, 379 | 'csrf': self.get_csrf(), 380 | } 381 | response = self._requests("post", url, data=payload, headers=headers) 382 | if not response or response.get("status") != True: 383 | fail.append(key) 384 | if not fail: 385 | self._log("隐私设置修改成功") 386 | return True 387 | else: 388 | self._log(f"隐私设置修改失败 {fail}") 389 | return False 390 | 391 | # 银瓜子兑换硬币 392 | def silver_to_coin(self, app=True, pc=False): 393 | # app = APP通道 394 | # pc = PC通道 395 | if app: 396 | param = f"access_key={self.access_token}&appkey={Bilibili.app_key}&ts={int(time.time())}" 397 | url = f"{self.protocol}://api.live.bilibili.com/AppExchange/silver2coin?{param}&sign={self.calc_sign(param)}" 398 | response = self._requests("get", url) 399 | if response and response.get("code") == 0: 400 | self._log("银瓜子兑换硬币(APP通道)成功") 401 | else: 402 | self._log(f"银瓜子兑换硬币(APP通道)失败 {response}") 403 | if pc: 404 | url = f"{self.protocol}://api.live.bilibili.com/pay/v1/Exchange/silver2coin" 405 | payload = { 406 | 'platform': "pc", 407 | 'csrf_token': self.get_csrf(), 408 | } 409 | headers = { 410 | 'Host': "api.live.bilibili.com", 411 | 'Origin': "https://live.bilibili.com", 412 | 'Referer': "https://live.bilibili.com/exchange", 413 | } 414 | response = self._requests("post", url, data=payload, headers=headers) 415 | if response and response.get("code") == 0: 416 | self._log("银瓜子兑换硬币(PC通道)成功") 417 | else: 418 | self._log(f"银瓜子兑换硬币(PC通道)失败 {response}") 419 | 420 | # 观看 421 | @__bvid_handle(1, "aid") 422 | def watch(self, aid): 423 | # aid = 稿件av号 424 | url = f"{self.protocol}://api.bilibili.com/x/web-interface/view?aid={aid}" 425 | response = self._requests("get", url) 426 | if response and response.get("data") is not None: 427 | cid = response['data']['cid'] 428 | duration = response['data']['duration'] 429 | else: 430 | self._log(f"av{aid}信息解析失败") 431 | return False 432 | url = f"{self.protocol}://api.bilibili.com/x/report/click/h5" 433 | payload = { 434 | 'aid': aid, 435 | 'cid': cid, 436 | 'part': 1, 437 | 'did': self.get_sid(), 438 | 'ftime': int(time.time()), 439 | 'jsonp': "jsonp", 440 | 'lv': None, 441 | 'mid': self.get_uid(), 442 | 'csrf': self.get_csrf(), 443 | 'stime': int(time.time()), 444 | } 445 | headers = { 446 | 'Host': "api.bilibili.com", 447 | 'Origin': "https://www.bilibili.com", 448 | 'Referer': f"https://www.bilibili.com/video/av{aid}", 449 | } 450 | response = self._requests("post", url, data=payload, headers=headers) 451 | if response and response.get("code") == 0: 452 | url = f"{self.protocol}://api.bilibili.com/x/report/web/heartbeat" 453 | payload = { 454 | 'aid': aid, 455 | 'cid': cid, 456 | 'jsonp': "jsonp", 457 | 'mid': self.get_uid(), 458 | 'csrf': self.get_csrf(), 459 | 'played_time': 0, 460 | 'pause': False, 461 | 'realtime': duration, 462 | 'dt': 7, 463 | 'play_type': 1, 464 | 'start_ts': int(time.time()), 465 | } 466 | response = self._requests("post", url, data=payload, headers=headers) 467 | if response and response.get("code") == 0: 468 | time.sleep(5) 469 | payload['played_time'] = duration - 1 470 | payload['play_type'] = 0 471 | payload['start_ts'] = int(time.time()) 472 | response = self._requests("post", url, data=payload, headers=headers) 473 | if response and response.get("code") == 0: 474 | self._log(f"av{aid}观看成功") 475 | return True 476 | self._log(f"av{aid}观看失败 {response}") 477 | return False 478 | 479 | # 点赞 480 | @__bvid_handle(1, "aid") 481 | def like(self, aid): 482 | # aid = 稿件av号 483 | url = f"{self.protocol}://api.bilibili.com/x/web-interface/archive/like" 484 | payload = { 485 | 'aid': aid, 486 | 'like': 1, 487 | 'csrf': self.get_csrf(), 488 | } 489 | headers = { 490 | 'Host': "api.bilibili.com", 491 | 'Origin': "https://www.bilibili.com", 492 | 'Referer': f"https://www.bilibili.com/video/av{aid}", 493 | } 494 | response = self._requests("post", url, data=payload, headers=headers) 495 | if response and response.get("code") == 0: 496 | self._log(f"av{aid}点赞成功") 497 | return True 498 | else: 499 | self._log(f"av{aid}点赞失败 {response}") 500 | return False 501 | 502 | # 投币 503 | @__bvid_handle(1, "aid") 504 | def reward(self, aid, double=True): 505 | # aid = 稿件av号 506 | # double = 双倍投币 507 | url = f"{self.protocol}://api.bilibili.com/x/web-interface/coin/add" 508 | payload = { 509 | 'aid': aid, 510 | 'multiply': 2 if double else 1, 511 | 'cross_domain': "true", 512 | 'csrf': self.get_csrf(), 513 | } 514 | headers = { 515 | 'Host': "api.bilibili.com", 516 | 'Origin': "https://www.bilibili.com", 517 | 'Referer': f"https://www.bilibili.com/video/av{aid}", 518 | } 519 | response = self._requests("post", url, data=payload, headers=headers) 520 | if response and response.get("code") == 0: 521 | self._log(f"av{aid}投{2 if double else 1}枚硬币成功") 522 | return True 523 | else: 524 | self._log(f"av{aid}投{2 if double else 1}枚硬币失败 {response}") 525 | return self.reward(aid, False) if double else False 526 | 527 | # 收藏 528 | @__bvid_handle(1, "aid") 529 | def favour(self, aid): 530 | # aid = 稿件av号 531 | url = f"{self.protocol}://api.bilibili.com/x/v2/fav/folder" 532 | headers = {'Host': "api.bilibili.com"} 533 | response = self._requests("get", url, headers=headers) 534 | if response and response.get("data"): 535 | fid = response['data'][0]['fid'] 536 | else: 537 | self._log("fid获取失败") 538 | return False 539 | url = f"{self.protocol}://api.bilibili.com/x/v2/fav/video/add" 540 | payload = { 541 | 'aid': aid, 542 | 'fid': fid, 543 | 'jsonp': "jsonp", 544 | 'csrf': self.get_csrf(), 545 | } 546 | headers = { 547 | 'Host': "api.bilibili.com", 548 | 'Origin': "https://www.bilibili.com", 549 | 'Referer': f"https://www.bilibili.com/video/av{aid}", 550 | } 551 | response = self._requests("post", url, data=payload, headers=headers) 552 | if response and response.get("code") == 0: 553 | self._log(f"av{aid}收藏成功") 554 | return True 555 | else: 556 | self._log(f"av{aid}收藏失败 {response}") 557 | return False 558 | 559 | # 三连推荐 560 | @__bvid_handle(1, "aid") 561 | def combo(self, aid): 562 | # aid = 稿件av号 563 | url = f"{self.protocol}://api.bilibili.com/x/web-interface/archive/like/triple" 564 | payload = { 565 | 'aid': aid, 566 | 'csrf': self.get_csrf(), 567 | } 568 | headers = { 569 | 'Host': "api.bilibili.com", 570 | 'Origin': "https://www.bilibili.com", 571 | 'Referer': f"https://www.bilibili.com/video/av{aid}", 572 | } 573 | response = self._requests("post", url, data=payload, headers=headers) 574 | if response and response.get("code") == 0: 575 | self._log(f"av{aid}三连推荐成功") 576 | return True 577 | else: 578 | self._log(f"av{aid}三连推荐失败 {response}") 579 | return False 580 | 581 | # 分享 582 | @__bvid_handle(1, "aid") 583 | def share(self, aid): 584 | # aid = 稿件av号 585 | url = f"{self.protocol}://api.bilibili.com/x/web-interface/share/add" 586 | payload = { 587 | 'aid': aid, 588 | 'jsonp': "jsonp", 589 | 'csrf': self.get_csrf(), 590 | } 591 | headers = { 592 | 'Host': "api.bilibili.com", 593 | 'Origin': "https://www.bilibili.com", 594 | 'Referer': f"https://www.bilibili.com/video/av{aid}", 595 | } 596 | response = self._requests("post", url, data=payload, headers=headers) 597 | if response and response.get("code") == 0: 598 | self._log(f"av{aid}分享成功") 599 | return True 600 | else: 601 | self._log(f"av{aid}分享失败 {response}") 602 | return False 603 | 604 | # 关注 605 | def follow(self, mid, secret=False): 606 | # mid = 被关注用户UID 607 | # secret = 悄悄关注 608 | url = f"{self.protocol}://api.bilibili.com/x/relation/modify" 609 | payload = { 610 | 'fid': mid, 611 | 'act': 3 if secret else 1, 612 | 're_src': 11, 613 | 'jsonp': "jsonp", 614 | 'csrf': self.get_csrf(), 615 | } 616 | headers = { 617 | 'Host': "api.bilibili.com", 618 | 'Origin': "https://space.bilibili.com", 619 | 'Referer': f"https://space.bilibili.com/{mid}/", 620 | } 621 | response = self._requests("post", url, data=payload, headers=headers) 622 | if response and response.get("code") == 0: 623 | self._log(f"用户{mid}{'悄悄' if secret else ''}关注成功") 624 | return True 625 | else: 626 | self._log(f"用户{mid}{'悄悄' if secret else ''}关注失败 {response}") 627 | return False 628 | 629 | # 批量关注 630 | def follow_batch(self, mids): 631 | # mids = 被关注用户UID 632 | url = f"{self.protocol}://api.bilibili.com/x/relation/batch/modify" 633 | payload = { 634 | 'fids': ",".join(map(str, mids)), 635 | 'act': 1, 636 | 'csrf': self.get_csrf(), 637 | 're_src': 222, 638 | } 639 | headers = { 640 | 'Host': "api.bilibili.com", 641 | 'Referer': "https://www.bilibili.com/blackboard/live/activity-NfUS01P8.html", 642 | } 643 | response = self._requests("post", url, data=payload, headers=headers) 644 | if response and response.get("code") == 0: 645 | self._log(f"用户{', '.join(map(str, mids))}批量关注成功") 646 | return True 647 | else: 648 | self._log(f"用户{', '.join(map(str, mids))}批量关注失败 {response}") 649 | return False 650 | 651 | # 弹幕发送 652 | @__bvid_handle(1, "aid") 653 | def danmaku_post(self, aid, message, page=1, moment=-1): 654 | # aid = 稿件av号 655 | # message = 弹幕内容 656 | # page = 分P 657 | # moment = 弹幕发送时间 658 | url = f"{self.protocol}://api.bilibili.com/x/web-interface/view?aid={aid}" 659 | response = self._requests("get", url) 660 | if response and response.get("data") is not None: 661 | page_info = {page['page']: { 662 | 'cid': page['cid'], 663 | 'duration': page['duration'], 664 | } for page in response['data']['pages']} 665 | if page in page_info: 666 | oid = page_info[page]['cid'] 667 | duration = page_info[page]['duration'] 668 | else: 669 | self._log(f"av{aid}不存在P{page}") 670 | return False 671 | else: 672 | self._log(f"av{aid}信息解析失败") 673 | return False 674 | url = f"{self.protocol}://api.bilibili.com/x/v2/dm/post" 675 | headers = { 676 | 'Host': "api.bilibili.com", 677 | 'Origin': "https://www.bilibili.com", 678 | 'Referer': f"https://www.bilibili.com/video/av{aid}", 679 | } 680 | while True: 681 | payload = { 682 | 'type': 1, 683 | 'oid': oid, 684 | 'msg': message, 685 | 'aid': aid, 686 | 'progress': int(moment * 1E3) if moment != -1 else random.randint(0, duration * 1E3), 687 | 'color': 16777215, 688 | 'fontsize': 25, 689 | 'pool': 0, 690 | 'mode': 1, 691 | 'rnd': int(time.time() * 1E6), 692 | 'plat': 1, 693 | 'csrf': self.get_csrf(), 694 | } 695 | response = self._requests("post", url, data=payload, headers=headers) 696 | if response and response.get("code") is not None: 697 | if response['code'] == 0: 698 | self._log(f"av{aid}(P{page})弹幕\"{message}\"发送成功") 699 | return True 700 | elif response['code'] == 36703: 701 | self._log(f"av{aid}(P{page})弹幕发送频率过快, 10秒后重试") 702 | time.sleep(10) 703 | else: 704 | self._log(f"av{aid}(P{page})弹幕\"{message}\"发送失败 {response}") 705 | return False 706 | 707 | # 评论点赞 708 | def comment_like(self, otype, oid, rpid): 709 | # otype = 作品类型 710 | # oid = 作品ID 711 | # rpid = 评论ID 712 | if Bilibili.patterns.get(otype) is None: 713 | return False 714 | url = f"{self.protocol}://api.bilibili.com/x/v2/reply/action" 715 | payload = { 716 | 'oid': oid, 717 | 'type': Bilibili.patterns[otype]['id'], 718 | 'rpid': rpid, 719 | 'action': 1, 720 | 'jsonp': "jsonp", 721 | 'csrf': self.get_csrf(), 722 | } 723 | headers = { 724 | 'Content-Type': "application/x-www-form-urlencoded; charset=UTF-8", 725 | 'Host': "api.bilibili.com", 726 | 'Origin': "https://www.bilibili.com", 727 | 'Referer': f"{Bilibili.patterns[otype]['prefix']}{oid}", 728 | } 729 | response = self._requests("post", url, data=payload, headers=headers) 730 | if response and response.get("code") == 0: 731 | self._log(f"评论{rpid}点赞成功") 732 | return True 733 | else: 734 | self._log(f"评论{rpid}点赞失败 {response}") 735 | return False 736 | 737 | # 评论发表 738 | def comment_post(self, otype, oid, message): 739 | # otype = 作品类型 740 | # oid = 作品ID 741 | # message = 评论内容 742 | if Bilibili.patterns.get(otype) is None: 743 | return False 744 | url = f"{self.protocol}://api.bilibili.com/x/v2/reply/add" 745 | while True: 746 | payload = { 747 | 'oid': oid, 748 | 'type': Bilibili.patterns[otype]['id'], 749 | 'message': message, 750 | 'plat': 1, 751 | 'jsonp': "jsonp", 752 | 'csrf': self.get_csrf(), 753 | } 754 | headers = { 755 | 'Content-Type': "application/x-www-form-urlencoded; charset=UTF-8", 756 | 'Host': "api.bilibili.com", 757 | 'Origin': "https://www.bilibili.com", 758 | 'Referer': f"{Bilibili.patterns[otype]['prefix']}{oid}", 759 | } 760 | response = self._requests("post", url, data=payload, headers=headers) 761 | if response and response.get("code") is not None: 762 | if response['code'] == 0: 763 | self._log(f"作品{oid}提交评论\"{message}\"成功") 764 | return True 765 | elif response['code'] == 12015: 766 | response = self._requests("get", response['data']['url'], headers=headers, decode_level=1) 767 | captcha = self._solve_captcha(response) 768 | if captcha: 769 | self._log(f"评论验证码识别结果: {captcha}") 770 | payload['code'] = captcha 771 | else: 772 | self._log(f"评论验证码识别服务暂时不可用, 1分钟后重试") 773 | time.sleep(60) 774 | elif response['code'] == 12035: 775 | self._log(f"作品{oid}提交评论\"{message}\"失败, 该账号被UP主列入评论黑名单") 776 | return False 777 | elif response['code'] == -105: 778 | if "code" in payload: 779 | payload.pop("code") 780 | else: 781 | self._log(f"作品{oid}提交评论\"{message}\"失败 {response}") 782 | return False 783 | 784 | # 动态点赞 785 | def dynamic_like(self, did): 786 | # did = 动态ID 787 | url = f"{self.protocol}://api.vc.bilibili.com/dynamic_like/v1/dynamic_like/thumb" 788 | payload = { 789 | 'uid': self.get_uid(), 790 | 'dynamic_id': did, 791 | 'up': 1, 792 | 'csrf_token': self.get_csrf(), 793 | } 794 | headers = { 795 | 'Content-Type': "application/x-www-form-urlencoded", 796 | 'Host': "api.vc.bilibili.com", 797 | 'Origin': "https://space.bilibili.com", 798 | 'Referer': "https://space.bilibili.com/208259/", 799 | } 800 | response = self._requests("post", url, data=payload, headers=headers) 801 | if response and response.get("code") == 0: 802 | self._log(f"动态{did}点赞成功") 803 | return True 804 | else: 805 | self._log(f"动态{did}点赞失败 {response}") 806 | return False 807 | 808 | # 动态转发 809 | def dynamic_repost(self, did, message="转发动态", ats=[]): 810 | # did = 动态ID 811 | # message = 转发内容 812 | # ats = 被@用户UID列表 813 | def uid_to_nickname(mid): 814 | url = f"{self.protocol}://api.bilibili.com/x/web-interface/card?mid={mid}" 815 | response = self._requests("get", url) 816 | if response and response.get("code") == 0: 817 | return response['data']['card']['name'] 818 | else: 819 | return "" 820 | 821 | url = f"{self.protocol}://api.vc.bilibili.com/dynamic_repost/v1/dynamic_repost/repost" 822 | ctrl = [] 823 | for at in zip(ats, [uid_to_nickname(mid) for mid in ats]): 824 | ctrl.append({ 825 | 'data': str(at[0]), 826 | 'location': len(message) + 1, 827 | 'length': len(at[1]) + 1, 828 | 'type': 1, 829 | }) 830 | message = f"{message} @{at[1]}" 831 | payload = { 832 | 'uid': self.get_uid(), 833 | 'dynamic_id': did, 834 | 'content': message, 835 | 'at_uids': ",".join([str(at) for at in ats]), 836 | 'ctrl': json.dumps(ctrl), 837 | 'csrf_token': self.get_csrf(), 838 | } 839 | headers = { 840 | 'Content-Type': "application/x-www-form-urlencoded", 841 | 'Host': "api.vc.bilibili.com", 842 | 'Origin': "https://space.bilibili.com", 843 | 'Referer': "https://space.bilibili.com/208259/", 844 | } 845 | response = self._requests("post", url, data=payload, headers=headers) 846 | if response and response.get("code") == 0: 847 | self._log(f"动态{did}转发成功") 848 | return True 849 | else: 850 | self._log(f"动态{did}转发失败 {response}") 851 | return False 852 | 853 | # 动态清理 854 | def dynamic_purge(self): 855 | def get_lottery_dynamics(): 856 | headers = { 857 | 'Host': "api.vc.bilibili.com", 858 | 'Origin': "https://space.bilibili.com", 859 | 'Referer': f"https://space.bilibili.com/{self.get_uid()}/dynamic", 860 | } 861 | dynamics = [] 862 | offset = 0 863 | while True: 864 | url = f"{self.protocol}://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history?visitor_uid={self.get_uid()}&host_uid={self.get_uid()}&offset_dynamic_id={offset}" 865 | response = self._requests("get", url, headers=headers) 866 | if response and response.get("code") == 0: 867 | if response['data']['has_more']: 868 | dynamics.extend([{ 869 | 'did': card['desc']['dynamic_id'], 870 | 'lottery_did': card['desc']['orig_dy_id'], 871 | } for card in response['data']['cards'] if card['desc']['orig_type'] == 2 or card['desc']['orig_type'] == 1024]) 872 | offset = response['data']['cards'][-1]['desc']['dynamic_id'] 873 | else: 874 | return dynamics 875 | 876 | dynamics = get_lottery_dynamics() 877 | self._log(f"发现{len(dynamics)}条互动抽奖动态") 878 | delete = 0 879 | for dynamic in dynamics: 880 | url = f"{self.protocol}://api.vc.bilibili.com/lottery_svr/v2/lottery_svr/lottery_notice?dynamic_id={dynamic['lottery_did']}" 881 | headers = { 882 | 'Host': "api.vc.bilibili.com", 883 | 'Origin': "https://t.bilibili.com", 884 | 'Referer': "https://t.bilibili.com/lottery/h5/index/", 885 | } 886 | response = self._requests("get", url, headers=headers) 887 | if response and response.get("code") == 0: 888 | expired = response['data']['status'] == 2 or response['data']['status'] == -1 889 | winning = any(self.get_uid() in winners for winners in [response['data'].get("lottery_result", {}).get(f"{level}_prize_result", []) for level in ["first", "second", "third"]]) 890 | if not expired: 891 | self._log(f"动态{dynamic['lottery_did']}尚未开奖({time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(response['data']['lottery_time']))}), 跳过") 892 | else: 893 | if winning: 894 | self._log(f"动态{dynamic['lottery_did']}中奖, 跳过") 895 | else: 896 | url = f"{self.protocol}://api.vc.bilibili.com/dynamic_repost/v1/dynamic_repost/rm_rp_dyn" 897 | payload = { 898 | 'uid': self.get_uid(), 899 | 'dynamic_id': dynamic['did'], 900 | 'csrf_token': self.get_csrf(), 901 | } 902 | headers = { 903 | 'Content-Type': "application/x-www-form-urlencoded", 904 | 'Host': "api.vc.bilibili.com", 905 | 'Origin': "https://space.bilibili.com", 906 | 'Referer': f"https://space.bilibili.com/{self.get_uid()}/dynamic", 907 | } 908 | response = self._requests("post", url, data=payload, headers=headers) 909 | if response and response.get("code") == 0: 910 | delete += 1 911 | self._log(f"动态{dynamic['lottery_did']}未中奖, 清理成功") 912 | else: 913 | self._log(f"动态{dynamic['lottery_did']}未中奖, 清理失败") 914 | time.sleep(1) 915 | self._log(f"清理了{delete}条动态") 916 | 917 | # 系统通知查询 918 | def system_notice(self, time_span=["", ""], keyword=[]): 919 | # time_span = 时间范围 920 | # keyword = 包含关键字 921 | cursor_span = [int(time.mktime(time.strptime(element, "%Y-%m-%d %H:%M:%S")) * 1E9) if element else "" for element in time_span] 922 | headers = { 923 | 'Host': "message.bilibili.com", 924 | 'Referer': "https://message.bilibili.com/", 925 | } 926 | notice_list = [] 927 | cursor = cursor_span[1] 928 | while True: 929 | url = f"{self.protocol}://message.bilibili.com/api/notify/query.sysnotify.list.do?data_type=1{'&cursor=' + str(cursor) if cursor else ''}" 930 | response = self._requests("get", url, headers=headers) 931 | if response and response.get("code") == 0: 932 | for notice in response['data']: 933 | if not cursor_span[0] or notice['cursor'] > cursor_span[0]: 934 | if not keyword or any(keyword in notice['title'] or keyword in notice['content'] for keyword in keyword): 935 | notice_list.append({ 936 | 'time': notice['time_at'], 937 | 'title': notice['title'], 938 | 'content': notice['content'], 939 | }) 940 | else: 941 | break 942 | else: 943 | if len(response['data']) == 20: 944 | cursor = notice['cursor'] 945 | continue 946 | self._log(f"系统通知获取成功, 总计{len(notice_list)}条通知") 947 | for notice in notice_list: 948 | self._log(f"{notice['title']}({notice['time']}): {notice['content']}") 949 | self.__push_to_queue("system_notice", notice_list) 950 | return notice_list 951 | 952 | # 会员购抢购 953 | def mall_rush(self, item_id, thread=1, headless=True, timeout=10): 954 | # item_id = 商品ID 955 | # thread = 线程数 956 | # headless = 隐藏窗口 957 | # timeout = 超时刷新 958 | def executor(thread_id): 959 | def find_and_click(class_name): 960 | try: 961 | element = driver.find_element_by_class_name(class_name) 962 | element.click() 963 | except: 964 | element = None 965 | return element 966 | 967 | options = webdriver.ChromeOptions() 968 | options.add_argument("log-level=3") 969 | if headless: 970 | options.add_argument("headless") 971 | else: 972 | options.add_argument("disable-infobars") 973 | options.add_argument("window-size=374,729") 974 | if platform.system() == "Linux": 975 | options.add_argument("no-sandbox") 976 | options.add_experimental_option("mobileEmulation", {'deviceName': "Nexus 5"}) 977 | if platform.system() == "Windows": 978 | options.binary_location = "chrome-win\\chrome.exe" 979 | driver = webdriver.Chrome(executable_path="chromedriver.exe" if platform.system() == "Windows" else "chromedriver", options=options) 980 | driver.get(f"{self.protocol}://mall.bilibili.com/detail.html?itemsId={item_id}") 981 | for key, value in self.get_cookies().items(): 982 | driver.add_cookie({ 983 | 'name': key, 984 | 'value': value, 985 | 'domain': ".bilibili.com", 986 | }) 987 | self._log(f"(线程{thread_id})商品{item_id}开始监视库存") 988 | url = f"{self.protocol}://mall.bilibili.com/mall-c/items/info?itemsId={item_id}" 989 | while True: 990 | response = self._requests("get", url) 991 | if response and response.get("code") == 0 and response['data']['activityInfoVO']['serverTime'] >= response['data']['activityInfoVO']['startTime'] if response['data']['activityInfoVO'] else True: 992 | break 993 | timestamp = time.time() 994 | in_stock = False 995 | while True: 996 | try: 997 | result = {class_name: find_and_click(class_name) for class_name in ["bottom-buy-button", "button", "dot", "pay-btn", "expire-time-format", "alert-ok", "error-button"]} 998 | if result['bottom-buy-button']: 999 | if "bottom-buy-disable" not in result['bottom-buy-button'].get_attribute("class"): 1000 | if not in_stock: 1001 | self._log(f"(线程{thread_id})商品{item_id}已开放购买") 1002 | in_stock = True 1003 | else: 1004 | if in_stock: 1005 | self._log(f"(线程{thread_id})商品{item_id}暂无法购买, 原因为{result['bottom-buy-button'].text}") 1006 | in_stock = False 1007 | driver.refresh() 1008 | timestamp = time.time() 1009 | if result['pay-btn']: 1010 | timestamp = time.time() 1011 | if result['alert-ok']: 1012 | driver.refresh() 1013 | if result['expire-time-format']: 1014 | self._log(f"(线程{thread_id})商品{item_id}订单提交成功, 请在{result['expire-time-format'].text}内完成支付") 1015 | driver.quit() 1016 | return True 1017 | if time.time() - timestamp > timeout: 1018 | self._log(f"(线程{thread_id})商品{item_id}操作超时, 当前页面为{driver.current_url}") 1019 | driver.get(f"{self.protocol}://mall.bilibili.com/detail.html?itemsId={item_id}") 1020 | timestamp = time.time() 1021 | except: 1022 | pass 1023 | 1024 | threads = [] 1025 | for i in range(thread): 1026 | threads.append(threading.Thread(target=executor, args=(i + 1,))) 1027 | for thread in threads: 1028 | thread.start() 1029 | for thread in threads: 1030 | thread.join() 1031 | 1032 | # 会员购优惠卷领取 1033 | def mall_coupon(self, coupon_id, thread=1): 1034 | # coupon_id = 优惠券ID 1035 | # thread = 线程数 1036 | def get_coupon_info(coupon_id): 1037 | url = f"{self.protocol}://mall.bilibili.com/mall-c/coupon/user_coupon_code_receive_status_list" 1038 | payload = { 1039 | 'couponIds': [str(coupon_id)], 1040 | 'mid': "", 1041 | 'csrf': self.get_csrf(), 1042 | } 1043 | headers = { 1044 | 'Host': "mall.bilibili.com", 1045 | 'Origin': "https://www.bilibili.com", 1046 | } 1047 | response = self._requests("post", url, json=payload, headers=headers) 1048 | if response and response.get("code") == 0: 1049 | return { 1050 | 'end': response['data'][0]['receiveEndTime'], 1051 | 'message': response['data'][0]['couponStatusMsg'], 1052 | 'name': response['data'][0]['couponName'], 1053 | 'total': response['data'][0]['provideNum'], 1054 | 'remain': response['data'][0]['remainNum'], 1055 | 'start': response['data'][0]['receiveStartTime'], 1056 | 'status': response['data'][0]['receiveStatus'], 1057 | } 1058 | 1059 | def get_server_time(target_time=0): 1060 | url = f"{self.protocol}://mall.bilibili.com/mall-c/common/time/remain?v={int(time.time())}&targetTime={target_time}" 1061 | headers = { 1062 | 'Host': "mall.bilibili.com", 1063 | 'Origin': "https://www.bilibili.com", 1064 | } 1065 | response = self._requests("get", url, headers=headers) 1066 | if response and response.get("code") == 0: 1067 | return { 1068 | 'current': response['data']['serverTime'], 1069 | 'remain': response['data']['remainSeconds'], 1070 | } 1071 | 1072 | def executor(thread_id): 1073 | url = f"{self.protocol}://mall.bilibili.com/mall-c/coupon/create_coupon_code?couponId={coupon_id}&deviceId=" 1074 | payload = {'csrf': self.get_csrf()} 1075 | headers = { 1076 | 'Host': "mall.bilibili.com", 1077 | 'Origin': "https://www.bilibili.com", 1078 | } 1079 | nonlocal flag 1080 | while not flag: 1081 | response = self._requests("post", url, json=payload, headers=headers) 1082 | if response and response.get("code") is not None: 1083 | if response['code'] == 83094004: 1084 | self._log(f"(线程{thread_id})会员购优惠卷\"{coupon_info['name']}\"(ID={coupon_id})领取成功") 1085 | elif response['code'] == 83110005: 1086 | self._log(f"(线程{thread_id})会员购优惠卷\"{coupon_info['name']}\"(ID={coupon_id})领取失败, 优惠券领取数量已达到上限") 1087 | elif response['code'] == 83110015: 1088 | self._log(f"(线程{thread_id})会员购优惠卷\"{coupon_info['name']}\"(ID={coupon_id})领取失败, 优惠券库存不足") 1089 | else: 1090 | continue 1091 | else: 1092 | self._log(f"(线程{thread_id})会员购优惠卷\"{coupon_info['name']}\"(ID={coupon_id})领取失败, 当前IP请求过于频繁") 1093 | flag = True 1094 | 1095 | coupon_info = get_coupon_info(coupon_id) 1096 | if coupon_info: 1097 | if coupon_info['message'] == "可领取": 1098 | server_time = get_server_time(coupon_info['start']) 1099 | if server_time: 1100 | delay = max(server_time['remain'] - 3, 0) 1101 | self._log(f"会员购优惠卷\"{coupon_info['name']}\"(ID={coupon_id})可领取时间为{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(coupon_info['start']))}至{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(coupon_info['end']))}, 库存{coupon_info['remain']}张, 将于{delay}秒后开始领取") 1102 | time.sleep(delay) 1103 | else: 1104 | self._log(f"会员购服务器时间获取失败") 1105 | return 1106 | else: 1107 | self._log(f"会员购优惠卷\"{coupon_info['name']}\"(ID={coupon_id}){coupon_info['message']}") 1108 | return 1109 | else: 1110 | self._log(f"会员购优惠卷{coupon_id}信息获取失败") 1111 | return 1112 | flag = False 1113 | threads = [] 1114 | for i in range(thread): 1115 | threads.append(threading.Thread(target=executor, args=(i + 1,))) 1116 | for thread in threads: 1117 | thread.start() 1118 | for thread in threads: 1119 | thread.join() 1120 | 1121 | # 会员购订单列表查询 1122 | def mall_order_list(self, status=0, type=[2]): 1123 | # status = 订单状态 1124 | # type = 订单类型 1125 | def get_order_list(status, type): 1126 | headers = { 1127 | 'Origin': "https://mall.bilibili.com", 1128 | 'Referer': "https://mall.bilibili.com/orderlist.html", 1129 | } 1130 | order_list = [] 1131 | page = 0 1132 | while True: 1133 | url = f"{self.protocol}://show.bilibili.com/api/ticket/ordercenter/list?pageNum={page}&pageSize=20&status={status}&customer=0&platform=h5&v={int(time.time())}" 1134 | response = self._requests("get", url, headers=headers) 1135 | if response and response.get("errno") == 0: 1136 | data = response['data']['list'] 1137 | if data: 1138 | for order in data: 1139 | if not type or order['order_type'] in type: 1140 | order_list.append(order) 1141 | page += 1 1142 | else: 1143 | self._log(f"会员购订单列表获取成功, 总计{len(order_list)}个订单") 1144 | break 1145 | else: 1146 | self._log(f"会员购订单列表获取失败 {response}") 1147 | return order_list 1148 | 1149 | def get_order_detail(order_id): 1150 | url = f"{self.protocol}://mall.bilibili.com/mall-c/order/detail?orderId={order_id}&platform=h5&time={int(time.time())}" 1151 | headers = { 1152 | 'Origin': "https://mall.bilibili.com", 1153 | 'Referer': f"https://mall.bilibili.com/orderdetail.html?orderId={order_id}", 1154 | } 1155 | response = self._requests("get", url, headers=headers) 1156 | if response and response.get("code") == 0 and response['data']['vo']: 1157 | data = response['data']['vo'] 1158 | self._log(f"会员购订单{order_id}详情获取成功, 包含\"{data['skuList'][0]['itemsName']}\"等{len(data['skuList'])}件商品") 1159 | return data 1160 | else: 1161 | self._log(f"会员购订单{order_id}详情获取失败 {response}") 1162 | return {} 1163 | 1164 | def get_order_express(order_id): 1165 | url = f"{self.protocol}://mall.bilibili.com/mall-c/order/express/detail?orderId={order_id}" 1166 | headers = { 1167 | 'Origin': "https://mall.bilibili.com", 1168 | 'Referer': f"https://mall.bilibili.com/orderdetail.html?orderId={order_id}", 1169 | } 1170 | for _ in range(5): 1171 | response = self._requests("get", url, headers=headers) 1172 | if response and response.get("code") == 0 and response['data']['vo']: 1173 | data = response['data']['vo'] 1174 | self._log(f"会员购订单{order_id}物流获取成功, 状态为{data['state_v']}") 1175 | return data 1176 | time.sleep(3) 1177 | self._log(f"会员购订单{order_id}物流获取失败 {response}") 1178 | return {} 1179 | 1180 | order_list = [] 1181 | for order in get_order_list(status, type): 1182 | order_detail = get_order_detail(order['order_id']) 1183 | order_express = get_order_express(order['order_id']) if order_detail and order_detail['orderExpress'] else {} 1184 | order_list.append({ 1185 | 'id': order.get("order_id"), 1186 | 'item': [{ 1187 | 'id': item.get("itemsId"), 1188 | 'name': item.get("itemsName"), 1189 | 'spec': item.get("skuSpec"), 1190 | 'number': item.get("skuNum"), 1191 | 'price': item.get("price"), 1192 | } for item in order_detail.get("skuList", [])], 1193 | 'create': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(order.get("order_ctime"))) if order.get("current_timestamp") else None, 1194 | 'status': { 1195 | 'code': order.get("status"), 1196 | 'name': order.get("status_name"), 1197 | }, 1198 | 'pay': { 1199 | 'id': order_detail['orderBasic'].get("payId") if order_detail.get("orderBasic") else None, 1200 | 'time': order.get("pay_ctime") if order.get("pay_ctime") != "0000-00-00 00:00:00" else None, 1201 | 'channel': order_detail['orderBasic'].get("paymentChannel") if order_detail.get("orderBasic") else None, 1202 | 'total': order.get("show_money") / 100 if order.get("show_money") else None, 1203 | 'origin': order_detail['orderBasic'].get("payTotalMoney") if order_detail.get("orderBasic") else None, 1204 | 'discount': order_detail['orderBasic'].get("discountMoneys") if order_detail.get("orderBasic") else None, 1205 | 'express': order.get("express_fee") / 100 if order.get("express_fee") else None, 1206 | }, 1207 | 'preorder': { 1208 | 'phone': order_detail['extData'].get("notifyPhoneOrigin") if order_detail.get("extData") else None, 1209 | 'front': { 1210 | 'total': order_detail['extData'].get("frontPayMoney") if order_detail.get("extData") else None, 1211 | 'origin': order_detail['extData'].get("frontMoney") if order_detail.get("extData") else None, 1212 | 'discount': order_detail['extData'].get("frontDisMoney") if order_detail.get("extData") else None, 1213 | }, 1214 | 'final': { 1215 | 'total': order_detail['extData'].get("finalPayMoney") if order_detail.get("extData") else None, 1216 | 'origin': order_detail['extData'].get("finalMoney") if order_detail.get("extData") else None, 1217 | 'discount': order_detail['extData'].get("finalDisMoney") if order_detail.get("extData") else None, 1218 | 'start': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(order_detail['extData'].get("finalMoneyStart") / 1E3)) if order_detail.get("extData") and order_detail['extData'].get("finalMoneyStart") else None, 1219 | 'end': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(order_detail['extData'].get("finalMoneyEnd") / 1E3)) if order_detail.get("extData") and order_detail['extData'].get("finalMoneyStart") else None, 1220 | }, 1221 | }, 1222 | 'shipping': { 1223 | 'name': order_detail['orderDeliver'].get("deliverName") if order_detail.get("orderDeliver") else None, 1224 | 'phone': order_detail['orderDeliver'].get("deliverPhone") if order_detail.get("orderDeliver") else None, 1225 | 'address': order_detail['orderDeliver'].get("deliverAddr") if order_detail.get("orderDeliver") else None, 1226 | 'company': order_detail['orderExpress'].get("com_v") if order_detail.get("orderExpress") else None, 1227 | 'number': order_detail['orderExpress'].get("sno") if order_detail.get("orderExpress") else None, 1228 | 'status': order_express.get("state_v"), 1229 | 'detail': order_express.get("detail"), 1230 | }, 1231 | }) 1232 | self.__push_to_queue("mall_order_list", order_list) 1233 | return order_list 1234 | 1235 | # 会员购优惠券列表查询 1236 | def mall_coupon_list(self, status=1): 1237 | # status = 优惠券状态 1238 | status_map = { 1239 | 1: "validList", 1240 | 2: "usedList", 1241 | 3: "invalidList", 1242 | } 1243 | if status not in status_map: 1244 | return [] 1245 | headers = { 1246 | 'Referer': "https://mall.bilibili.com/couponlist.html?noTitleBar=1", 1247 | } 1248 | coupon_list = [] 1249 | page = 1 1250 | while True: 1251 | url = f"{self.protocol}://mall.bilibili.com/mall-c/coupon/list?status={status}&pageIndex={page}&pageSize=20" 1252 | response = self._requests("get", url, headers=headers) 1253 | if response and response.get("code") == 0: 1254 | if response['data'][status_map[status]]: 1255 | for coupon in response['data'][status_map[status]]['list']: 1256 | coupon_list.append({ 1257 | 'name': coupon['couponCodeName'], 1258 | 'description': coupon['couponDesc'], 1259 | 'detail': coupon['couponDetail'], 1260 | 'discount': coupon['couponDiscount'], 1261 | 'status': coupon['status'], 1262 | 'type': coupon['couponCodeType'], 1263 | 'start': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(coupon['useStartTime'] / 1E3)) if coupon['useStartTime'] else None, 1264 | 'end': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(coupon['useEndTime'] / 1E3)) if coupon['useEndTime'] else None, 1265 | 'use': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(coupon['useTime'] / 1E3)) if coupon['useTime'] else None, 1266 | 'expire': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(coupon['expireDate'] / 1E3)) if coupon['expireDate'] else None, 1267 | }) 1268 | if response['data'][status_map[status]]['hasNextPage']: 1269 | page += 1 1270 | continue 1271 | self._log(f"会员购优惠券列表获取成功, 总计{len(coupon_list)}张优惠券") 1272 | for coupon in coupon_list: 1273 | self._log(f"会员购优惠券: {coupon['name']}" + (f", 失效时间为{coupon['expire']}" if coupon['expire'] else f", 使用时间为{coupon['use']}" if coupon['use'] else f", 使用有效期为{coupon['start']}至{coupon['end']}" if coupon['start'] and coupon['end'] else "")) 1274 | break 1275 | else: 1276 | self._log(f"会员购优惠券列表获取失败 {response}") 1277 | break 1278 | self.__push_to_queue("mall_coupon_list", coupon_list) 1279 | return coupon_list 1280 | 1281 | # 会员购奖品列表查询 1282 | def mall_prize_list(self, status=0, type=[1, 2]): 1283 | # status = 奖品状态 1284 | # type = 奖品类型 1285 | headers = { 1286 | 'Referer': "https://mall.bilibili.com/prizecenter.html", 1287 | } 1288 | prize_list = [] 1289 | page = 1 1290 | while True: 1291 | url = f"{self.protocol}://mall.bilibili.com/mall-c/prize/list?pageNum={page}&pageSize=20&type={status}&v={int(time.time())}" 1292 | response = self._requests("get", url, headers=headers) 1293 | if response and response.get("code") == 0: 1294 | for prize in response['data']['pageInfo']['list']: 1295 | if not type or prize['prizeType'] in type: 1296 | prize_list.append({ 1297 | 'name': prize['prizeName'], 1298 | 'source': prize['sourceName'], 1299 | 'status': prize['status'], 1300 | 'type': prize['prizeType'], 1301 | 'expire': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(prize['expireTime'])), 1302 | }) 1303 | if response['data']['pageInfo']['hasNextPage']: 1304 | page += 1 1305 | else: 1306 | self._log(f"会员购奖品列表获取成功, 总计{len(prize_list)}个奖品, {response['data']['waitDeliveryNum']}个奖品待发货") 1307 | for prize in prize_list: 1308 | self._log(f"会员购奖品: {prize['name']}, 来自{prize['source']}, 领取有效期至{prize['expire']}") 1309 | break 1310 | else: 1311 | self._log(f"会员购奖品列表获取失败 {response}") 1312 | break 1313 | self.__push_to_queue("mall_prize_list", prize_list) 1314 | return prize_list 1315 | 1316 | # 直播奖品列表查询 1317 | def live_prize_list(self): 1318 | headers = { 1319 | 'Origin': "https://link.bilibili.com", 1320 | 'Referer': "https://link.bilibili.com/p/center/index", 1321 | } 1322 | prize_list = [] 1323 | page = 1 1324 | while True: 1325 | url = f"{self.protocol}://api.live.bilibili.com/lottery/v1/award/award_list?page={page}&month=" 1326 | response = self._requests("get", url, headers=headers) 1327 | if response and response.get("code") == 0: 1328 | for prize in response['data']['list']: 1329 | prize_list.append({ 1330 | 'name': prize['gift_name'], 1331 | 'number': prize['gift_num'], 1332 | 'source': prize['source'], 1333 | 'status': prize['status'], 1334 | 'type': prize['gift_type'], 1335 | 'create': prize['create_time'], 1336 | 'expire': prize['expire_time'], 1337 | }) 1338 | if page < response['data']['total_page']: 1339 | page += 1 1340 | else: 1341 | self._log(f"直播奖品列表获取成功, 总计{len(prize_list)}个奖品") 1342 | for prize in prize_list: 1343 | self._log(f"直播奖品: {prize['name']} x{prize['number']}, 来自{prize['source']}, 中奖时间为{prize['create']}, 领取有效期至{prize['expire']}") 1344 | break 1345 | else: 1346 | self._log(f"直播奖品列表获取失败 {response}") 1347 | break 1348 | self.__push_to_queue("live_prize_list", prize_list) 1349 | return prize_list 1350 | 1351 | def detect_charset(file, fallback="utf-8"): 1352 | with open(file, "rb") as f: 1353 | detector = chardet.UniversalDetector() 1354 | for line in f.readlines(): 1355 | detector.feed(line) 1356 | if detector.done: 1357 | return detector.result['encoding'] 1358 | return fallback 1359 | 1360 | def download(url, save_as=None): 1361 | print(f"正在下载{url}") 1362 | if save_as is None: 1363 | save_as = url.split("/")[-1] 1364 | with open(save_as, "wb") as f: 1365 | response = requests.get(url, stream=True) 1366 | length = response.headers.get("content-length") 1367 | if length: 1368 | length = int(length) 1369 | receive = 0 1370 | for data in response.iter_content(chunk_size=100 * 1024): 1371 | f.write(data) 1372 | receive += len(data) 1373 | percent = receive / length 1374 | print(f"\r[{'=' * int(50 * percent)}{' ' * (50 - int(50 * percent))}] {percent:.0%}", end="", flush=True) 1375 | print() 1376 | else: 1377 | f.write(response.content) 1378 | return save_as 1379 | 1380 | def decompress(file, remove=True): 1381 | shutil.unpack_archive(file) 1382 | if remove: 1383 | os.remove(file) 1384 | print(f"{file}解压完毕") 1385 | 1386 | def export(queue, config): 1387 | bucket = {} 1388 | log_file = open(config['global']['log'], "a", encoding="utf-8") if config['global']['log'] else None 1389 | try: 1390 | while True: 1391 | packet = queue.get() 1392 | if isinstance(packet, dict) and all(key in packet for key in ['uid', 'manufacturer', 'data']): 1393 | if packet['manufacturer'] == "log": 1394 | if log_file: 1395 | log_file.write(packet['data'] + "\n") 1396 | else: 1397 | if packet['manufacturer'] not in bucket: 1398 | bucket[packet['manufacturer']] = {} 1399 | if packet['uid'] not in bucket[packet['manufacturer']]: 1400 | bucket[packet['manufacturer']][packet['uid']] = [] 1401 | if isinstance(packet['data'], list): 1402 | bucket[packet['manufacturer']][packet['uid']].extend(packet['data']) 1403 | else: 1404 | bucket[packet['manufacturer']][packet['uid']].append(packet['data']) 1405 | elif packet is None: 1406 | for manufacturer, data in bucket.items(): 1407 | if config.get(manufacturer, {}).get("export"): 1408 | with open(config[manufacturer]['export'], "w", encoding="utf-8") as f: 1409 | f.write(json.dumps(data, indent=4, ensure_ascii=False)) 1410 | return 1411 | finally: 1412 | if log_file: 1413 | log_file.close() 1414 | 1415 | def wrapper(arg): 1416 | def delay_wrapper(func, interval, arg_list=[()], shuffle=False): 1417 | if shuffle: 1418 | random.shuffle(arg_list) 1419 | for i in range(len(arg_list)): 1420 | func(*arg_list[i]) 1421 | if i < len(arg_list) - 1: 1422 | time.sleep(interval) 1423 | 1424 | config, account, queue = arg['config'], arg['account'], arg['queue'] 1425 | instance = Bilibili(config['global']['https'], queue) 1426 | if config['proxy']['enable']: 1427 | if isinstance(config['proxy']['pool'], str): 1428 | try: 1429 | with open(config['proxy']['pool'], "r", encoding=detect_charset(config['proxy']['pool'])) as f: 1430 | instance.set_proxy(add=[proxy for proxy in f.read().strip().splitlines() if proxy and proxy[0] != "#"]) 1431 | except: 1432 | pass 1433 | elif isinstance(config['proxy']['pool'], list): 1434 | instance.set_proxy(add=config['proxy']['pool']) 1435 | if instance.login(force_refresh_token=config['user']['force_refresh_token'], **account): 1436 | threads = [] 1437 | if config['get_user_info']['enable']: 1438 | threads.append(threading.Thread(target=instance.get_user_info)) 1439 | if config['set_privacy']['enable']: 1440 | threads.append(threading.Thread(target=instance.set_privacy, args=(config['set_privacy']['show_favourite'], config['set_privacy']['show_bangumi'], config['set_privacy']['show_tag'], config['set_privacy']['show_reward'], config['set_privacy']['show_info'], config['set_privacy']['show_game']))) 1441 | if config['silver_to_coin']['enable']: 1442 | threads.append(threading.Thread(target=instance.silver_to_coin)) 1443 | if config['watch']['enable']: 1444 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.watch, 5, list(zip(config['watch']['aid']))))) 1445 | if config['like']['enable']: 1446 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.like, 5, list(zip(config['like']['aid']))))) 1447 | if config['reward']['enable']: 1448 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.reward, 5, list(zip(config['reward']['aid'], config['reward']['double']))))) 1449 | if config['favour']['enable']: 1450 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.favour, 5, list(zip(config['favour']['aid']))))) 1451 | if config['combo']['enable']: 1452 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.combo, 5, list(zip(config['combo']['aid']))))) 1453 | if config['share']['enable']: 1454 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.share, 5, list(zip(config['share']['aid']))))) 1455 | if config['follow']['enable']: 1456 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.follow, 5, list(zip(config['follow']['mid'], config['follow']['secret']))))) 1457 | if config['follow_batch']['enable']: 1458 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.follow_batch, 5, list((config['follow_batch']['mid'][i:i + 50],) for i in range(0, len(config['follow_batch']['mid']), 50))))) 1459 | if config['danmaku_post']['enable']: 1460 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.danmaku_post, 5, list(zip(config['danmaku_post']['aid'], config['danmaku_post']['message'], config['danmaku_post']['page'], config['danmaku_post']['moment']))))) 1461 | if config['comment_like']['enable']: 1462 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.comment_like, 5, list(zip(config['comment_like']['otype'], config['comment_like']['oid'], config['comment_like']['rpid']))))) 1463 | if config['comment_post']['enable']: 1464 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.comment_post, 5, list(zip(config['comment_post']['otype'], config['comment_post']['oid'], config['comment_post']['message']))))) 1465 | # for comment in zip(config['comment_post']['otype'], config['comment_post']['oid'], config['comment_post']['message']): 1466 | # threads.append(threading.Thread(target=instance.comment_post, args=(comment[0], comment[1], comment[2]))) 1467 | if config['dynamic_like']['enable']: 1468 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.dynamic_like, 5, list(zip(config['dynamic_like']['did']))))) 1469 | if config['dynamic_repost']['enable']: 1470 | threads.append(threading.Thread(target=delay_wrapper, args=(instance.dynamic_repost, 5, list(zip(config['dynamic_repost']['did'], config['dynamic_repost']['message'], config['dynamic_repost']['ats']))))) 1471 | if config['dynamic_purge']['enable']: 1472 | threads.append(threading.Thread(target=instance.dynamic_purge)) 1473 | if config['system_notice']['enable']: 1474 | threads.append(threading.Thread(target=instance.system_notice, args=(config['system_notice']['time_span'], config['system_notice']['keyword']))) 1475 | if config['mall_rush']['enable']: 1476 | for item in zip(config['mall_rush']['item_id'], config['mall_rush']['thread']): 1477 | threads.append(threading.Thread(target=instance.mall_rush, args=(item[0], item[1], config['mall_rush']['headless'], config['mall_rush']['timeout']))) 1478 | if config['mall_coupon']['enable']: 1479 | for coupon in zip(config['mall_coupon']['coupon_id'], config['mall_coupon']['thread']): 1480 | threads.append(threading.Thread(target=instance.mall_coupon, args=(coupon[0], coupon[1]))) 1481 | if config['mall_order_list']['enable']: 1482 | threads.append(threading.Thread(target=instance.mall_order_list, args=(config['mall_order_list']['status'], config['mall_order_list']['type']))) 1483 | if config['mall_coupon_list']['enable']: 1484 | threads.append(threading.Thread(target=instance.mall_coupon_list, args=(config['mall_coupon_list']['status'],))) 1485 | if config['mall_prize_list']['enable']: 1486 | threads.append(threading.Thread(target=instance.mall_prize_list, args=(config['mall_prize_list']['status'], config['mall_prize_list']['type']))) 1487 | if config['live_prize_list']['enable']: 1488 | threads.append(threading.Thread(target=instance.live_prize_list)) 1489 | # instance._log("任务开始执行") 1490 | for thread in threads: 1491 | thread.start() 1492 | for thread in threads: 1493 | thread.join() 1494 | # instance._log("任务执行完毕") 1495 | return { 1496 | 'username': instance.username, 1497 | 'password': instance.password, 1498 | 'access_token': instance.access_token, 1499 | 'refresh_token': instance.refresh_token, 1500 | 'cookie': instance.get_cookies(), 1501 | } 1502 | 1503 | def main(): 1504 | print(f"{banner}\n{__doc__}\n版本: {__version__}\n") 1505 | config_file = sys.argv[1] if len(sys.argv) > 1 else "config.toml" 1506 | try: 1507 | with open(config_file, "r", encoding=detect_charset(config_file)) as f: 1508 | config = toml.load(f) 1509 | except: 1510 | print(f"无法加载{config_file}") 1511 | return 1512 | accounts = [] 1513 | for line in config['user']['account'].splitlines(): 1514 | try: 1515 | if line[0] == "#": 1516 | continue 1517 | pairs = {} 1518 | for pair in line.strip(";").split(";"): 1519 | if len(pair.split("=")) == 2: 1520 | key, value = pair.split("=") 1521 | pairs[key] = value 1522 | password = all(key in pairs for key in ["username", "password"]) 1523 | token = all(key in pairs for key in ["access_token", "refresh_token"]) 1524 | cookie = all(key in pairs for key in ["bili_jct", "DedeUserID", "DedeUserID__ckMd5", "sid", "SESSDATA"]) 1525 | if password or token or cookie: 1526 | accounts.append(pairs) 1527 | except: 1528 | pass 1529 | config['user'].pop("account") 1530 | print(f"导入了{len(accounts)}个用户") 1531 | if not accounts: 1532 | return 1533 | if config['mall_rush']['enable']: 1534 | if platform.system() == "Linux" and os.path.exists("/etc/debian_version"): 1535 | prefix = "sudo " if shutil.which("sudo") else "" 1536 | if shutil.which("chromium-browser") is None: 1537 | os.system(f"{prefix}apt -y install chromium-browser") 1538 | if shutil.which("chromedriver") is None: 1539 | os.system(f"{prefix}apt -y install chromium-chromedriver") 1540 | os.system(f"{prefix}ln -s /usr/lib/chromium-browser/chromedriver /usr/bin") 1541 | elif platform.system() == "Linux" and os.path.exists("/etc/redhat-release"): 1542 | prefix = "sudo " if shutil.which("sudo") else "" 1543 | if shutil.which("chromium-browser") is None: 1544 | os.system(f"{prefix}yum -y install chromium") 1545 | if shutil.which("chromedriver") is None: 1546 | os.system(f"{prefix}yum -y install chromedriver") 1547 | elif platform.system() == "Windows": 1548 | if not os.path.exists("chrome-win\\chrome.exe"): 1549 | decompress(download("https://npm.taobao.org/mirrors/chromium-browser-snapshots/Win/706915/chrome-win.zip")) 1550 | if not os.path.exists("chromedriver.exe"): 1551 | decompress(download("https://npm.taobao.org/mirrors/chromedriver/79.0.3945.36/chromedriver_win32.zip")) 1552 | else: 1553 | print("会员购抢购组件不支持在当前平台上运行") 1554 | config['mall_rush']['enable'] = False 1555 | queue = Manager().Queue() 1556 | export_process = Process(target=export, args=(queue, config)) 1557 | export_process.start() 1558 | with Pool(min(config['global']['process'], len(accounts))) as p: 1559 | result = p.map(wrapper, [{ 1560 | 'config': config, 1561 | 'account': account, 1562 | 'queue': queue, 1563 | } for account in accounts]) 1564 | p.close() 1565 | p.join() 1566 | if config['user']['update']: 1567 | with open(config_file, "r+", encoding=detect_charset(config_file)) as f: 1568 | content = f.read() 1569 | before = content.split("account")[0] 1570 | after = content.split("account")[-1].split("\"\"\"")[-1] 1571 | f.seek(0) 1572 | f.truncate() 1573 | f.write(before) 1574 | f.write("account = \"\"\"\n") 1575 | for credential in result: 1576 | new_line = False 1577 | for key, value in credential.items(): 1578 | if value: 1579 | if key == "cookie": 1580 | f.write(f"{';'.join(f'{key}={value}' for key, value in value.items())};") 1581 | else: 1582 | f.write(f"{key}={value};") 1583 | new_line = True 1584 | if new_line: 1585 | f.write("\n") 1586 | f.write("\"\"\"") 1587 | f.write(after) 1588 | print("凭据已更新") 1589 | queue.put(None) 1590 | export_process.join() 1591 | 1592 | if __name__ == "__main__": 1593 | freeze_support() 1594 | main() 1595 | if platform.system() == "Windows": 1596 | os.system("pause >nul | set /p =请按任意键退出") 1597 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.8-alpine3.12 2 | 3 | # 安装依赖 4 | RUN pip3 install --no-cache-dir requests rsa chardet 5 | 6 | # 添加文件 7 | RUN mkdir /data 8 | COPY ["bilibili.py", "manga_sign.py", "/data/"] 9 | 10 | ENTRYPOINT ["python3", "/data/manga_sign.py"] 11 | -------------------------------------------------------------------------------- /manga_sign.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import bilibili 3 | import re 4 | import time 5 | import os 6 | 7 | msg = "" 8 | 9 | serverJ = os.environ['push_key'] 10 | # 尝试登陆 11 | b = bilibili.Bilibili() 12 | b.login(username=os.environ['account'], password=os.environ['password']) 13 | 14 | # 获取 Cookie 15 | cookie_str = "" 16 | cookies = b.get_cookies() 17 | for cookie in cookies: 18 | cookie_str += cookie + "=" + cookies[cookie] + "; " 19 | 20 | headers_with_cookie={ 21 | 'User-Agent': "Mozilla/5.0 BiliDroid/6.4.0 (bbcallen@gmail.com) os/android model/M1903F11I mobi_app/android build/6040500 channel/bili innerVer/6040500 osVer/9.0.0 network/2", 22 | 'Cookie': cookie_str 23 | } 24 | 25 | print("哔哩哔哩漫画开始签到 start>>>") 26 | msg = msg + "哔哩哔哩漫画开始签到: \n" 27 | 28 | r = requests.post("https://manga.bilibili.com/twirp/activity.v1.Activity/ClockIn", verify=False, headers=headers_with_cookie, data={ 29 | "platform": "android" 30 | }) 31 | 32 | # print("响应: " + r.text) 33 | if r.json()['code'] == 0: 34 | print("签到成功.") 35 | msg = msg + "签到成功🐶\n" 36 | if r.json()['code'] == "invalid_argument": 37 | print("今日已签到.") 38 | msg = msg + "今日已签到⚠\n" 39 | 40 | time.sleep(2) 41 | 42 | print("哔哩哔哩漫画获取签到信息 start>>>") 43 | msg = msg + "哔哩哔哩漫画获取签到信息: \n" 44 | r = requests.post("https://manga.bilibili.com/twirp/activity.v1.Activity/GetClockInInfo", verify=False, headers=headers_with_cookie) 45 | 46 | print("累计签到" + str(r.json()['data']['day_count']) + "天🐶") 47 | msg = msg + "累计签到" + str(r.json()['data']['day_count']) + "天🐶\n" 48 | 49 | time.sleep(3) 50 | 51 | print("哔哩哔哩银瓜子兑换硬币 start>>>") 52 | print(b.silver_to_coin()) 53 | 54 | # Server酱 55 | if serverJ != "": 56 | api = "https://sc.ftqq.com/"+ serverJ + ".send" 57 | title = u"哔哩哔哩漫画签到" 58 | content = msg 59 | data = { 60 | "text":title, 61 | "desp":content 62 | } 63 | req = requests.post(api,data = data) 64 | --------------------------------------------------------------------------------