def __make_schedule_post_txt(self, lives): '''读取lives列表,输出用于发动态的时间表字符串 ''' DAILY_DYNAMIC_FORM = CONFIGs().DAILY_DYNAMIC_FORM txt = '今日转播:\n时间均为日本时区\n' for live in lives: txt += DAILY_DYNAMIC_FORM.format( time=live.time.strftime(r'%m.%d %H:%M'), liver=live.liver, site=live.site, title=live.title) return txt
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 __init__(self, time, liver, site='', title=''): '''Live类构造函数 site与title参数为与旧格式兼容 可支持省略或填入空字符串 :param datetime|str time:可使用datetime实例或长度为4的时间字符串 :param str liver: :param str site: 默认值为'YouTube' :param str title: 默认值为liver+' 转播' ''' if isinstance(time, str): time = Live.analyse_time_text(time) if site == '': site = 'YouTube' if title == '': DEFAULT_TITLE_PARAM = CONFIGs().DEFAULT_TITLE_PARAM title = DEFAULT_TITLE_PARAM.format( time=time.strftime(r'%m.%d %H:%M'), liver=liver, site=site) self.time = time self.liver = liver self.site = site self.title = title
def makeLives(): '''读取schedule.txt,解析,并输出直播信息列表 Returns: lives: live类列表 ''' # Read config config = CONFIGs() SCHEDULE_TXT_PATH = config.SCHEDULE_TXT_PATH # Get live list text = "" with open(SCHEDULE_TXT_PATH, encoding='utf-8') as file: text = file.read() lives = __analyse_live_list(text) lives.sort(key=lambda live:live.time) return lives
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 saveLives(lives): '''读取live列表,以相应格式输入schedule.txt ''' # Read config config = CONFIGs() SCHEDULE_TXT_PATH = config.SCHEDULE_TXT_PATH # Make text texts = [ '# 时间表', '# 格式:', '# time@liver@site@title', '# time:直播时间,格式为HHMM,只接受未来24小时内的直播,检测出直播时间在运行程序之前时,会自动认为直播在运行程序第二天开始。', '# liver:只接受liveInfo.json中存在的liver名。(可自行按json格式添加到liveInfo文件中)', '# site:直播网站,目前只接受YouTube。而且不填默认为YouTube。', '# title:填入config.ini中直播间标题的title中,可选,不填默认为liver+"转播"', '', '# 例:', '# 1900@桜凛月@绝地求生', '# 2100@黒井しば', '# 2240@飛鳥ひな@YouTube', '# 0640@伏見ガク@YouTube@おはガク!', ] for live in lives: line = '{time}@{liver}@{site}@{title}'.format( time=live.time.strftime('%H%M'), liver=live.liver, site=live.site, title=live.title ) texts.append(line) text = '' for line in texts: text += line+'\n' with open(SCHEDULE_TXT_PATH, 'w', encoding='utf-8') as file: file.write(text)
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']))
def configs(): '''设置页面 POST访问时动作由参数Action值决定 为Apply时应用form中设置 为Reset时丢弃form中设置 ''' if flRequest.method == 'POST' and flRequest.form['Action'] == 'Apply': config = CONFIGs() config.BILIBILI_ROOM_TITLE = flRequest.form['BILIBILI_ROOM_TITLE'] config.DEFAULT_TITLE_PARAM = flRequest.form['DEFAULT_TITLE_PARAM'] config.BILIBILI_ROOM_AREA_ID = int( flRequest.form['BILIBILI_ROOM_AREA_ID']) config.IS_SEND_DAILY_DYNAMIC = True if flRequest.form.get( 'IS_SEND_DAILY_DYNAMIC') else False config.DAILY_DYNAMIC_FORM = flRequest.form['DAILY_DYNAMIC_FORM'] config.IS_SEND_PRELIVE_DYNAMIC = True if flRequest.form.get( 'IS_SEND_PRELIVE_DYNAMIC') else False config.PRELIVE_DYNAMIC_FORM = flRequest.form['PRELIVE_DYNAMIC_FORM'] config.LIVE_QUALITY = int(flRequest.form['LIVE_QUALITY']) config.save_configs() return redirect(url_for('autoLive.schedule')) return render_template('configs.html', header_menus=header_menus(), form_action=url_for('autoLive.configs'), sections=configs_sections())
def schedule_sections(): '''返回schedule页所需的数据 ''' # 获得site列表 现在可用的只有YouTube sites = ['YouTube'] # 获得liver列表 lives_info = loadJson(CONFIGs().LIVE_INFO_PATH) livers = [] for live_info in lives_info: livers.append(live_info['liver']) # 获得查看时间表预定中所需的列表 lives = LiveScheduler().get_lives() rows = [] # 获取时将时间表按时间排序 for live_id, live in sorted(lives.items(), key=lambda kv: kv[1].time): rows.append( { 'values':[ live.time.strftime(r'%m.%d %H:%M'), live.liver, live.site, live.title ], 'id': live_id } ) # 获得查看运行中项目所需的job列表 jobs = [] for live_id, job in LiveScheduler().get_livings().items(): running_rows=[ {'title': 'YouTuber', 'value': job['live'].liver}, {'title': '直播网站', 'value': job['live'].site}, {'title': '直播间自定义标题', 'value': job['live'].title}, {'title': '预计开始时间', 'value': job['live'].time}, {'title': '实际开始时间', 'value': job['startT']}, {'title': '持续时间', 'value': datetime.now() - job['startT']} ] jobs.append( { 'title': live_id, 'rows': running_rows } ) # 添加直播项中需要的值 add_job_value = { 'title': '添加新项目', 'descr': '添加时间表项目。\n'\ '\n'\ '时间一栏可填入以下内容:\n'\ '1. "now": 程序会自动将开播时间定为点击提交的30秒后。\n'\ '2. 填入HHMM格式的四位数字,代表未来24小时内对应日本时区的时间。\n'\ '目前只能接受未来24小时内开始的直播。\n'\ '\n'\ '自定义标题可不填,不填时默认值为"YouTuber名+转播"。', 'table_titles': __default_table_title, 'row': [ {'input_type': 'text', 'name': 'time'}, {'input_type': 'select', 'name': 'liver', 'contains': livers}, {'input_type': 'select', 'name': 'site', 'contains': sites}, {'input_type': 'text', 'name': 'title'} ] } # 查看时间表中需要的值 schedule_jobs_value = { 'title': '已注册项目', 'descr': '时间表中预定要运行的项目。\n'\ '时间一栏为日本时区时间。\n'\ '每天会在本地时间下午15时在B站发送一次每日时间表动态,内容即为当时此时间表内的内容。\n'\ '表内的项目不一定都能转播成功,直播更改日期或是延迟20分钟以上都不能转播成功。', 'table_titles': __default_table_title, 'rows': rows } # 查看运行中项目所需要的值 running_jobs_value = { 'title': '运行中的项目', 'descr': '正在运行中的项目。\n'\ '其中的项目不一定都正在转播,有可能是直播开始前正在尝试开始转播,或是直播结束后正在尝试掉线重连。', 'jobs': jobs } sections = { 'add_job': add_job_value, 'schedule_jobs': schedule_jobs_value, 'running_jobs': running_jobs_value } return sections
def configs_sections(): '''返回设置页面所需数据 ''' config = CONFIGs() # BILIBILI_ROOM_AREA_ID所需的分区列表 b = login_bilibili(config.COOKIES_TXT_PATH) area_list_data = b.getLiveAreaList() area_list = [] for zone in area_list_data: for area in zone['list']: if area['lock_status'] == '0': area_list.append( {'value': int(area['id']), 'display': area['name']} ) # bilibili item_BILIBILI_ROOM_TITLE = { 'title': '直播间标题格式', 'input_type': 'text', 'name': 'BILIBILI_ROOM_TITLE', 'value': config.BILIBILI_ROOM_TITLE } item_DEFAULT_TITLE_PARAM = { 'title': '默认title参数格式', 'input_type': 'text', 'name': 'DEFAULT_TITLE_PARAM', 'value': config.DEFAULT_TITLE_PARAM } item_BILIBILI_ROOM_AREA_ID = { 'title': '直播间分区', 'input_type': 'select', 'name': 'BILIBILI_ROOM_AREA_ID', 'value': config.BILIBILI_ROOM_AREA_ID, 'options': area_list } item_IS_SEND_DAILY_DYNAMIC = { 'title': '发送每日转播列表动态', 'input_type': 'checkbox', 'name': 'IS_SEND_DAILY_DYNAMIC', 'value': config.IS_SEND_DAILY_DYNAMIC } item_DAILY_DYNAMIC_FORM = { 'title': '每日转播列表动态格式', 'input_type': 'text', 'name': 'DAILY_DYNAMIC_FORM', 'value': config.DAILY_DYNAMIC_FORM } item_IS_SEND_PRELIVE_DYNAMIC = { 'title': '转播前发送动态', 'input_type': 'checkbox', 'name': 'IS_SEND_PRELIVE_DYNAMIC', 'value': config.IS_SEND_PRELIVE_DYNAMIC } item_PRELIVE_DYNAMIC_FORM = { 'title': '转播前动态格式', 'input_type': 'text', 'name': 'PRELIVE_DYNAMIC_FORM', 'value': config.PRELIVE_DYNAMIC_FORM } section_bilibili = { 'title': 'BILIBILI', 'descr': 'B站直播间与动态相关设置。\n'\ '“直播间标题格式”中可使用参数: {time}, {liver}, {site}, {title}\n'\ '分别意义为:直播开始时间、主播、直播网站、添加直播项时填入的自定义标题。\n'\ '\n'\ '“默认title参数格式”中可使用参数:{time}, {liver}, {site}\n'\ '当添加直播项中“自定义标题”一栏为空时,会填入此处的默认title参数。\n'\ '\n'\ '如果勾选“发送每日转播列表动态”,会在每天本地时间15时发送一个B站动态。\n'\ '内容为当时时间表内所有的已注册项目,并且每个项目套用“每日转播列表动态格式”,可使用\\n转义。\n'\ '“每日转播列表动态格式”中可使用参数:{time}, {liver}, {site}, {title}\n'\ '如果当时已注册项目列表为空,则就算勾选了“发送每日转播列表动态”也不会发送动态。\n'\ '\n'\ '如果勾选“转播前发送动态”,则在直播前会发送一个B站动态。\n'\ '内容会套用“转播前动态格式”,并且会在转播任务结束后自动删除。\n'\ '“转播前动态格式”中可使用参数:{time}, {liver}, {site}, {title}, {url}\n'\ '其中url参数意义为直播间链接,其他四项与上面相同。', 'items': [ item_BILIBILI_ROOM_TITLE, item_DEFAULT_TITLE_PARAM, item_BILIBILI_ROOM_AREA_ID, item_IS_SEND_DAILY_DYNAMIC, item_DAILY_DYNAMIC_FORM, item_IS_SEND_PRELIVE_DYNAMIC, item_PRELIVE_DYNAMIC_FORM, ] } # liveParam item_LIVE_QUALITY = { 'title': '最高清晰度', 'input_type': 'select', 'name': 'LIVE_QUALITY', 'value': config.LIVE_QUALITY, 'options': [ {'value': 0, 'display': 0}, {'value': 240, 'display': 240}, {'value': 360, 'display': 360}, {'value': 480, 'display': 480}, {'value': 720, 'display': 720}, {'value': 1080, 'display': 1080}, ] } section_liveParam = { 'title': 'liveParam', 'descr': '清晰度等直播相关设定\n'\ '转播清晰度会尽量取最大,但不会超过“最高清晰度”,请根据自己服务器的网络情况来选取。\n'\ '如果“最高清晰度”设为0,即为不设清晰度上限,转播清晰度永远取最大值。', 'items': [ item_LIVE_QUALITY, ] } sections = { 'bilibili': section_bilibili, 'liveParam': section_liveParam, } return sections