├── .gitignore ├── .gitmodules ├── readme.md ├── index.py ├── config.py ├── tools.py └── bilibili.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "push"] 2 | path = push 3 | url = https://github.com/arcturus-script/push.git 4 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## BiliBili(云函数版) 2 | 3 | ### 实现功能 4 | 5 | - [x] 获取用户信息 6 | - [x] 直播签到 7 | - [x] 漫画签到 8 | - [x] 投币 9 | - [x] 分享视频 10 | - [x] 每日看视频 11 | - [x] 多账户支持 12 | - [x] 自动兑换银瓜子 13 | - [ ] 大会员积分签到(等我有大会员了再搞(╹ڡ╹ )) 14 | 15 | ### 步骤 16 | 17 | 注意把子模块也一起下载 18 | 19 | ```bash 20 | git clone --recursive https://github.com/arcturus-script/bilibili.git 21 | ``` 22 | 23 | 直接配置 config.py 即可, 入口改为 index.main 24 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | from bilibili import BiliBili 2 | from config import config 3 | from push import PushSender, parse 4 | 5 | 6 | def parse_message(message, push_type): 7 | if push_type == "pushplus": 8 | return parse(message, template="html") 9 | else: 10 | return parse(message, template="markdown") 11 | 12 | 13 | def pushMessage(message, config): 14 | if isinstance(config, list): 15 | for item in config: 16 | t = item.get("type") 17 | 18 | p = PushSender(t, item.get("key")) 19 | 20 | p.send(parse_message(message, t), title="Bilibili") 21 | else: 22 | t = config.get("type") 23 | 24 | p = PushSender(config.get("type"), config.get("key")) 25 | 26 | p.send(parse_message(message, t), title="Bilibili") 27 | 28 | 29 | def main(*args): 30 | accounts = config.get("multi") 31 | push_together = config.get("push") 32 | 33 | messages = [] 34 | 35 | for item in accounts: 36 | obj = BiliBili(**item) 37 | 38 | res = obj.start() 39 | 40 | push = item.get("push") 41 | 42 | if push is None: 43 | if push_together is not None: 44 | messages.extend(res) 45 | else: 46 | pushMessage(res, push) 47 | 48 | if len(messages) != 0 and push_together is not None: 49 | pushMessage(messages, push_together) 50 | 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | config = { 2 | "multi": [ 3 | { 4 | "cookie": "xxx", 5 | "options": { 6 | "watch": True, # 每日观看视频 7 | "coins": 1, # 投币个数 8 | "share": True, # 视频分享 9 | "comics": True, # 漫画签到 10 | "lb": True, # 直播签到 11 | "threshold": 100, # 仅剩多少币时不再投币(不写默认100) 12 | "toCoin": False, # 银瓜子兑换硬币 13 | }, 14 | # "push": { 15 | # "type": "pushplus", 16 | # "key": "xxx", 17 | # }, 18 | }, 19 | { 20 | "cookie": "xxx", 21 | "options": { 22 | "watch": True, # 每日观看视频 23 | "coins": 2, # 投币个数 24 | "share": True, # 视频分享 25 | "comics": True, # 漫画签到 26 | "lb": True, # 直播签到 27 | "toCoin": False, # 银瓜子兑换硬币 28 | }, 29 | # "push": [ 30 | # # 以数组的形式填写, 则会向多个服务推送消息 31 | # { 32 | # "type": "pushplus", 33 | # "key": "xxx", 34 | # }, 35 | # { 36 | # "type": "workWechat", 37 | # "key": { 38 | # "agentid": 1000002, 39 | # "corpSecret": "xxx", 40 | # "corpid": "xxx", 41 | # }, 42 | # }, 43 | # ], 44 | }, 45 | ], 46 | "push": { 47 | # 只作用于在multi并未配置 push 的组 48 | "type": "pushplus", 49 | "key": "xxx", 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /tools.py: -------------------------------------------------------------------------------- 1 | def handler(fn): 2 | def inner(*args, **kwargs): 3 | res = fn(*args, **kwargs) 4 | 5 | content = [ 6 | { 7 | "h4": { 8 | "content": res["name"], 9 | }, 10 | }, 11 | { 12 | "txt": { 13 | "content": f"等级: {res['level']}", 14 | }, 15 | }, 16 | { 17 | "txt": { 18 | "content": f"硬币: {res['coin']}", 19 | }, 20 | }, 21 | { 22 | "txt": { 23 | "content": f"经验: {res['exp']}", 24 | }, 25 | }, 26 | ] 27 | 28 | watch = res.get("watch") 29 | 30 | if watch is not None: 31 | content.append( 32 | { 33 | "txt": { 34 | "content": watch, 35 | } 36 | } 37 | ) 38 | 39 | share = res.get("share") 40 | 41 | if share is not None: 42 | content.append( 43 | { 44 | "txt": { 45 | "content": f"分享视频: {share}", 46 | } 47 | } 48 | ) 49 | 50 | coins = res.get("coins") 51 | 52 | if coins is not None: 53 | content.append( 54 | { 55 | "h5": { 56 | "content": "投币", 57 | }, 58 | "orderedList": { 59 | "content": coins, 60 | }, 61 | } 62 | ) 63 | 64 | comics = res.get("comics") 65 | 66 | if comics is not None: 67 | content.extend( 68 | [ 69 | { 70 | "h5": { 71 | "content": "漫画签到", 72 | }, 73 | "txt": { 74 | "content": f"连续签到 {comics} 天", 75 | }, 76 | }, 77 | ] 78 | ) 79 | 80 | lb = res.get("lb") 81 | 82 | if lb is not None: 83 | content.extend( 84 | [ 85 | { 86 | "h5": { 87 | "content": "直播", 88 | }, 89 | "txt": { 90 | "content": lb["raward"], 91 | }, 92 | }, 93 | ] 94 | ) 95 | 96 | toCoin = res.get("toCoin") 97 | 98 | if toCoin is not None: 99 | content.append( 100 | { 101 | "h5": { 102 | "content": "银瓜子兑换硬币", 103 | }, 104 | "txt": { 105 | "content": toCoin, 106 | }, 107 | } 108 | ) 109 | 110 | return content 111 | 112 | return inner 113 | 114 | 115 | def failed(*args, **kwargs): 116 | print("[\033[31mfailed\033[0m] ", end="") 117 | print(*args, **kwargs) 118 | 119 | 120 | def success(*args, **kwargs): 121 | print("[\033[32msuccess\033[0m] ", end="") 122 | print(*args, **kwargs) 123 | 124 | 125 | def info(*args, **kwargs): 126 | print("[\033[34minfo\033[0m] ", end="") 127 | print(*args, **kwargs) -------------------------------------------------------------------------------- /bilibili.py: -------------------------------------------------------------------------------- 1 | import requests as req 2 | from re import compile 3 | from tools import failed, handler, info, success 4 | from datetime import datetime 5 | import time 6 | 7 | # 获取视频信息地址 8 | VIDEO_INFO = "https://api.bilibili.com/x/web-interface/view" 9 | 10 | # 获取用户信息 11 | PERSONAL_INFO = "https://api.bilibili.com/x/space/myinfo" 12 | 13 | # 直播签到 14 | LIVE_BROADCAST = "https://api.live.bilibili.com/sign/doSign" 15 | 16 | # 漫画签到 17 | COMICS = "https://manga.bilibili.com/twirp/activity.v1.Activity/ClockIn" 18 | 19 | # 漫画签到信息 20 | COMICS_INFO = "https://manga.bilibili.com/twirp/activity.v1.Activity/GetClockInInfo" 21 | 22 | # 获取热门推荐 23 | RECOMMAND = "https://api.bilibili.com/x/web-interface/popular" 24 | 25 | # 客户端分享视频 26 | VIDEO_SHARE = "https://api.bilibili.com/x/web-interface/share/add" 27 | 28 | # 投币 29 | COIN = "https://api.bilibili.com/x/web-interface/coin/add" 30 | 31 | # 看视频 32 | VIDEO_CLICK = "https://api.bilibili.com/x/click-interface/click/web/h5" 33 | 34 | VIDEO_HEARTBEAT = "https://api.bilibili.com/x/click-interface/web/heartbeat" 35 | 36 | # 兑换硬币 37 | TO_COIN = "https://api.live.bilibili.com/xlive/revenue/v1/wallet/silver2coin" 38 | 39 | # 获取当日投币情况 40 | COIN_LOG = " https://api.bilibili.com/x/member/web/coin/log" 41 | 42 | headers = { 43 | "user-agent": "Mozilla/5.0", 44 | "Content-Type": "application/x-www-form-urlencoded", 45 | "Referer": "https://www.bilibili.com/", 46 | } 47 | 48 | class BiliBili: 49 | headers = headers.copy() 50 | 51 | def __init__(self, **config) -> None: 52 | self.cookie = config.get("cookie") 53 | self.options = config.get("options", {}) 54 | 55 | self.sid = BiliBili.extract("sid", self.cookie) 56 | self.csrf = BiliBili.extract("bili_jct", self.cookie) 57 | self.uid = BiliBili.extract("DedeUserID", self.cookie) 58 | self.headers.update({"Cookie": self.cookie}) 59 | 60 | @staticmethod 61 | def extract(key: str, cookie: str): 62 | """根据键从 cookie 中抽取数据 63 | 64 | Args: 65 | key: 需要抽取数据的键, 可能值 bili_jct, sid, DedeUserID 66 | cookie (str): BiliBili 的 cookie 67 | """ 68 | regEx = compile(f"(?<={key}=).+?(?=;)|(?<={key}=).+") 69 | csrf = regEx.findall(cookie) 70 | if len(csrf) != 0: 71 | return csrf[0] 72 | else: 73 | return "" 74 | 75 | # 获取视频信息 76 | @staticmethod 77 | def get_video_info(bv): 78 | try: 79 | rep = req.get( 80 | VIDEO_INFO, 81 | params={"bvid": bv}, 82 | headers=BiliBili.headers, 83 | ).json() 84 | 85 | if rep["code"] == 0: 86 | data = rep["data"] 87 | 88 | return { 89 | "bvid": data["bvid"], # 视频 BV 号 90 | "aid": data["aid"], # 视频 AV 号 91 | "duration": data["duration"], 92 | "cid": data["cid"], 93 | "title": data["title"], # 视频标题 94 | } 95 | else: 96 | failed(f"获取视频信息失败, 原因: {rep['message']}") 97 | except Exception as ex: 98 | failed(f"获取视频信息时出错, 原因: {ex}") 99 | 100 | # 获取用户信息 101 | def get_user_info(self): 102 | try: 103 | rep = req.get(PERSONAL_INFO, headers=self.headers).json() 104 | 105 | if rep["code"] == 0: 106 | data = rep["data"] 107 | 108 | current_exp = data["level_exp"]["current_exp"] 109 | next_exp = data["level_exp"]["next_exp"] 110 | 111 | self.name = data["name"] # 用户名 112 | self.level = data["level"] # 等级 113 | self.coin = data["coins"] # 硬币数 114 | self.exp = f"{current_exp}/{next_exp}" # 经验 115 | self.silence = data["silence"] # 不知道是什么 116 | 117 | success(f"获取用户信息成功, 用户: {self.name}") 118 | else: 119 | raise Exception(rep["message"]) 120 | except Exception as ex: 121 | failed(f"获取用户信息时出错, 原因: {ex}") 122 | 123 | self.name = "Unkown" 124 | self.level = "lv0" 125 | self.coin = 0 126 | self.exp = "0/0" 127 | self.silence = "Unkown" 128 | 129 | # 直播签到 130 | def live_broadcast_checkin(self): 131 | if not self.options.get("lb", False): 132 | return 133 | 134 | try: 135 | rep = req.get(LIVE_BROADCAST, headers=self.headers).json() 136 | 137 | if rep["code"] == 0: 138 | # 签到成功 139 | data = rep["data"] 140 | 141 | success(f"直播签到: 奖励 {data['text']}") 142 | 143 | return { 144 | "raward": data["text"], 145 | "specialText": data["specialText"], 146 | } 147 | else: 148 | raise Exception(rep["message"]) 149 | 150 | except Exception as ex: 151 | failed(f"直播签到失败, {ex}") 152 | 153 | # 漫画签到 154 | def comics_checkin(self): 155 | if not self.options.get("comics", False): 156 | return 157 | 158 | try: 159 | rep = req.post( 160 | COMICS, 161 | headers=self.headers, 162 | data={ 163 | "platform": "android", 164 | }, 165 | ).json() 166 | 167 | if rep["code"] == 0: 168 | success("漫画签到完成") 169 | 170 | result = self.comics_checkin_info() 171 | 172 | if result is not None: 173 | return result 174 | else: 175 | return "unkown" 176 | else: 177 | raise Exception(rep.get("msg", "Unknown error")) 178 | except Exception as ex: 179 | failed(f"漫画签到失败, {ex}") 180 | 181 | def comics_checkin_info(self): 182 | rep = req.post(COMICS_INFO, headers=self.headers).json() 183 | 184 | if rep["code"] == 0: 185 | success(f"获取漫画签到信息成功, 您已经连续签到 {rep['data']['day_count']} 天") 186 | 187 | return rep["data"]["day_count"] 188 | else: 189 | failed(f"获取漫画签到信息失败, 原因: {rep['msg']}") 190 | 191 | # 获取推荐视频 192 | @staticmethod 193 | def video_suggest(ps: int = 50, pn: int = 1) -> list or None: 194 | """ 195 | Args: 196 | ps (int): 视频个数 197 | pn (int): 第几页数据 198 | 199 | Returns: 200 | video_list: 一个列表, 例如 201 | [ 202 | {"aid": 551162867, "title": "2022我的世界拜年纪", "bvid": xxx}, 203 | {"aid": 508722277, "title": "B站UP主, 办了个电影节", "bvid": yyy}, 204 | ... 205 | ] 206 | """ 207 | 208 | rep = req.get(RECOMMAND, params={"ps": ps, "pn": pn},headers=headers).json() 209 | 210 | if rep["code"] == 0: 211 | res = [] 212 | 213 | videos = rep["data"]["list"] 214 | 215 | for video in videos: 216 | # 将视频主要信息保存到字典里 217 | res.append( 218 | { 219 | "aid": video["aid"], 220 | "bvid": video["bvid"], 221 | "title": video["title"], 222 | } 223 | ) 224 | 225 | return res 226 | else: 227 | failed(f"获取视频推荐列表失败, 原因: {rep['message']}") 228 | 229 | return [{"bvid": "BV1LS4y1C7Pa"}] 230 | 231 | # 投币 232 | def give_coin(self, videos, per_coin_num=1, select_like=0): 233 | coined = self.getCoinLog() # 已经投币数 234 | 235 | max_coin = self.options.get("coins", 0) 236 | 237 | if max_coin == 0: 238 | return 239 | 240 | surplus = max_coin - coined 241 | surplus = 0 if surplus < 0 else surplus 242 | 243 | info(f"还需投币 {surplus} 个") 244 | 245 | coin_videos = [] 246 | 247 | for video in videos: 248 | # 当已投币数超过想投币数时退出 249 | if coined < max_coin: 250 | data = { 251 | "aid": str(video["aid"]), 252 | "multiply": per_coin_num, # 每次投币多少个, 默认 1 个 253 | "select_like": select_like, # 是否同时点赞, 默认不点赞 254 | "cross_domain": "true", 255 | "csrf": self.csrf, 256 | } 257 | 258 | rep = req.post(COIN, headers=self.headers, data=data).json() 259 | 260 | if rep["code"] == 0: 261 | # 投币成功 262 | success(f"给[{video['title']}]投币成功") 263 | 264 | coin_videos.append(video["title"]) 265 | 266 | coined += 1 # 投币次数加 1 267 | else: 268 | # 投币失败 269 | failed(f"给[{video['title']}]投币失败, 原因: {rep['message']}") 270 | else: 271 | success(f"投币完成, 今日共投了 {coined} 个硬币") 272 | 273 | break 274 | 275 | return coin_videos 276 | 277 | # 分享视频 278 | def share_video(self, videos): 279 | if not self.options.get("share", False): 280 | return 281 | 282 | for video in videos: 283 | # 分享视频 284 | data = { 285 | "aid": video["aid"], 286 | "csrf": self.csrf, 287 | } 288 | 289 | rep = req.post(VIDEO_SHARE, data=data, headers=self.headers).json() 290 | 291 | if rep["code"] == 0: 292 | # 如果分享成功, 退出循环, 并返回分享的视频名 293 | success(f"分享视频完成, [{video['title']}]") 294 | 295 | return video["title"] 296 | else: 297 | failed(f"分享视频[{video['title']}]失败, {rep['message']}") 298 | 299 | # 每日看视频 300 | def watch(self, bvid): 301 | if not self.options.get("watch", False): 302 | return 303 | 304 | video_info = BiliBili.get_video_info(bvid) 305 | 306 | # 获取视频信息成功 307 | if video_info: 308 | data = { 309 | "aid": video_info["aid"], 310 | "cid": video_info["cid"], 311 | "part": 1, 312 | "ftime": int(time.time()), 313 | "jsonp": "jsonp", 314 | "mid": self.uid, 315 | "csrf": self.csrf, 316 | "stime": int(time.time()), 317 | } 318 | 319 | rep = req.post(VIDEO_CLICK, data=data, headers=self.headers).json() 320 | 321 | # 进入视频页 322 | if rep["code"] == 0: 323 | data = { 324 | "aid": video_info["aid"], 325 | "cid": video_info["cid"], 326 | "jsonp": "jsonp", 327 | "mid": self.uid, 328 | "csrf": self.csrf, 329 | "played_time": 0, 330 | "pause": False, 331 | "play_type": 1, 332 | "realtime": video_info["duration"], 333 | "start_ts": int(time.time()), 334 | } 335 | 336 | rep = req.post(VIDEO_HEARTBEAT, data=data, headers=self.headers).json() 337 | 338 | if rep["code"] == 0: 339 | # 模拟观看视频 340 | time.sleep(5) 341 | 342 | data["played_time"] = video_info["duration"] - 1 343 | data["play_type"] = 0 344 | data["start_ts"] = int(time.time()) 345 | 346 | rep = req.post( 347 | VIDEO_HEARTBEAT, 348 | data=data, 349 | headers=self.headers, 350 | ).json() 351 | 352 | if rep["code"] == 0: 353 | success(f"观看视频完成, [{video_info['title']}]") 354 | 355 | return f"观看视频[{video_info['title']}]成功" 356 | 357 | failed(f"观看视频失败, [{video_info['title']}]") 358 | 359 | # 银瓜子兑换银币 360 | def toCoin(self): 361 | if not self.options.get("toCoin", False): 362 | return 363 | 364 | resp = req.post( 365 | TO_COIN, 366 | headers=self.headers, 367 | data={ 368 | "csrf_token": self.csrf, 369 | "csrf": self.csrf, 370 | }, 371 | ).json() 372 | 373 | return resp.get("message", "兑换失败") 374 | 375 | def getCoinLog(self): 376 | resp = req.get( 377 | COIN_LOG, 378 | headers=self.headers, 379 | params={ 380 | "csrf": self.csrf, 381 | "jsonp": "jsonp", 382 | }, 383 | ).json() 384 | 385 | res = 0 386 | 387 | if resp.get("code") == 0: 388 | coin_log = resp.get("data").get("list") 389 | 390 | today = datetime.today().date() 391 | for i in coin_log: 392 | t = datetime.strptime(i["time"], "%Y-%m-%d %H:%M:%S") 393 | 394 | if t.date() == today: 395 | if i["delta"] < 0: 396 | res += -i["delta"] 397 | 398 | success(f"获取硬币投递情况成功, 当前已投币 {res} 个") 399 | else: 400 | failed("获取投币情况失败") 401 | 402 | return res 403 | 404 | @handler 405 | def start(self): 406 | self.get_user_info() # 获取用户信息 407 | 408 | videos = self.video_suggest() # 获取热门视频 409 | 410 | return { 411 | "name": self.name, 412 | "level": self.level, 413 | "coin": self.coin, 414 | "exp": self.exp, 415 | "coins": self.give_coin(videos), # 投币 416 | "share": self.share_video(videos), # 视频分享 417 | "comics": self.comics_checkin(), # 漫画签到 418 | "lb": self.live_broadcast_checkin(), # 直播签到 419 | "watch": self.watch(videos[0]["bvid"]), # 观看视频 420 | "toCoin": self.toCoin(), # 银瓜子兑换硬币, 421 | } 422 | --------------------------------------------------------------------------------