def login_bilibili(path): '''封装登陆Bilibili时的log及raise exception Args: path: str, cookies文件路径 Return: b: Bilibili类, 且已登录 Raise: Exception: Cookies登陆失败 ''' b = Bilibili() logmsg('尝试通过Cookies登陆Bilibili') LOGIN_STATUS = False if os.path.exists(path): b.login_by_cookies(path) if b.isLogin(): LOGIN_STATUS = True else: errmsg('login', '找不到cookies.txt') if LOGIN_STATUS == False: raise Exception('Cookies登陆Bilibili失败') my_info = b.get_my_basic_info() logmsg("[已登录账号{}][mid:{}][昵称:{}]".format(my_info['userid'], my_info['mid'], my_info['uname'])) return b
def get_live_url(path, liver, site='YouTube'): lives_info = loadJson(path) live_url = '' for live_info in lives_info: if live_info['liver'] == liver: for room in live_info['room']: if room['site'] == site: live_url = room['url'] if len(live_url) == 0: raise Exception(path + '\n文件中找不到直播url地址') logmsg('获得直播url地址:' + live_url) return live_url
def push_stream(url_rtmp, url_live, url_m3u8, command): '''调用ffmpeg将url_rtmp推至url_rtmp Args: url_rtmp: url_live: 仅用于记录 url_m3u8: command: str, 推流命令 ''' logmsg('开始推流\nusing push_stream in Youtube\n{}\n{}\n{}\n{}\n'.format(url_rtmp,url_live,url_m3u8,command)) command = command.format(url_m3u8, url_rtmp) out, err, errcode = RunCMD(command) logmsg('结束推流') return out, err, errcode
def get_m3u8(url_live, live_quality): '''获得YouTube直播的m3u8地址 调用youtube-dl,获得清晰度列表, 并返回其中清晰度不高于live_quality的最高清晰度m3u8。 Args: url_live: str, 直播间的url live_quality: int, 最高清晰度,返回的m3u8清晰度不会高于此值 Returns: m3u8_url: str, 直播的m3u8地址 Raise: Exception ''' # 获取清晰度 out, err, errcode = RunCMD('youtube-dl --no-check-certificate -j {}'.format(url_live)) out = out.decode('utf-8') if isinstance(out, (bytes, bytearray)) else out if errcode != 0: raise Exception('youtube-dl不正常返回,code={}'.format(errcode)) try: vDict = json.loads(out) except Exception: raise Exception('清晰度列表无法用json解析') try: # 按清晰度由小到大排序 vDict['formats'].sort(key=lambda live_format : live_format['height']) count = -1 if live_quality != 0: for live_format in vDict['formats']: if live_format['height'] <= live_quality: count += 1 # 指向不大于所选择清晰度的最大值 else: break if count == -1: count = 0 # 选择清晰度小于最小清晰度时,返回最小清晰度 else: count = -1 # 自动使用最高清晰度时,返回最后一组live_format # 获取m3u8 m3u8_url = vDict['formats'][count]['url'] logmsg('获得直播源m3u8地址:\n{m3u8}'.format(m3u8=m3u8_url)) return m3u8_url except Exception: raise Exception('解析清晰度时格式出错,vDict:\n{}'.format(json.dumps(vDict, ensure_ascii=False, indent=2)))
def post_schedule(self, lives=None): '''发送时间表动态 Args: lives: 直播信息列表 Returns: 0: 正常发送动态 -1: 发送动态过程中出错 ''' # 兼容旧版保留lives参数 # 如果没有给lives参数则获取时间表中的lives if not lives: lives = self.__lives.values() # Read config config = CONFIGs() COOKIES_TXT_PATH = config.COOKIES_TXT_PATH IS_SEND_DAILY_DYNAMIC = config.IS_SEND_DAILY_DYNAMIC # 如果没有直播预定或设置为不发送动态,则中止 if len(lives) == 0 or not IS_SEND_DAILY_DYNAMIC: return 0 # Post dynamic try: schedule_post_txt = self.__make_schedule_post_txt(lives) b = login_bilibili(COOKIES_TXT_PATH) if b.send_dynamic(schedule_post_txt): logmsg('发送每日动态成功') else: errmsg('发送每日动态失败') return -1 except Exception as e: txt = '' if len(str(e).strip()) == 0: txt = '\n' + tracemsg(e) errmsg('schedule', str(e) + txt) return -1 return 0
def main(CONFIG_PATH): '''程序主入口 调用makeLives获得live列表,添加到scheduler后启动scheduler。 Args: CONFIG_PATH: str, config.ini的储存位置。 ''' # CONFIG初始化,必须放在所有步骤前 configs = CONFIGs() configs.set_configs(CONFIG_PATH) WEB_PORT = configs.WEB_PORT logmsg('程序启动') # 解析schedule.txt并发送每日转播表动态 # 发送每日动态的任务已整合入LiveScheduler类中 lives = makeLives() # post_schedule(lives) # LiveScheduler为单例类,初始化需在web运行前 scheduler = LiveScheduler(timezone='Asia/Tokyo') for live in lives: scheduler.add_live(rebroadcast, live) try: scheduler.start() logmsg('时间表启动') # APScheduler直接调用shutdown不会等待未开始执行的任务 # get_jobs返回空列表时所有任务都已开始执行 # 此时调用shutdown才会等待已开始执行的任务结束 # while len(scheduler.get_jobs()) != 0: # sleep(600) web.run(host='0.0.0.0', port=WEB_PORT) scheduler.shutdown() except KeyboardInterrupt: errmsg('normal', '因KeyboardInterrupt退出') except Exception as e: msg = str(e) + '\n' + tracemsg(e) errmsg('normal', msg) if scheduler.running: scheduler.shutdown(wait=False) saveLives(scheduler.get_lives().values()) logmsg('程序结束')
def rebroadcast(live): """一次转播任务的主函数 一次转播任务分两个阶段 初始化: 登陆bilibili 从liveInfo.json中查得直播间地址 并获得直播网站的对应get_m3u8与push_stream函数 此时出现任何异常都会推出rebroadcast函数 每分钟一次,共20次循环: 获取m3u8地址 开启bilibili直播间 发送开播动态(仅一次) 开始推流 为了liver推迟开播、直播中途断开等容错 此时出现任何异常都会立即继续下一次循环 其中开播动态只会在第一次成功开始推流前发送一次 Args: args: dict, 结构如下 { 'time': datetime.datetime, 'liver': string, 'site': string, default='YouTube', 'title': string, default='liver+'转播', } """ args = live.args() try: logmsg('开始推流项目:\n{liver}:{site}'.format(liver=args['liver'], site=args['site'])) # Read Config config = CONFIGs() COOKIES_TXT_PATH = config.COOKIES_TXT_PATH LIVE_INFO_PATH = config.LIVE_INFO_PATH BILIBILI_ROOM_TITLE = config.BILIBILI_ROOM_TITLE FFMPEG_COMMAND = config.FFMPEG_COMMAND BILIBILI_ROOM_AREA_ID = config.BILIBILI_ROOM_AREA_ID LIVE_QUALITY = config.LIVE_QUALITY IS_SEND_PRELIVE_DYNAMIC = config.IS_SEND_PRELIVE_DYNAMIC PRELIVE_DYNAMIC_FORM = config.PRELIVE_DYNAMIC_FORM b = login_bilibili(COOKIES_TXT_PATH) live_url = get_live_url(LIVE_INFO_PATH, args['liver'], args['site']) get_m3u8, push_stream = get_method(args['site']) retry_count = 0 has_posted_dynamic = False while retry_count <= 20: try: url_m3u8 = get_m3u8(live_url, LIVE_QUALITY) room_id = b.getMyRoomId() # 防止前一次直播未结束,先暂存旧直播标题,推流错误时将标题重新改回 old_title = b.getRoomTitle(room_id) b.updateRoomTitle( room_id, BILIBILI_ROOM_TITLE.format(time=args['time'], liver=args['liver'], site=args['site'], title=args['title'])) url_rtmp = b.startLive(room_id, BILIBILI_ROOM_AREA_ID) logmsg("开播成功,获得推流地址:{}".format(url_rtmp)) sleep(5) # 每次直播只发送一次动态 if not has_posted_dynamic and IS_SEND_PRELIVE_DYNAMIC: dynamic_id = b.send_dynamic( PRELIVE_DYNAMIC_FORM.format( liver=args['liver'], time=args['time'].strftime(r'%m.%d %H:%M'), site=args['site'], title=args['title'], url='https://live.bilibili.com/' + str(room_id))) has_posted_dynamic = True logmsg('项目{}发送动态'.format(args['liver'])) out, err, errcode = push_stream(url_rtmp, live_url, url_m3u8, FFMPEG_COMMAND) # 前一次直播未结束 if errcode == 1: sleep(10) b.updateRoomTitle(room_id, old_title) sleep(60) raise Exception('直播间被占用') except Exception as e: msg = tracemsg(e) if len(str(e).strip()) == 0 else str(e) errmsg( 'normal', '项目:{time} {liver}\n尝试推流失败,retry_count={retry_count}\n'. format(time=args['time'], liver=args['liver'], retry_count=retry_count) + msg) retry_count += 1 sleep(60) # 关闭项目前删除已发送的动态 # 若未发送动态则一定未转播成功 # 但已发送动态不一定转播成功,可能是由于前一次转播未结束导致 # 此BUG以后再调整 if has_posted_dynamic: b.delete_dynamic(dynamic_id) logmsg('项目{}删除动态'.format(args['liver'])) """ else: b.send_dynamic( '转播失败: {liver}, {site}\n时间: {time}\n{title}'.format( liver=args['liver'], time=args['time'], title=args['title'], site=args['site'] ) ) logmsg('项目{}发送转播失败动态'.format(args['liver'])) """ except Exception as e: txt = '' if len(str(e).strip()) == 0: txt = '\n' + tracemsg(e) errmsg('schedule', str(e) + txt) logmsg('关闭推流项目:\n{liver}:{site}'.format(liver=args['liver'], site=args['site']))