From 01a9094e6aff77357861f0467d8ee09130cbc5e7 Mon Sep 17 00:00:00 2001 From: ihmily <961532186@qq.com> Date: Mon, 11 Mar 2024 04:38:55 +0800 Subject: [PATCH] fix: optimize the recording for huya and overseas platforms --- README.md | 9 ++++- main.py | 110 ++++++++++++++++++++++++++++++++++++++++++------------ spider.py | 52 ++++++++++++++++++++------ 3 files changed, 135 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index fc3f5c42..2adb2796 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ - 压缩包解压后,在 `config` 文件夹内的 `URL_config.ini` 中添加录制直播间地址,一行一个直播间地址。如果要自定义配置录制,可以修改`config.ini` 文件,推荐将录制格式修改为`ts`。 - 以上步骤都做好后,就可以运行`DouyinLiveRecorder.exe` 程序进行录制了。录制的视频文件保存在同目录下的 `downloads` 文件夹内。 -- 另外,如果需要录制TikTok、AfreecaTV等海外平台,请在配置文件中设置开启代理并添加proxy_addr链接 如:`http://127.0.0.1:7890` (这只是示例地址,具体根据实际填写)。 +- 另外,如果需要录制TikTok、AfreecaTV等海外平台,请在配置文件中设置开启代理并添加proxy_addr链接 如:`127.0.0.1:7890` (这只是示例地址,具体根据实际填写)。 - 假如`URL_config.ini`文件中添加的直播间地址,有个别直播间暂时不想录制又不想移除链接,可以在对应直播间的链接开头加上`#`,那么下次启动软件录制时将跳过该直播间。 @@ -280,6 +280,11 @@ docker-compose stop ## ⏳提交日志 +- 20240311 + - 修复海外平台录制bug,增加画质选择,增强录制稳定性 + + - 修复虎牙录制bug (虎牙`一起看`频道 有特殊限制,有时无法录制) + - 20240309 - 修复虎牙直播、小红书直播和B站直播录制 - 新增5个直播平台录制,包括winktv、flextv、look、popkontv、twitcasting @@ -287,7 +292,7 @@ docker-compose stop - 新增自定义配置需要使用代理录制的平台 - 新增只推送开播消息不进行录制设置 - 修复了一些bug - + - 20240209 - 优化AfreecaTV录制,新增账号密码登录获取cookie以及持久保存 - 修复了小红书直播因官方更新直播域名,导致无法录制直播的问题 diff --git a/main.py b/main.py index 571c0b23..288f1200 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ Author: Hmily GitHub: https://github.com/ihmily Date: 2023-07-17 23:52:05 -Update: 2024-03-09 03:25:39 +Update: 2024-03-11 00:42:11 Copyright (c) 2023-2024 by Hmily, All Rights Reserved. Function: Record live stream video. """ @@ -40,7 +40,7 @@ get_xhs_stream_url, get_bigo_stream_url, get_blued_stream_url, - get_afreecatv_stream_url, + get_afreecatv_stream_data, get_netease_stream_data, get_qiandurebo_stream_data, get_pandatv_stream_data, @@ -444,8 +444,7 @@ def get_anti_code(old_anti_code): anti_code = ( f'wsSecret={ws_secret_md5}&wsTime={ws_time}&seqid={seq_id}&ctype={url_query["ctype"][0]}&ver=1' f'&fs={url_query["fs"][0]}&uuid={init_uuid}&u={uid}&t={params_t}&sv={sdk_version}' - f'&sphdcdn={url_query["sphdcdn"][0]}&sphdDC={url_query["sphdDC"][0]}&sphd={url_query["sphd"][0]}' - f'&exsphd={url_query["exsphd"][0]}&sdk_sid={sdk_sid}&codec=264' + f'&sdk_sid={sdk_sid}&codec=264' ) return anti_code @@ -593,6 +592,27 @@ def get_url(m, n): return result +@trace_error_decorator +def get_afreecatv_stream_url(json_data: dict, video_quality: str) -> dict: + if not json_data['is_live']: + return json_data + + play_url_list = json_data['play_url_list'] + quality_list = {'原画': 0, '蓝光': 0, '超清': 1, '高清': 2, '标清': 3} + while len(play_url_list) < 4: + play_url_list.append(play_url_list[-1]) + + selected_quality = quality_list[video_quality] + m3u8_url = play_url_list[selected_quality] + + return { + "anchor_name": json_data['anchor_name'], + "is_live": True, + "m3u8_url": json_data['m3u8_url'], + "record_url": m3u8_url + } + + @trace_error_decorator def get_netease_stream_url(json_data: dict, video_quality: str) -> dict: if not json_data['is_live']: @@ -615,25 +635,66 @@ def get_netease_stream_url(json_data: dict, video_quality: str) -> dict: } +@trace_error_decorator +def get_pandatv_stream_url(json_data: dict, video_quality: str) -> dict: + if not json_data['is_live']: + return json_data + + play_url_list = json_data['play_url_list'] + quality_list = {'原画': 0, '蓝光': 0, '超清': 1, '高清': 2, '标清': 3} + while len(play_url_list) < 4: + play_url_list.append(play_url_list[-1]) + + selected_quality = quality_list[video_quality] + m3u8_url = play_url_list[selected_quality] + + return { + "anchor_name": json_data['anchor_name'], + "is_live": True, + "m3u8_url": json_data['m3u8_url'], + "record_url": m3u8_url + } + + @trace_error_decorator def get_winktv_stream_url(json_data: dict, video_quality: str) -> dict: if not json_data['is_live']: return json_data - quality_length = len(json_data['play_url_list']) - quality_list = {'原画': 'hls', '蓝光': 'hls', '超清': 'hls2', '高清': 'hls3', '标清': 'hls4'} - for i in json_data['play_url_list']: - if i in 'hls' and i not in list(quality_list.values()): - json_data['play_url_list'][i] = json_data['play_url_list'][quality_length - 1] + play_url_list = json_data['play_url_list'] + quality_list = {'原画': 0, '蓝光': 0, '超清': 1, '高清': 2, '标清': 3} + while len(play_url_list) < 4: + play_url_list.append(play_url_list[-1]) selected_quality = quality_list[video_quality] - flv_url = json_data['play_url_list'][selected_quality][0]['url'] + m3u8_url = play_url_list[selected_quality] return { + "anchor_name": json_data['anchor_name'], "is_live": True, + "m3u8_url": json_data['m3u8_url'], + "record_url": m3u8_url + } + + +@trace_error_decorator +def get_flextv_stream_url(json_data: dict, video_quality: str) -> dict: + if not json_data['is_live']: + return json_data + + play_url_list = json_data['play_url_list'] + quality_list = {'原画': 0, '蓝光': 0, '超清': 1, '高清': 2, '标清': 3} + while len(play_url_list) < 4: + play_url_list.append(play_url_list[-1]) + + selected_quality = quality_list[video_quality] + m3u8_url = play_url_list[selected_quality] + + return { "anchor_name": json_data['anchor_name'], - "flv_url": flv_url, - "record_url": flv_url + "is_live": True, + "m3u8_url": json_data['m3u8_url'], + "record_url": m3u8_url } @@ -645,7 +706,7 @@ def push_message(content: str): if '钉钉' in live_status_push: push_pts.append('钉钉') dingtalk(dingtalk_api_url, content, dingtalk_phone_num) - if 'TG' in live_status_push: + if 'TG' in live_status_push or 'tg' in live_status_push: push_pts.append('TG') tg_bot(tg_chat_id, tg_token, content) push_pts = '、'.join(push_pts) if len(push_pts) > 0 else '' @@ -795,12 +856,13 @@ def start_record(url_data: tuple, count_variable: int = -1): platform = 'AfreecaTV' with semaphore: if global_proxy or proxy_address: - port_info = get_afreecatv_stream_url( + json_data = get_afreecatv_stream_data( url=record_url, proxy_addr=proxy_address, cookies=afreecatv_cookie, username=afreecatv_username, password=afreecatv_password ) + port_info = get_afreecatv_stream_url(json_data, record_quality) else: logger.warning(f"错误信息: 网络异常,请检查本网络是否能正常访问AfreecaTV平台") @@ -820,11 +882,12 @@ def start_record(url_data: tuple, count_variable: int = -1): platform = 'PandaTV' with semaphore: if global_proxy or proxy_address: - port_info = get_pandatv_stream_data( + json_data = get_pandatv_stream_data( url=record_url, proxy_addr=proxy_address, cookies=pandatv_cookie ) + port_info = get_pandatv_stream_url(json_data, record_quality) else: logger.warning(f"错误信息: 网络异常,请检查本网络是否能正常访问PandaTV直播平台") @@ -850,13 +913,14 @@ def start_record(url_data: tuple, count_variable: int = -1): platform = 'FlexTV' with semaphore: if global_proxy or proxy_address: - port_info = get_flextv_stream_data( + json_data = get_flextv_stream_data( url=record_url, proxy_addr=proxy_address, cookies=flextv_cookie, username=flextv_username, password=flextv_password ) + port_info = get_flextv_stream_url(json_data, record_quality) else: logger.warning(f"错误信息: 网络异常,请检查本网络是否能正常访问FlexTV直播平台") @@ -1471,7 +1535,7 @@ def read_config_value(config_parser: configparser.RawConfigParser, section: str, file.write(input_url) video_save_path = read_config_value(config, '录制设置', '直播保存路径(不填则默认)', "") - video_save_type = read_config_value(config, '录制设置', '视频保存格式TS|MKV|FLV|MP4|TS音频|MKV音频', "ts") + video_save_type = read_config_value(config, '录制设置', '视频保存格式ts|mkv|flv|mp4|ts音频|mkv音频', "ts") video_record_quality = read_config_value(config, '录制设置', '原画|超清|高清|标清', "原画") use_proxy = options.get(read_config_value(config, '录制设置', '是否使用代理ip(是/否)', "是"), False) proxy_addr_bak = read_config_value(config, '录制设置', '代理地址', "") @@ -1483,9 +1547,9 @@ def read_config_value(config_parser: configparser.RawConfigParser, section: str, loop_time = options.get(read_config_value(config, '录制设置', '是否显示循环秒数', "否"), False) split_video_by_time = options.get(read_config_value(config, '录制设置', '分段录制是否开启', "否"), False) split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 1800)) - ts_to_mp4 = options.get(read_config_value(config, '录制设置', 'TS录制完成后自动转为mp4格式', "否"), + ts_to_mp4 = options.get(read_config_value(config, '录制设置', 'ts录制完成后自动转为mp4格式', "否"), False) - ts_to_m4a = options.get(read_config_value(config, '录制设置', 'TS录制完成后自动增加生成m4a格式', "否"), + ts_to_m4a = options.get(read_config_value(config, '录制设置', 'ts录制完成后自动增加生成m4a格式', "否"), False) delete_origin_file = options.get(read_config_value(config, '录制设置', '追加格式后删除原文件', "否"), False) create_time_file = options.get(read_config_value(config, '录制设置', '生成时间文件', "否"), False) @@ -1494,12 +1558,12 @@ def read_config_value(config_parser: configparser.RawConfigParser, section: str, enable_proxy_platform_list = enable_proxy_platform.replace(',', ',').split(',') if enable_proxy_platform else None extra_enable_proxy = read_config_value(config, '录制设置', '额外使用代理录制的平台(逗号分隔)', '') extra_enable_proxy_platform_list = extra_enable_proxy.replace(',', ',').split(',') if extra_enable_proxy else None - live_status_push = read_config_value(config, '推送配置', '直播状态通知(可选微信|钉钉|TG或者都填)', "") + live_status_push = read_config_value(config, '推送配置', '直播状态通知(可选微信|钉钉|tg或者都填)', "") dingtalk_api_url = read_config_value(config, '推送配置', '钉钉推送接口链接', "") xizhi_api_url = read_config_value(config, '推送配置', '微信推送接口链接', "") dingtalk_phone_num = read_config_value(config, '推送配置', '钉钉通知@对象(填手机号)', "") - tg_token = read_config_value(config, '推送配置', 'TGAPI令牌', "") - tg_chat_id = read_config_value(config, '推送配置', 'TG聊天ID(个人或者群组ID)', "") + tg_token = read_config_value(config, '推送配置', 'tgapi令牌', "") + tg_chat_id = read_config_value(config, '推送配置', 'tg聊天id(个人或者群组id)', "") disable_record = options.get(read_config_value(config, '推送配置', '只推送通知不录制(是/否)', "否"), False) push_check_seconds = int(read_config_value(config, '推送配置', '直播推送检测频率(秒)', 1800)) afreecatv_username = read_config_value(config, '账号密码', 'afreecatv账号', '') @@ -1526,7 +1590,7 @@ def read_config_value(config_parser: configparser.RawConfigParser, section: str, netease_cookie = read_config_value(config, 'Cookie', 'netease_cookie', '') qiandurebo_cookie = read_config_value(config, 'Cookie', '千度热播_cookie', '') pandatv_cookie = read_config_value(config, 'Cookie', 'pandatv_cookie', '') - maoerfm_cookie = read_config_value(config, 'Cookie', '猫耳FM_cookie', '') + maoerfm_cookie = read_config_value(config, 'Cookie', '猫耳fm_cookie', '') winktv_cookie = read_config_value(config, 'Cookie', 'winktv_cookie', '') flextv_cookie = read_config_value(config, 'Cookie', 'flextv_cookie', '') look_cookie = read_config_value(config, 'Cookie', 'look_cookie', '') diff --git a/spider.py b/spider.py index e924eecc..33777a7b 100644 --- a/spider.py +++ b/spider.py @@ -4,7 +4,7 @@ Author: Hmily GitHub:https://github.com/ihmily Date: 2023-07-15 23:15:00 -Update: 2024-03-09 01:39:17 +Update: 2024-03-11 03:37:59 Copyright (c) 2023 by Hmily, All Rights Reserved. Function: Get live stream data. """ @@ -83,6 +83,19 @@ def get_partner_code(url, params): return None +def get_play_url_list(m3u8: str, proxy: Union[str, None] = None, header: Union[dict, None] = None) -> list: + resp = get_req(url=m3u8, proxy_addr=proxy, headers=header, abroad=True) + play_url_list = [] + for i in resp.split('\n'): + if i.startswith('https://'): + play_url_list.append(i.strip()) + bandwidth_pattern = re.compile(r'BANDWIDTH=(\d+)') + bandwidth_list = bandwidth_pattern.findall(resp) + url_to_bandwidth = {url: int(bandwidth) for bandwidth, url in zip(bandwidth_list, play_url_list)} + play_url_list = sorted(play_url_list, key=lambda url: url_to_bandwidth[url], reverse=True) + return play_url_list + + @trace_error_decorator def get_douyin_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ Dict[str, Any]: @@ -597,7 +610,7 @@ def get_afreecatv_tk(url: str, rtype: str, proxy_addr: Union[str, None] = None, @trace_error_decorator -def get_afreecatv_stream_url( +def get_afreecatv_stream_data( url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None, username: Union[str, None] = None, password: Union[str, None] = None ) -> Dict[str, Any]: @@ -638,6 +651,19 @@ def get_afreecatv_stream_url( "is_live": False, } + def get_url_list(m3u8: str) -> list: + resp = get_req(url=m3u8, proxy_addr=proxy_addr, headers=headers, abroad=True) + play_url_list = [] + url_prefix = m3u8.rsplit('/', maxsplit=1)[0] + '/' + for i in resp.split('\n'): + if i.startswith('auth_playlist'): + play_url_list.append(url_prefix + i.strip()) + bandwidth_pattern = re.compile(r'BANDWIDTH=(\d+)') + bandwidth_list = bandwidth_pattern.findall(resp) + url_to_bandwidth = {url: int(bandwidth) for bandwidth, url in zip(bandwidth_list, play_url_list)} + play_url_list = sorted(play_url_list, key=lambda url: url_to_bandwidth[url], reverse=True) + return play_url_list + if not anchor_name: def handle_login(): cookie = login_afreecatv(username, password, proxy_addr=proxy_addr) @@ -653,7 +679,7 @@ def fetch_data(cookie): result['anchor_name'] = anchor_name result['m3u8_url'] = m3u8_url result['is_live'] = True - result['record_url'] = m3u8_url + result['play_url_list'] = get_url_list(m3u8_url) return result if json_data['data']['code'] == -3001: @@ -685,7 +711,7 @@ def fetch_data(cookie): m3u8_url = view_url + '?aid=' + hls_authentication_key result['m3u8_url'] = m3u8_url result['is_live'] = True - result['record_url'] = m3u8_url + result['play_url_list'] = get_url_list(m3u8_url) return result @@ -782,13 +808,14 @@ def get_pandatv_stream_data(url: str, proxy_addr: Union[str, None] = None, cooki anchor_name = f"{json_data['bjInfo']['nick']}-{anchor_id}" result['anchor_name'] = anchor_name live_status = 'media' in json_data + if live_status: json_str = get_req(url2, proxy_addr=proxy_addr, headers=headers, data=data2, abroad=True) json_data = json.loads(json_str) play_url = json_data['PlayList']['hls'][0]['url'] result['m3u8_url'] = play_url result['is_live'] = True - result['record_url'] = play_url + result['play_url_list'] = get_play_url_list(m3u8=play_url, proxy=proxy_addr, header=headers) return result @@ -888,7 +915,9 @@ def get_winktv_stream_data(url: str, proxy_addr: Union[str, None] = None, cookie play_api = 'https://api.winktv.co.kr/v1/live/play' json_str = get_req(url=play_api, proxy_addr=proxy_addr, headers=headers, data=data, abroad=True) json_data = json.loads(json_str) - result['play_url_list'] = json_data['PlayList'] + m3u8_url = json_data['PlayList']['hls'][0]['url'] + result['m3u8_url'] = m3u8_url + result['play_url_list'] = get_play_url_list(m3u8=m3u8_url, proxy=proxy_addr, header=headers) return result @@ -942,7 +971,7 @@ def login_flextv(username: str, password: str, proxy_addr: Union[str, None] = No def get_flextv_stream_url( url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None, username: Union[str, None] = None, password: Union[str, None] = None -) -> Dict[str, Any]: +) -> Union[str, Any]: def fetch_data(cookie): headers = { 'accept': 'application/json, text/plain, */*', @@ -1010,7 +1039,7 @@ def get_flextv_stream_data( url=url, proxy_addr=proxy_addr, cookies=cookies, username=username, password=password) if play_url: result['m3u8_url'] = play_url - result['record_url'] = play_url + result['play_url_list'] = get_play_url_list(m3u8=play_url, proxy=proxy_addr, header=headers) except Exception as e: print('FlexTV直播间数据获取失败', e) return result @@ -1354,7 +1383,8 @@ def get_data(header): html_str = get_req(url, proxy_addr=proxy_addr, headers=header) anchor = re.search("(.*?)\(@(.*?)\) 's Live - TwitCasting",html_str) status = re.search('data-is-onlive="(.*?)"\n\s+data-view-mode', html_str) - return f'{anchor.group(1).strip()}-{anchor.group(2)}', status.group(1) + movie_id = re.search('data-movie-id="(.*?)" data-audience-id', html_str) + return f'{anchor.group(1).strip()}-{anchor.group(2)}-{movie_id.group(1)}', status.group(1) result = {"anchor_name": '', "is_live": False} @@ -1421,7 +1451,7 @@ def get_data(header): # print(get_xhs_stream_url(room_url, proxy_addr='')) # print(get_bigo_stream_url(room_url, proxy_addr='')) # print(get_blued_stream_url(room_url, proxy_addr='')) - # print(get_afreecatv_stream_url(room_url, proxy_addr='', username='', password='')) + # print(get_afreecatv_stream_data(room_url, proxy_addr='', username='', password='')) # print(get_netease_stream_data(room_url, proxy_addr='')) # print(get_qiandurebo_stream_data(room_url, proxy_addr='')) # print(get_pandatv_stream_data(room_url, proxy_addr='')) @@ -1430,4 +1460,4 @@ def get_data(header): # print(get_flextv_stream_data(room_url,proxy_addr='', username='', password='')) # print(get_looklive_stream_url(room_url, proxy_addr='')) # print(get_popkontv_stream_url(room_url, proxy_addr='', username='', password='')) - # print(get_twitcasting_stream_url(room_url, proxy_addr='', username='', password='')) + # print(get_twitcasting_stream_url(room_url, proxy_addr='', username='', password='')) \ No newline at end of file