class Display(): def __init__(self): self.console = Console(force_terminal=True, color_system='truecolor') self.console._environ['TERM'] = 'SMART' self._lock = threading.Lock() self.live_infos = Infos() self.last_time = datetime.datetime.now() self.last_net_sent = 0.0 self.last_net_recv = 0.0 def generate_info(self, row_id: int, live_info: dict) -> Info: info = None while info is None: try: if live_info is not None and 'live_status' in live_info: info = Info( row_id=row_id, room_id=live_info['room_id'], anchor=live_info['uname'], title=live_info['title'], live_status=live_info['live_status'], record_status=live_info['recording'], start_time=datetime.datetime.fromtimestamp( live_info['live_start_time']).strftime( '%Y-%m-%d %H:%M:%S'), record_start_time=live_info['record_start_time'], queue_status=live_info['queue_status'], finish_time=live_info['finish_time']) else: break except Exception as e: continue return info def create_info_table(self, live_infos): dct = {0: 0, 1: 100, 2: 50, 4: 10} dct2 = {0: 0, 1: 100, 2: 50, 3: 30} infos = sorted([ self.generate_info(rid, live_infos[key]) for key, rid in zip(live_infos.keys(), range(len(live_infos))) ], key=lambda i: dct[i.live_status] * 100 + 100 * dct2[ i.record_status] - i.row_id + i.queue_status, reverse=True) table1 = Table("行号", "房间ID", "主播", "直播标题", "直播状态", "录制状态", "开播时间", "录制时长", "队列情况", "完成时间", title="%s" % datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), box=box.SIMPLE) for info in infos: table1.add_row(str(info.row_id), info.room_id_map, info.anchor_map, info.title_map, info.live_status_map, info.record_status_map, info.start_time_map, info.record_time, info.queue_status_map, info.finish_time_map) table2 = Table("CPU", "Memory", "NetSent", "NetRecv", box=box.SIMPLE) time_now = datetime.datetime.now() now_recv = psutil.net_io_counters().bytes_recv now_sent = psutil.net_io_counters().bytes_sent table2.add_row( str(psutil.cpu_percent(None)) + '%' + ' %.2fGHz' % (psutil.cpu_freq().current / 1000.0), str(psutil.virtual_memory().percent) + '%' + ' %s/%s' % (bytes2human(psutil.virtual_memory().used), bytes2human(psutil.virtual_memory().total)), bytes2human((now_sent - self.last_net_sent) / (time_now - self.last_time).total_seconds()) + '/s', bytes2human((now_recv - self.last_net_recv) / (time_now - self.last_time).total_seconds()) + '/s') self.last_time = time_now self.last_net_sent = now_sent self.last_net_recv = now_recv return RenderGroup(table1, table2) def run(self): # self.console.clear() with Live(console=self.console, auto_refresh=False) as live: while True: try: live.update(self.create_info_table(self.live_infos.copy()), refresh=True) time.sleep(1) except Exception as e: logger.critical(e) continue
class Queue(): def __init__(self): self._lock = threading.Lock() self._lock2 = threading.Lock() self.config = Config() self.infos = Infos() self.queue = [] self.qname = '' self.func = lambda x:time.sleep(400) self.threadRecorder = threadRecorder() self.base_num = 0 def func_call(self, key): with self._lock2: logger.info('%s 开始%s' % (self.infos.copy()[key]['uname'], self.qname)) live_info = self.infos.copy()[key] live_info['queue_status'] = self.base_num self.infos.update(key, live_info) self.func(key) live_info['queue_status'] = self.base_num + 500 live_info['finish_time'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') self.infos.update(key, live_info) def update_status(self): for key in self.queue: live_info = self.infos.copy()[key] live_info['queue_status'] = self.base_num + self.queue.index(key) + 1 self.infos.update(key, live_info) def enqueue(self, key): with self._lock: live_info = self.infos.copy()[key] live_info['queue_status'] = self.base_num + len(self.queue) + 1 if key not in self.queue: self.queue.append(key) logger.info('%s 进入%s等待队列' % (self.infos.copy()[key]['uname'],self.qname)) else: self.queue.remove(key) self.queue.append(key) logger.info('%s 在%s等待队列中的状态更新了' % (self.infos.copy()[key]['uname'],self.qname)) self.infos.update(key, live_info) def dequeue(self): if self._lock2.locked(): return None with self._lock: with self._lock2: if len(self.queue) > 0: key = self.queue[0] del self.queue[0] self.update_status() logger.info('%s 退出%s等待队列' % (self.infos.copy()[key]['uname'],self.qname)) return key else: return None def run(self): self.threadRecorder.add('%s heartbeat' % self.qname,self.heartbeat,None,True) while True: time.sleep(1) if len(self.queue) > 0: key = self.dequeue() if key is not None: self.threadRecorder.add('%s_%s' % (self.qname,key),self.func_call,[key,],False) def heartbeat(self): while True: time.sleep(180) logger.info('当前%s队列情况: %s' % (self.qname, ' '.join([self.infos.copy()[key]['uname'] for key in self.queue])))
class Live(): def __init__(self): self.config = Config() self.cookies = login() logger.info(self.cookies) self.base_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), self.config.config['output']['path']) self.live_infos = Infos() self.display = Display() self.decoder = Decoder() self.uploader = Upload() self.threadRecorder = threadRecorder() logger.info('基路径:%s' % (self.base_path)) self.load_room_info() self.get_live_url() logger.info('初始化完成') def create_duration(self, start_time, end_time): t = datetime.datetime.now() tt = t.strftime('%Y%m%d %H%M%S') if start_time == end_time == '0': return '0' tmp = datetime.datetime.strptime(tt.split(' ')[0] + ' %s' % start_time, '%Y%m%d %H%M%S') if t > tmp: base_time1 = tt.split(' ')[0] base_time2 = (t + datetime.timedelta(days=1)).strftime('%Y%m%d %H%M%S').split(' ')[0] else: base_time1 = (t - datetime.timedelta(days=1)).strftime('%Y%m%d %H%M%S').split(' ')[0] base_time2 = tt.split(' ')[0] if start_time > end_time: start_time = '%s %s' % (base_time1, start_time) end_time = '%s %s' % (base_time2, end_time) else: start_time = '%s %s' % (tt.split(' ')[0], start_time) end_time = '%s %s' % (tt.split(' ')[0], end_time) return '%s-%s' % (start_time, end_time) def check_live(self, key): duration = self.live_infos.get(key)['duration'] if duration == '0': return True lst = duration.split('-') now_time = datetime.datetime.now() if len(lst) == 2: start_time = datetime.datetime.strptime(lst[0], '%Y%m%d %H%M%S') end_time = datetime.datetime.strptime(lst[1], '%Y%m%d %H%M%S') if now_time > start_time and now_time < end_time: return True else: logger.debug('%s[RoomID:%s]不在直播时间段' % (self.live_infos.get(key)['uname'], key)) return False else: return False def load_room_info(self): live_infos = self.live_infos.copy() for lst in self.config.config['live']['room_info']: if lst[0] not in live_infos: live_info = {} live_info['record_start_time'] = '' live_info['queue_status'] = 0 live_info['recording'] = 0 live_info['finish_time'] = '' else: live_info = live_infos[lst[0]] live_info['need_rec'] = lst[1] live_info['need_mask'] = lst[2] live_info['maxsecond'] = lst[3] live_info['need_upload'] = lst[4] live_info['duration'] = self.create_duration(lst[5], lst[6]) live_info['cookies'] = self.cookies live_info['base_path'] = self.base_path self.live_infos.update(lst[0],live_info) def load_realtime(self): ''' 实时加载配置,更新房间信息 ''' self.config.load_cfg() # logger.info(self.config.config) room_lst = [i[0] for i in self.config.config['live']['room_info']] del_lst = [] for key in self.live_infos.copy(): if key not in room_lst: del_lst.append(key) for key in del_lst: self.live_infos.delete(key) self.load_room_info() def judge_in(self, key): room_lst = [i[0] for i in self.config.config['live']['room_info']] if key not in room_lst: return False return True def judge_download(self,key): if not self.judge_in(key): return False live_info = self.live_infos.copy()[key] if live_info['live_status'] != 1: live_info['recording'] =0 self.live_infos.update(key,live_info) return False elif not self.check_live(key) and live_info['live_status'] == 1: live_info['recording'] =2 self.live_infos.update(key,live_info) return False elif self.check_live(key) and live_info['live_status'] == 1 and live_info['need_rec'] == '0': live_info['recording']=3 self.live_infos.update(key,live_info) return False elif live_info['live_status'] == 1 and live_info['need_rec'] == '1': live_info['recording']=1 self.live_infos.update(key,live_info) return True else: logger.warning('%s[RoomID:%s]进入了未知的分支呢' % (live_info['uname'],key)) live_info['recording'] =0 self.live_infos.update(key,live_info) return False def get_live_url(self): ''' 获取所有监听直播间的信息 ''' room_lst = [i[0] for i in self.config.config['live']['room_info']] for id in room_lst: info = None while info is None: try: info = live.get_room_info(id, cookies=self.cookies) time.sleep(0.3) except: logger.error('[RoomID:%s]获取信息失败,重新尝试' % (id)) continue live_info = self.live_infos.copy()[id] live_info['room_id'] = id live_info['real_id'] = info['room_info']['room_id'] try: if live_info['live_status'] != 1 and info['room_info']['live_status'] == 1: logger.info('%s[RoomID:%s]开播了' % (live_info['uname'], id)) toaster = ToastNotifier() toaster.show_toast("开播通知", '%s[RoomID:%s]开播了' % (live_info['uname'], id), icon_path=None, duration=3) except: pass try: live_info['live_status'] = info['room_info']['live_status'] live_info['uid'] = info['room_info']['uid'] live_info['uname'] = info['anchor_info']['base_info']['uname'] live_info['save_name'] = '%s_%s.flv' % ( live_info['uname'], time.strftime("%Y%m%d%H%M%S", time.localtime())) live_info['title'] = info['room_info']['title'] live_info['live_start_time'] = info['room_info']['live_start_time'] self.live_infos.update(id,live_info) logger.debug( '%s[RoomID:%s]直播状态\t%s' % (live_info['uname'], id, live_info['live_status'])) except Exception as e: logger.critical(e) logger.error('[RoomID:%s]房间信息更新失败' % (id)) logger.error(info) # logger.info(self.live_infos.copy()) def get_stream(self, key): ''' 获取直播流 :param key: 房间显示id :return: stream ''' if not self.judge_in(key): return None live_info = self.live_infos.copy()[key] logger.info('%s[RoomID:%s]获取直播流' % (live_info['uname'], key)) session = streamlink.Streamlink() session.set_option("http-cookies", self.cookies) session.set_option("http-headers", headers) log_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'log', 'stream.log') session.set_loglevel("debug") session.set_logoutput(open(log_path, 'a',encoding='utf-8')) streams = None while streams is None: try: streams = session.streams('https://live.bilibili.com/%s' % key) except: logger.warning('%s[RoomID:%s]获取直播流失败,正在重试' % (live_info['uname'], key)) time.sleep(1) if streams == {}: logger.error('%s[RoomID:%s]未获取到直播流,可能是下播或者网络问题' % (live_info['uname'], key)) return None if 'best' in streams: logger.info('%s[RoomID:%s]获取到best直播流' % (live_info['uname'], key)) return streams['best'] elif 'source' in streams: logger.info('%s[RoomID:%s]获取到source直播流' % (live_info['uname'], key)) return streams['source'] elif 'worst' in streams: logger.info('%s[RoomID:%s]获取到worst直播流' % (live_info['uname'], key)) return streams['worst'] else: logger.info('%s[RoomID:%s]未获取到直播流' % (live_info['uname'], key)) return None def unlive(self, key, unlived): if not self.judge_in(key): return None live_info = self.live_infos.copy()[key] logger.info('%s[RoomID:%s]似乎下播了' % (live_info['uname'], key)) live_info['recording'] = 0 logger.info('%s[RoomID:%s]录制结束,录制了%.2f分钟' % (live_info['uname'], key, ( datetime.datetime.now() - datetime.datetime.strptime( live_info['record_start_time'], '%Y-%m-%d %H:%M:%S')).total_seconds() / 60.0)) live_info['record_start_time'] = '' if unlived: logger.info('%s[RoomID:%s]确认下播,加入转码上传队列' % (live_info['uname'], key)) # if live_info['need_upload'] == '1': # live_info['filename'] = live_info['uname'] + live_info['duration'].split('-')[0].split(' ')[0] # live_info['filepath'] = os.path.join(self.base_path, live_info['uname'], '%s_%s' % (live_info['uname'], live_info['duration'].split('-')[0].split(' ')[0])) # if live_info['need_mask'] == '1': # live_info['filepath'] += '_mask.mp4' # else: # live_info['filepath'] += '.mp4' self.decoder.enqueue(key) self.live_infos.update(key,live_info) def download_live(self, key): if not self.judge_in(key): return None save_path = os.path.join(self.base_path, self.live_infos.get(key)['uname'], 'recording') logger.info('%s[RoomID:%s]准备下载直播流,保存在%s' % (self.live_infos.get(key)['uname'], key, save_path)) self.live_infos.get(key)['recording'] = 1 if not os.path.exists(save_path): os.makedirs(save_path) stream = self.get_stream(key) if stream is None: logger.error('%s[RoomID:%s]获取直播流失败' % (self.live_infos.get(key)['uname'], key)) self.live_infos.get(key)['record_start_time'] = '' self.live_infos.get(key)['recording'] = 0 return filename = os.path.join(save_path, self.live_infos.get(key)['save_name']) self.live_infos.get(key)['record_start_time'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') try: fd = stream.open() except Exception as e: self.unlive(key,unlived=False) logger.critical('%s[RoomID:%s]fd open error' % (self.live_infos.get(key)['uname'], key)) logger.error(e) return with open(filename, 'wb') as f: while self.judge_in(key) and self.live_infos.get(key)['live_status'] == 1 and self.live_infos.get(key)[ 'need_rec'] == '1' and self.check_live(key): try: data = fd.read(1024 * 8) if len(data) > 0: f.write(data) else: fd.close() logger.warning('%s[RoomID:%s]直播流断开,尝试重连' % (self.live_infos.get(key)['uname'], key)) stream = self.get_stream(key) if stream is None: logger.warning('%s[RoomID:%s]重连失败' % (self.live_infos.get(key)['uname'], key)) self.unlive(key, True) return else: logger.info('%s[RoomID:%s]重连成功' % (self.live_infos.get(key)['uname'], key)) fd = stream.open() except Exception as e: fd.close() self.unlive(key,unlived=False) logger.critical('%s[RoomID:%s]遇到了什么问题' % (self.live_infos.get(key)['uname'], key)) logger.error(e) return fd.close() self.unlive(key, True) def run(self): while True: time.sleep(1) self.load_realtime() self.get_live_url() live_infos = self.live_infos.copy() for key in live_infos: if live_infos[key]['recording'] != 1 and self.judge_download(key): self.threadRecorder.add('download_live_%s' % (key),self.download_live,[key,],False) time.sleep(0.2) def start(self): self.threadRecorder.add('display_run',self.display.run,None,False) self.threadRecorder.add('decoder_run',self.decoder.run,None,False) self.threadRecorder.add('uploader_run',self.uploader.run,None,False) self.threadRecorder.add('live_run',self.run,None,False)
class History(): def __init__(self): self._lock = threading.Lock() self.live_infos = Infos() self.base_path = os.path.join( os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'history') self.init() logger.debug('History模块初始化完成') def init(self): if not os.path.exists(self.base_path): os.mkdir(self.base_path) for key in self.live_infos.copy(): history_file = os.path.join( self.base_path, '%s_%s' % (key, self.live_infos.get(key)['uname'])) if not os.path.exists(history_file): with open(history_file, 'w', encoding='utf-8') as a: pass def add_info(self, key, para, output): self.init() history_file = os.path.join( self.base_path, '%s_%s' % (key, self.live_infos.get(key)['uname'])) with self._lock: with open(history_file, 'a', encoding='utf-8') as a: a.write( '####📢 %s, 当前%s状态为: %s, 当前时间: %s\n' % (output, para, self.live_infos.get(key)[para], datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) def heartbeat(self): while True: try: time.sleep(600) self.init() for key in self.live_infos.copy(): history_file = os.path.join( self.base_path, '%s_%s' % (key, self.live_infos.get(key)['uname'])) with self._lock: with open(history_file, 'a', encoding='utf-8') as a: a.write('####✨ 当前时间: %s\n' % (datetime.datetime.now().strftime( '%Y-%m-%d %H:%M:%S'))) a.write('录制时段: %s\n' % (self.live_infos.get(key)['duration'])) a.write('直播状态: %s\n' % (self.live_infos.get(key)['live_status'])) a.write('录制状态: %s\n' % (self.live_infos.get(key)['recording'])) a.write('是否录制: %s\n' % (self.live_infos.get(key)['need_rec'])) a.write('是否遮挡: %s\n' % (self.live_infos.get(key)['need_mask'])) a.write('是否上传: %s\n' % (self.live_infos.get(key)['need_upload'])) except Exception as e: logger.critical(e) continue