Ejemplo n.º 1
0
 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
Ejemplo n.º 2
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('程序结束')
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
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)
Ejemplo n.º 7
0
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']))
Ejemplo n.º 8
0
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())
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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