├── .gitignore ├── README.md ├── api.py ├── main.py └── mvc ├── grab_sofa_control.py ├── grab_sofa_model.py └── grab_sofa_view.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .DS_Store 3 | *.cookies 4 | *.config 5 | *.png 6 | *.log 7 | *.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # play with bilibili 2 | ## 简介 3 | * 模拟登陆bilibili弹幕网进行各类操作 4 | * 哔哩哔哩 - ( ゜- ゜)つロ 乾杯~ - bilibili 5 | 6 | ## 安装说明 7 | * 需要在python3环境下运行 8 | * 需要安装以下库 9 | 10 | ```shell 11 | sudo pip3 install bs4 12 | sudo pip3 install requests 13 | ``` 14 | 15 | ## 使用说明 16 | 直播免验证码登录接口失效了,传统登录接口也换了验证方式,暂时无法破解,只能使用cookies登录。 17 | * 安装[一键下载cookies的chrome插件](https://github.com/ookcode/CookiesDownloader) 18 | * 使用chrome手动登录bilibili,然后点击插件下载cookies 19 | * 将cookies放入本目录下 20 | * 运行main.py监听番剧更新 21 | 22 | ## 其他说明 23 | 如果您只是想参考部分bilibili的Api,请直接阅读api.py文件 24 | 25 | ## 计划实现功能 26 | b站更新频繁,最近没啥时间,暂停维护了。 -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #coding=utf-8 3 | ############################################################ 4 | # 5 | # bilibli api 6 | # 7 | ############################################################ 8 | import os,sys 9 | import requests 10 | import requests.utils 11 | import pickle 12 | import json 13 | from bs4 import BeautifulSoup 14 | import urllib 15 | 16 | headers = { 17 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36', 18 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 19 | } 20 | 21 | class Client(): 22 | def __init__(self): 23 | self.session = requests.Session() 24 | self.session.headers = headers 25 | self.userdata = '' 26 | 27 | def load_cookies_str(self, path): 28 | with open(path, 'rb') as f: 29 | cookies = f.read() 30 | self.session.headers['cookie'] = cookies 31 | self.userdata = {} 32 | 33 | def load_cookies(self, path): 34 | with open(path, 'rb') as f: 35 | self.session.cookies = requests.utils.cookiejar_from_dict(pickle.load(f)) 36 | self.userdata = {} 37 | 38 | def save_cookies(self, path): 39 | with open(path, 'wb') as f: 40 | cookies_dic = requests.utils.dict_from_cookiejar(self.session.cookies) 41 | pickle.dump(cookies_dic, f) 42 | 43 | 44 | #使用cookies登陆 45 | def cookies_login(self): 46 | root_path = os.path.dirname(os.path.realpath(sys.argv[0])) 47 | cookies_file = None 48 | # 当前目录查找后缀.cookies的文件 49 | for f in os.listdir(root_path): 50 | if os.path.splitext(f)[1] == ".cookies": 51 | cookies_file = os.path.join(root_path, f) 52 | break 53 | if not os.path.exists(cookies_file): 54 | print("在当前目录下未找到.cookies文件.") 55 | sys.exit() 56 | # 读取cookies文件 57 | self.load_cookies_str(cookies_file) 58 | if not self.get_account_info(): 59 | print(username + '.cookies失效,,请重新获取') 60 | sys.exit() 61 | print('欢迎您:', self.userdata['uname']) 62 | return self.userdata['uname'] 63 | 64 | #获取个人信息 65 | def get_account_info(self): 66 | response = self.session.get('https://account.bilibili.com/home/userInfo') 67 | data = json.loads(response.content.decode('utf-8')) 68 | try: 69 | if data['status'] == True: 70 | self.userdata = data['data'] 71 | return True 72 | except Exception as e: 73 | print(e) 74 | return False 75 | 76 | #获取个人通知消息个数 77 | def get_notify_count(self): 78 | #CaptchaKey 79 | response = self.session.get('http://www.bilibili.com/plus/widget/ajaxGetCaptchaKey.php?js') 80 | captcha = response.text.split('\"')[1] 81 | response = self.session.get('http://message.bilibili.com/api/notify/query.notify.count.do?captcha=' + captcha) 82 | 83 | #抢沙发 84 | def do_reply(self, avid, content): 85 | print("开始抢沙发:", content) 86 | preload = { 87 | "jsonp":"jsonp", 88 | "message":content, 89 | "type":1, 90 | "plat":1, 91 | "oid":avid 92 | } 93 | preload = urllib.parse.urlencode(preload) 94 | response = self.session.post("http://api.bilibili.com/x/reply/add", data=preload) 95 | print(response.text) 96 | 97 | #获取番剧详情 98 | def get_bangumi_detail(self, bangumi_id): 99 | response = self.session.get("https://bangumi.bilibili.com/jsonp/seasoninfo/{}.ver".format(bangumi_id)) 100 | content = response.content.decode('utf-8') 101 | begin = "seasonListCallback(" 102 | end = ");" 103 | content = content[len(begin): - len(end)] 104 | print(content) 105 | data = json.loads(content) 106 | try: 107 | if data['code'] == 0: 108 | return data['result'] 109 | except Exception as e: 110 | print('error', e) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #coding=utf-8 3 | ############################################################ 4 | # 5 | # 监听番剧更新并抢沙发 6 | # 7 | ############################################################ 8 | import os,sys 9 | if not sys.version_info[0] == 3: 10 | print("当前脚本只能在python3.x下运行,请更换您的python版本!") 11 | sys.exit() 12 | import mvc.grab_sofa_control as grab_sofa_control 13 | 14 | def main(): 15 | control = grab_sofa_control.Control() 16 | control.run() 17 | 18 | if __name__ == '__main__': 19 | main() -------------------------------------------------------------------------------- /mvc/grab_sofa_control.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #coding:utf-8 3 | 4 | from functools import partial 5 | from datetime import datetime 6 | import time 7 | import math 8 | import threading 9 | import mvc.grab_sofa_model as grab_sofa_model 10 | import mvc.grab_sofa_view as grab_sofa_view 11 | 12 | ################################### 13 | # 14 | # MVC - Control 15 | # 16 | #################################### 17 | class Control(): 18 | def __init__(self): 19 | self.view = grab_sofa_view.View() 20 | self.model = grab_sofa_model.Model() 21 | for av_id in self.model.data_dic.keys(): 22 | self.view.add_row(self.model.data_dic[av_id]) 23 | self._bind_events() 24 | t = threading.Thread(target=self._update) 25 | t.start() 26 | 27 | def _bind_events(self): 28 | self.view.create_btn.configure(command = self._create) 29 | for av_id in self.model.data_dic.keys(): 30 | row = self.view.rows[av_id] 31 | row["operate_btn"].configure(command = partial(self._operate, av_id)) 32 | row["delete_btn"].configure(command = partial(self._delete, av_id)) 33 | 34 | def _create(self): 35 | try: 36 | av_id = int(self.view.create_id.get()) 37 | except Exception as e: 38 | print("error can't create because:") 39 | print(e) 40 | return 41 | data = self.model.request_data(av_id) 42 | data['stoped'] = True 43 | self.model.add_data(av_id, data) 44 | self.view.add_row(data) 45 | self._bind_events() 46 | self.model.write_cache() 47 | 48 | def _delete(self, av_id): 49 | del self.model.data_dic[av_id] 50 | self.view.clear() 51 | for av_id in self.model.data_dic.keys(): 52 | self.view.add_row(self.model.data_dic[av_id]) 53 | self._bind_events() 54 | self.model.write_cache() 55 | 56 | def _operate(self, av_id): 57 | data = self.model.data_dic[av_id] 58 | if data['stoped']: 59 | data = self.model.request_data(av_id) 60 | data['stoped'] = False 61 | self.model.add_data(av_id, data) 62 | self.view.update_row(av_id, data) 63 | self.model.write_cache() 64 | else: 65 | data['stoped'] = True 66 | self.view.update_operate_btn(av_id, data['stoped']) 67 | 68 | def _update(self): 69 | while True: 70 | now = datetime.now() 71 | for av_id in self.model.data_dic.keys(): 72 | data = self.model.data_dic[av_id] 73 | # 不操作停止状态的数据 74 | if data['stoped']: 75 | continue 76 | ################## 77 | # 午时已到 78 | ################## 79 | if now > data['nextdate']: 80 | dt = now - data['nextdate'] 81 | # 当前日期超过预计更新日期太久,判定为今日停更,进入下一周期 82 | if dt.seconds >= self.model.max_over_minute * 60: 83 | print("{}超过预计更新时间超过{}分钟,今日停更,进入下一个周期!".format(av_id, self.model.max_over_minute)) 84 | self.model.defer_to_next_period(av_id) 85 | 86 | # 开始请求服务器数据 87 | else: 88 | self.view.update_countdown_lbl(av_id, "刷新中{}".format(dt.seconds)) 89 | new_data = self.model.request_data(av_id) 90 | print("{} {}".format(av_id, dt.seconds)) 91 | print(new_data['lastdate'], data['nextdate']) 92 | # 当发现有更新时 93 | if new_data['lastdate'] == data['nextdate']: 94 | print(data['title'], "发现更新") 95 | print(new_data) 96 | # 抢沙发 97 | reply = "稳坐二楼的千万手速王(`_´)ゞ" 98 | self.model.request_reply(new_data['last_id'], reply) 99 | self.model.add_data(av_id, new_data) 100 | 101 | ###################### 102 | # 午时未到,更新倒计时 103 | ###################### 104 | else: 105 | dt = data['nextdate'] - now 106 | seconds = dt.seconds 107 | minutes = math.floor(seconds / 60) 108 | hours = math.floor(minutes / 60) + dt.days * 24 109 | second = seconds % 60 110 | minute = minutes % 60 111 | self.view.update_countdown_lbl(av_id, "%d:%02d:%02d" % (hours, minute, second)) 112 | time.sleep(1) 113 | 114 | def run(self): 115 | self.view.run() -------------------------------------------------------------------------------- /mvc/grab_sofa_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #coding=utf-8 3 | import os,sys 4 | import api 5 | import json 6 | from datetime import datetime 7 | from datetime import timedelta 8 | 9 | g_root_path = os.path.dirname(os.path.realpath(sys.argv[0])) 10 | g_cache_path = os.path.join(g_root_path, 'cache.json') 11 | 12 | ################################### 13 | # 14 | # MVC - Model 15 | # 16 | #################################### 17 | class Model(): 18 | def __init__(self): 19 | self.max_over_minute = 120 20 | self.data_dic = self._read_cache() 21 | self.api = api.Client() 22 | self.api.cookies_login() 23 | 24 | def _read_cache(self): 25 | try: 26 | f = open(g_cache_path, 'r') 27 | cache_data = json.loads(f.read()) 28 | f.close() 29 | data_dic = {} 30 | for item in cache_data: 31 | data = {} 32 | data['id'] = item['id'] 33 | data['title'] = item['title'] 34 | data['weekday'] = [] 35 | data['uptime'] = item['uptime'] 36 | for weekday in item['weekday'].split(","): 37 | data['weekday'].append(int(weekday)) 38 | data['stoped'] = True 39 | data_dic[data['id']] = data 40 | return data_dic 41 | except Exception as e: 42 | print('error read cache.json:', e) 43 | return {} 44 | 45 | def _cal_delta_day(self, weeklist, today_weekday): 46 | # 周更 47 | if len(weeklist) == 1: 48 | return 7 49 | # 非周更 50 | else: 51 | for index, weekday in enumerate(weeklist): 52 | # TODO:处理在非计划时间内突然更新了的情况 53 | if weekday == today_weekday: 54 | try: 55 | next_weekday = weeklist[index + 1] 56 | except Exception as e: 57 | next_weekday = weeklist[0] 58 | break 59 | if next_weekday > today_weekday: 60 | return next_weekday - today_weekday 61 | else: 62 | return 7 - today_weekday + next_weekday 63 | 64 | def defer_to_next_period(self, av_id): 65 | data = self.data_dic[av_id] 66 | delta_day = self._cal_delta_day(data['weekday'], data['lastdate'].isoweekday()) 67 | data['nextdate'] = data['nextdate'] + timedelta(delta_day) 68 | 69 | def write_cache(self): 70 | cache_list = [] 71 | for key in self.data_dic.keys(): 72 | data = self.data_dic[key] 73 | cache = {} 74 | cache['id'] = data['id'] 75 | cache['title'] = data['title'] 76 | cache['weekday'] = (',').join(str(i) for i in data['weekday']) 77 | cache['uptime'] = data['uptime'] 78 | cache_list.append(cache) 79 | 80 | f = open(g_cache_path, 'w') 81 | f.write(json.dumps(cache_list)) 82 | f.close() 83 | 84 | def add_data(self, av_id, data): 85 | if av_id in self.data_dic.keys(): 86 | for key in data.keys(): 87 | self.data_dic[av_id][key] = data[key] 88 | else: 89 | self.data_dic[av_id] = data 90 | 91 | def request_data(self, av_id): 92 | info = self.api.get_bangumi_detail(av_id) 93 | if not info: 94 | print("{} not exists".format(av_id)) 95 | return None 96 | if info['pub_time'] == "": 97 | print("{} pub_time unknow".format(av_id)) 98 | return None 99 | # if int(info['is_finish']) != 0: 100 | # print(info['title'], "is finished") 101 | # return None 102 | data = {} 103 | data['id'] = int(info['season_id']) 104 | data['title'] = info['title'] 105 | data['uptime'] = info['pub_time'].split(" ")[1] 106 | weekday = int(info['weekday']) 107 | if weekday == 0: 108 | data['weekday'] = [7] 109 | elif weekday == -1: 110 | data['weekday'] = [1,2,3,4,5] 111 | else: 112 | data['weekday'] = [weekday] 113 | episodes = info['episodes'] 114 | lastdate = episodes[0]['update_time'].split(" ")[0] + " " + data['uptime'] 115 | # lastdate = "2016-08-25 08:42:00" #测试数据 116 | data['lastdate'] = datetime.strptime(lastdate, "%Y-%m-%d %H:%M:%S") 117 | data['last_id'] = int(episodes[0]['av_id']) 118 | # 计算下次更新时间 119 | delta_day = self._cal_delta_day(data['weekday'], data['lastdate'].isoweekday()) 120 | data['nextdate'] = data['lastdate'] + timedelta(delta_day) 121 | return data 122 | 123 | def request_reply(self, last_id, content): 124 | self.api.do_reply(last_id, content) 125 | -------------------------------------------------------------------------------- /mvc/grab_sofa_view.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #coding:utf-8 3 | import tkinter as tk 4 | from tkinter import ttk 5 | 6 | ################################### 7 | # 8 | # MVC - View 9 | # 10 | #################################### 11 | class View(): 12 | def __init__(self): 13 | self.root = tk.Tk() 14 | self.root.title('哔哩哔哩 - ( ゜- ゜)つロ 乾杯~ - bilibili') 15 | self.root.geometry('600x400') 16 | self.root.resizable(width=False, height=False) 17 | self.col_count = 6 18 | self.rows = {} 19 | for i in range(0, self.col_count): 20 | self.root.columnconfigure(i, weight=1) 21 | panel = ttk.Labelframe(self.root, text="欢迎光临") 22 | panel.columnconfigure(0, weight=1) 23 | panel.grid(row=0, column=0, columnspan=self.col_count, stick='we') 24 | 25 | self.create_id = tk.StringVar() 26 | self.create_id.set("请输入番剧ID") 27 | 28 | self.create_entry = tk.Entry(panel, textvariable=self.create_id) 29 | self.create_btn = tk.Button(panel, text='create') 30 | 31 | self.create_entry.grid(row=0, column=1) 32 | self.create_btn.grid(row=0, column=2) 33 | 34 | id_lbl = tk.Label(self.root, text="ID") 35 | title_lbl = tk.Label(self.root, text="名称") 36 | uptime_lbl = tk.Label(self.root, text="更新时间") 37 | countdown_lbl = tk.Label(self.root, text="倒计时") 38 | status_lbl = tk.Label(self.root, text="状态") 39 | 40 | id_lbl.grid(row=1, column=0) 41 | title_lbl.grid(row=1, column=1) 42 | uptime_lbl.grid(row=1, column=2) 43 | countdown_lbl.grid(row=1, column=3) 44 | status_lbl.grid(row=1, column=4) 45 | 46 | # 增 47 | def add_row(self, data): 48 | index = len(self.rows) 49 | av_id = data['id'] 50 | row_items = {} 51 | id_lbl = tk.Label(self.root, text="") 52 | title_lbl = tk.Label(self.root, text="") 53 | uptime_lbl = tk.Label(self.root, text="") 54 | countdown_lbl = tk.Label(self.root, text="") 55 | operate_btn = tk.Button(self.root, text="") 56 | delete_btn = tk.Button(self.root, text='del') 57 | 58 | row_items["id_lbl"] = id_lbl 59 | row_items["title_lbl"] = title_lbl 60 | row_items["uptime_lbl"] = uptime_lbl 61 | row_items["countdown_lbl"] = countdown_lbl 62 | row_items["operate_btn"] = operate_btn 63 | row_items["delete_btn"] = delete_btn 64 | 65 | id_lbl.grid(row=index + 2, column=0) 66 | title_lbl.grid(row=index + 2, column=1) 67 | uptime_lbl.grid(row=index + 2, column=2) 68 | countdown_lbl.grid(row=index + 2, column=3) 69 | operate_btn.grid(row=index + 2, column=4) 70 | delete_btn.grid(row=index + 2, column=5) 71 | 72 | self.rows[av_id] = row_items 73 | self.update_row(av_id, data) 74 | 75 | # 清屏 76 | def clear(self): 77 | for row_key in self.rows.keys(): 78 | row = self.rows[row_key] 79 | for key in row.keys(): 80 | row[key].grid_forget() 81 | self.rows.clear() 82 | 83 | # 改 84 | def update_row(self, av_id, data): 85 | row = self.rows[av_id] 86 | uptime_text = "周{} {}".format((',').join(str(i) for i in data['weekday']), data['uptime']) 87 | row["id_lbl"].configure(text = str(data['id'])) 88 | row["title_lbl"].configure(text = data['title']) 89 | row["uptime_lbl"].configure(text = uptime_text) 90 | self.update_operate_btn(av_id, data['stoped']) 91 | 92 | # 改变操作按钮状态 93 | def update_operate_btn(self, av_id, stoped): 94 | row = self.rows[av_id] 95 | if stoped: 96 | row["countdown_lbl"].configure(text = "--:--:--") 97 | row["operate_btn"].configure(text = "stoped") 98 | else: 99 | row["operate_btn"].configure(text = "running") 100 | 101 | # 更新倒计时 102 | def update_countdown_lbl(self, av_id, timestr): 103 | row = self.rows[av_id] 104 | row["countdown_lbl"].configure(text = timestr) 105 | 106 | # ui运行 107 | def run(self): 108 | self.root.mainloop() --------------------------------------------------------------------------------