mirror of
https://github.com/ihmily/DouyinLiveRecorder.git
synced 2026-03-22 07:28:24 +08:00
Add afreecatv live record and fix some bugs
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
- [x] 小红书
|
||||
- [x] bigo
|
||||
- [x] blued
|
||||
- [x] AfreecaTV
|
||||
- [ ] 更多平台正在更新中
|
||||
|
||||
</div>
|
||||
@@ -60,7 +61,7 @@
|
||||
- 当同时在录制多个直播时,最好线程数设置大一些,否则可能出现其中一个直播录制出错。当然设置的过大也没用,要同时考虑自身电脑的配置,如CPU内核数、网络带宽等限制。
|
||||
- 如果想直接使用打包好的录制软件,进入[Releases](https://github.com/ihmily/DouyinLiveRecorder/releases) 下载最新发布的 zip压缩包即可,有些电脑可能会报毒,直接忽略即可。
|
||||
- 如果要长时间挂着软件循环监测直播,最好循环时间设置长一点,避免因请求频繁导致被官方封禁IP 。
|
||||
- 最后,欢迎大家提交fork和PR
|
||||
- 最后,欢迎大家fork以及pr。
|
||||
|
||||
 
|
||||
|
||||
@@ -98,6 +99,9 @@ https://www.bigo.tv/cn/716418802
|
||||
|
||||
buled直播:
|
||||
https://app.blued.cn/live?id=Mp6G2R
|
||||
|
||||
AfreecaTV:
|
||||
https://play.afreecatv.com/sw7love/249471484
|
||||
```
|
||||
|
||||
Tiktok目前只支持PC网页端地址(我没下载app),其他平台 app端直播间分享地址和网页端长地址都能正常进行录制(抖音尽量用长链接,避免因短链接转换失效导致不能正常录制)。
|
||||
@@ -138,6 +142,9 @@ GET https://hmily.vip/api/jx/live/convert.php?url=https://v.douyin.com/iQLgKSj/
|
||||
|
||||
## ⏳提交日志
|
||||
|
||||
- 20231210
|
||||
- 新增AfreecaTV直播录制,修复某些可能会发生的bug
|
||||
|
||||
- 20231207
|
||||
- 新增blued直播录制,修复YY直播录制,新增直播结束消息推送
|
||||
|
||||
|
||||
@@ -32,3 +32,4 @@ B站cookie =
|
||||
小红书cookie =
|
||||
bigo_cookie =
|
||||
blued_cookie =
|
||||
afreecatv_cookie =
|
||||
|
||||
323
main.py
323
main.py
@@ -4,7 +4,7 @@
|
||||
Author: Hmily
|
||||
GitHub: https://github.com/ihmily
|
||||
Date: 2023-07-17 23:52:05
|
||||
Update: 2023-12-07 01:33:19
|
||||
Update: 2023-12-10 04:39:32
|
||||
Copyright (c) 2023 by Hmily, All Rights Reserved.
|
||||
Function: Record live stream video.
|
||||
"""
|
||||
@@ -34,7 +34,8 @@ from spider import (
|
||||
get_bilibili_stream_data,
|
||||
get_xhs_stream_url,
|
||||
get_bigo_stream_url,
|
||||
get_blued_stream_url
|
||||
get_blued_stream_url,
|
||||
get_afreecatv_stream_url
|
||||
)
|
||||
|
||||
from web_rid import (
|
||||
@@ -47,28 +48,26 @@ from utils import (
|
||||
)
|
||||
from msg_push import dingtalk, xizhi
|
||||
|
||||
# 版本号
|
||||
version = "v2.0.5"
|
||||
platforms = "抖音|Tiktok|快手|虎牙|斗鱼|YY|B站|小红书|bigo直播|blued直播"
|
||||
version = "v2.0.6"
|
||||
platforms = "抖音|Tiktok|快手|虎牙|斗鱼|YY|B站|小红书|bigo直播|blued直播|AfreecaTV"
|
||||
# --------------------------全局变量-------------------------------------
|
||||
recording = set()
|
||||
unrecording = set()
|
||||
warning_count = 0
|
||||
max_request = 0
|
||||
Monitoring = 0
|
||||
monitoring = 0
|
||||
runing_list = []
|
||||
url_tuples_list = []
|
||||
textNoRepeatUrl = []
|
||||
text_no_repeat_url = []
|
||||
create_var = locals()
|
||||
first_start = True
|
||||
name_list = []
|
||||
firstRunOtherLine = True
|
||||
first_run = True
|
||||
live_list = []
|
||||
not_record_list = []
|
||||
start5_time = datetime.datetime.now()
|
||||
start_display_time = datetime.datetime.now()
|
||||
global_proxy = False
|
||||
recording_time_list = {}
|
||||
user_agent = "Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36"
|
||||
config_file = './config/config.ini'
|
||||
url_config_file = './config/URL_config.ini'
|
||||
backup_dir = './backup_config'
|
||||
@@ -82,15 +81,15 @@ default_path = os.getcwd()
|
||||
|
||||
def display_info():
|
||||
# TODO: 显示当前录制配置信息
|
||||
global start5_time
|
||||
global start_display_time
|
||||
global recording_time_list
|
||||
time.sleep(5)
|
||||
while True:
|
||||
try:
|
||||
time.sleep(5)
|
||||
os.system("cls")
|
||||
print("\r共监测" + str(Monitoring) + "个直播中", end=" | ")
|
||||
print("同一时间访问网络的线程数:", max_request, end=" | ")
|
||||
print(f"\r共监测{monitoring}个直播中", end=" | ")
|
||||
print(f"同一时间访问网络的线程数: {max_request}", end=" | ")
|
||||
if len(video_save_path) > 0:
|
||||
if not os.path.exists(video_save_path):
|
||||
print("配置文件里,直播保存路径并不存在,请重新输入一个正确的路径.或留空表示当前目录,按回车退出")
|
||||
@@ -98,34 +97,34 @@ def display_info():
|
||||
sys.exit(0)
|
||||
|
||||
print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ")
|
||||
if Splitvideobysize:
|
||||
print(f"TS录制分段开启,录制分段大小为 {Splitsize} M", end=" | ")
|
||||
if split_video_by_size:
|
||||
print(f"TS录制分段开启,录制分段大小为 {split_size} M", end=" | ")
|
||||
print(f"是否生成时间文件: {'是' if create_time_file else '否'}", end=" | ")
|
||||
print("录制视频质量为: " + str(video_quality), end=" | ")
|
||||
print("录制视频格式为: " + str(video_save_type), end=" | ")
|
||||
print("目前瞬时错误数为: " + str(warning_count), end=" | ")
|
||||
nowdate = time.strftime("%H:%M:%S", time.localtime())
|
||||
print(f"当前时间: {nowdate}")
|
||||
print(f"录制视频质量为: {video_quality}", end=" | ")
|
||||
print(f"录制视频格式为: {video_save_type}", end=" | ")
|
||||
print(f"目前瞬时错误数为: {warning_count}", end=" | ")
|
||||
format_now_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
print(f"当前时间: {format_now_time}")
|
||||
|
||||
if len(recording) == 0 and len(unrecording) == 0:
|
||||
time.sleep(5)
|
||||
print("\r没有正在录制的直播 " + nowdate, end="")
|
||||
print(f"\r没有正在录制的直播 {format_now_time[-8:]}", end="")
|
||||
print("")
|
||||
continue
|
||||
else:
|
||||
now_time = datetime.datetime.now()
|
||||
if len(recording) > 0:
|
||||
print("x" * 60)
|
||||
NoRepeatrecording = list(set(recording))
|
||||
print(f"正在录制{len(NoRepeatrecording)}个直播: ")
|
||||
for recording_live in NoRepeatrecording:
|
||||
no_repeat_recording = list(set(recording))
|
||||
print(f"正在录制{len(no_repeat_recording)}个直播: ")
|
||||
for recording_live in no_repeat_recording:
|
||||
have_record_time = now_time - recording_time_list[recording_live]
|
||||
print(f"{recording_live} 正在录制中 " + str(have_record_time).split('.')[0])
|
||||
|
||||
# print('\n本软件已运行:'+str(now_time - start5_time).split('.')[0])
|
||||
# print('\n本软件已运行:'+str(now_time - start_display_time).split('.')[0])
|
||||
print("x" * 60)
|
||||
else:
|
||||
start5_time = now_time
|
||||
start_display_time = now_time
|
||||
except Exception as e:
|
||||
print(f"错误信息:{e}\r\n发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
@@ -151,7 +150,7 @@ def converts_mp4(address):
|
||||
"-c:a", "copy",
|
||||
"-f", "mp4", address.split('.')[0] + ".mp4",
|
||||
], stderr=subprocess.STDOUT)
|
||||
if delFilebeforeconversion:
|
||||
if delete_origin_file:
|
||||
time.sleep(1)
|
||||
if os.path.exists(address):
|
||||
os.remove(address)
|
||||
@@ -165,7 +164,7 @@ def converts_m4a(address):
|
||||
"-c:a", "aac", "-bsf:a", "aac_adtstoasc", "-ab", "320k",
|
||||
address.split('.')[0] + ".m4a",
|
||||
], stderr=subprocess.STDOUT)
|
||||
if delFilebeforeconversion:
|
||||
if delete_origin_file:
|
||||
time.sleep(1)
|
||||
if os.path.exists(address):
|
||||
os.remove(address)
|
||||
@@ -182,7 +181,7 @@ def create_ass_file(filegruop):
|
||||
|
||||
while True:
|
||||
index_time += 1
|
||||
txt = str(index_time) + "\n" + tranform_int_to_time(index_time) + ',000 --> ' + tranform_int_to_time(
|
||||
txt = str(index_time) + "\n" + transform_int_to_time(index_time) + ',000 --> ' + transform_int_to_time(
|
||||
index_time + 1) + ',000' + "\n" + str(re_datatime) + "\n"
|
||||
|
||||
with open(ass_filename + ".ass", 'a', encoding='utf8') as f:
|
||||
@@ -242,6 +241,8 @@ def change_max_connect():
|
||||
|
||||
@trace_error_decorator
|
||||
def get_douyin_stream_url(json_data):
|
||||
# TODO: 获取抖音直播源地址
|
||||
|
||||
anchor_name = json_data.get('anchor_name', None)
|
||||
|
||||
result = {
|
||||
@@ -283,6 +284,8 @@ def get_douyin_stream_url(json_data):
|
||||
|
||||
@trace_error_decorator
|
||||
def get_tiktok_stream_url(json_data):
|
||||
# TODO: 获取tiktok直播源地址
|
||||
|
||||
def get_video_quality_url(stream_data, quality_key):
|
||||
return {
|
||||
'hls': re.sub("https", "http", stream_data[quality_key]['main']['hls']),
|
||||
@@ -320,6 +323,8 @@ def get_tiktok_stream_url(json_data):
|
||||
|
||||
@trace_error_decorator
|
||||
def get_kuaishou_stream_url(json_data):
|
||||
# TODO: 获取快手直播源地址
|
||||
|
||||
if json_data['type'] == 1:
|
||||
return json_data
|
||||
live_status = json_data['is_live']
|
||||
@@ -360,6 +365,8 @@ def get_kuaishou_stream_url(json_data):
|
||||
|
||||
@trace_error_decorator
|
||||
def get_huya_stream_url(json_data):
|
||||
# TODO: 获取虎牙直播源地址
|
||||
|
||||
game_live_info = json_data.get('data', [])[0].get('gameLiveInfo', {})
|
||||
stream_info_list = json_data.get('data', [])[0].get('gameStreamInfoList', [])
|
||||
anchor_name = game_live_info.get('nick', '')
|
||||
@@ -413,8 +420,7 @@ def get_huya_stream_url(json_data):
|
||||
|
||||
@trace_error_decorator
|
||||
def get_douyu_stream_url(json_data, cookies):
|
||||
# 获取斗鱼直播源地址
|
||||
|
||||
# TODO: 获取斗鱼直播源地址
|
||||
video_quality_options = {
|
||||
"原画": '0',
|
||||
"蓝光": '0',
|
||||
@@ -529,12 +535,14 @@ def start_record(url_tuple, count_variable=-1):
|
||||
try:
|
||||
record_finished = False
|
||||
record_finished_2 = False
|
||||
Runonce = False
|
||||
run_once = False
|
||||
is_long_url = False
|
||||
no_error = True
|
||||
new_record_url = ''
|
||||
count_time = time.time()
|
||||
record_url = url_tuple[0]
|
||||
anchor_name = url_tuple[1]
|
||||
print("\r运行新线程,传入地址 " + record_url)
|
||||
print(f"\r运行新线程,传入地址 {record_url}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
@@ -543,7 +551,7 @@ def start_record(url_tuple, count_variable=-1):
|
||||
# 判断如果是浏览器长链接
|
||||
with semaphore:
|
||||
# 使用semaphore来控制同时访问资源的线程数量
|
||||
json_data = get_douyin_stream_data(record_url, dy_cookie) # 注意这里需要配置文件中的cookie
|
||||
json_data = get_douyin_stream_data(record_url, dy_cookie)
|
||||
port_info = get_douyin_stream_url(json_data)
|
||||
elif record_url.find("https://v.douyin.com/") > -1:
|
||||
# 判断如果是app分享链接
|
||||
@@ -602,6 +610,10 @@ def start_record(url_tuple, count_variable=-1):
|
||||
with semaphore:
|
||||
port_info = get_blued_stream_url(record_url, blued_cookie)
|
||||
|
||||
elif record_url.find("afreecatv.com/") > -1:
|
||||
with semaphore:
|
||||
port_info = get_afreecatv_stream_url(record_url, afreecatv_cookie)
|
||||
|
||||
anchor_name: str = port_info.get("anchor_name", '')
|
||||
|
||||
if not anchor_name:
|
||||
@@ -616,12 +628,12 @@ def start_record(url_tuple, count_variable=-1):
|
||||
name_list.append(f'{record_url}|#{record_url}')
|
||||
return
|
||||
|
||||
if url_tuple[1] == "" and Runonce is False:
|
||||
if url_tuple[1] == "" and run_once is False:
|
||||
if is_long_url:
|
||||
name_list.append(f'{record_url}|{new_record_url},主播: {anchor_name.strip()}')
|
||||
else:
|
||||
name_list.append(f'{record_url}|{record_url},主播: {anchor_name.strip()}')
|
||||
Runonce = True
|
||||
run_once = True
|
||||
|
||||
if port_info['is_live'] is False:
|
||||
print(f"{record_name} 等待直播... ")
|
||||
@@ -649,8 +661,9 @@ def start_record(url_tuple, count_variable=-1):
|
||||
if not os.path.exists(full_path):
|
||||
os.makedirs(full_path)
|
||||
else:
|
||||
full_path = './' + anchor_name
|
||||
if not os.path.exists(anchor_name):
|
||||
os.makedirs('./' + anchor_name)
|
||||
os.makedirs(full_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"路径错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
@@ -662,10 +675,13 @@ def start_record(url_tuple, count_variable=-1):
|
||||
logger.warning(
|
||||
"错误信息: 保存路径不存在,不能生成录制.请避免把本程序放在c盘,桌面,下载文件夹,qq默认传输目录.请重新检查设置")
|
||||
|
||||
user_agent = ("Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 ("
|
||||
"KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile "
|
||||
"Safari/537.36")
|
||||
ffmpeg_command = [
|
||||
ffmpeg_path, "-y",
|
||||
"-v", "verbose",
|
||||
"-rw_timeout", "10000000", # 10s
|
||||
"-rw_timeout", "15000000", # 15s
|
||||
"-loglevel", "error",
|
||||
"-hide_banner",
|
||||
"-user_agent", user_agent,
|
||||
@@ -685,11 +701,14 @@ def start_record(url_tuple, count_variable=-1):
|
||||
]
|
||||
|
||||
# 添加代理参数
|
||||
if 'tiktok' in real_url:
|
||||
if use_proxy and proxy_addr != '':
|
||||
# os.environ["http_proxy"] = proxy_addr
|
||||
ffmpeg_command.insert(1, "-http_proxy")
|
||||
ffmpeg_command.insert(2, proxy_addr)
|
||||
need_proxy_url = ['tiktok','afreecatv']
|
||||
for i in need_proxy_url:
|
||||
if i in real_url:
|
||||
if use_proxy and proxy_addr != '':
|
||||
# os.environ["http_proxy"] = proxy_addr
|
||||
ffmpeg_command.insert(1, "-http_proxy")
|
||||
ffmpeg_command.insert(2, proxy_addr)
|
||||
break
|
||||
|
||||
recording.add(record_name)
|
||||
start_record_time = datetime.datetime.now()
|
||||
@@ -713,20 +732,20 @@ def start_record(url_tuple, count_variable=-1):
|
||||
if flv_url:
|
||||
_filepath, _ = urllib.request.urlretrieve(real_url,
|
||||
full_path + '/' + filename)
|
||||
record_finished = True
|
||||
record_finished_2 = True
|
||||
count_time = time.time()
|
||||
|
||||
else:
|
||||
raise Exception('该直播无flv直播流,请切换视频保存类型')
|
||||
|
||||
except Exception as e:
|
||||
print(f"\r{time.strftime('%Y-%m-%d %H:%M:%S')} {anchor_name} 未开播")
|
||||
logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
warning_count += 1
|
||||
no_error = False
|
||||
|
||||
elif video_save_type == "MKV":
|
||||
filename = anchor_name + '_' + now + ".mkv"
|
||||
print(f'{rec_info}/{filename}')
|
||||
file = full_path + '/' + filename
|
||||
save_file_path = full_path + '/' + filename
|
||||
if create_time_file:
|
||||
filename_gruop = [anchor_name, filename_short]
|
||||
create_var[str(filename_short)] = threading.Thread(target=create_ass_file,
|
||||
@@ -739,25 +758,23 @@ def start_record(url_tuple, count_variable=-1):
|
||||
"-map", "0",
|
||||
"-c:v", "copy", # 直接用copy的话体积特别大.
|
||||
"-f", "matroska",
|
||||
"{path}".format(path=file),
|
||||
"{path}".format(path=save_file_path),
|
||||
]
|
||||
ffmpeg_command.extend(command)
|
||||
|
||||
_output = subprocess.check_output(ffmpeg_command, stderr=subprocess.STDOUT)
|
||||
|
||||
record_finished = True
|
||||
record_finished_2 = True
|
||||
count_time = time.time()
|
||||
except subprocess.CalledProcessError as e:
|
||||
# logging.warning(str(e.output))
|
||||
print(f"{e.output} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
warning_count += 1
|
||||
no_error = False
|
||||
|
||||
elif video_save_type == "MP4":
|
||||
|
||||
filename = anchor_name + '_' + now + ".mp4"
|
||||
print(f'{rec_info}/{filename}')
|
||||
file = full_path + '/' + filename
|
||||
save_file_path = full_path + '/' + filename
|
||||
|
||||
if create_time_file:
|
||||
filename_gruop = [anchor_name, filename_short]
|
||||
@@ -771,7 +788,7 @@ def start_record(url_tuple, count_variable=-1):
|
||||
"-map", "0",
|
||||
"-c:v", "copy", # 直接用copy的话体积特别大.
|
||||
"-f", "mp4",
|
||||
"{path}".format(path=file),
|
||||
"{path}".format(path=save_file_path),
|
||||
]
|
||||
ffmpeg_command.extend(command)
|
||||
_output = subprocess.check_output(ffmpeg_command, stderr=subprocess.STDOUT)
|
||||
@@ -780,73 +797,69 @@ def start_record(url_tuple, count_variable=-1):
|
||||
# if proxy_addr:
|
||||
# del os.environ["http_proxy"]
|
||||
|
||||
record_finished = True
|
||||
record_finished_2 = True
|
||||
count_time = time.time()
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
# logging.warning(str(e.output))
|
||||
print(f"{e.output} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
warning_count += 1
|
||||
no_error = False
|
||||
|
||||
elif video_save_type == "MKV音频":
|
||||
filename = anchor_name + '_' + now + ".mkv"
|
||||
print(f'{rec_info}/{filename}')
|
||||
file = full_path + '/' + filename
|
||||
save_file_path = full_path + '/' + filename
|
||||
|
||||
try:
|
||||
command = [
|
||||
"-map", "0:a", # 不同点
|
||||
"-f", "matroska", # 不同点
|
||||
"{path}".format(path=file),
|
||||
"-map", "0:a",
|
||||
"-f", "matroska",
|
||||
"{path}".format(path=save_file_path),
|
||||
]
|
||||
ffmpeg_command.extend(command)
|
||||
_output = subprocess.check_output(ffmpeg_command, stderr=subprocess.STDOUT)
|
||||
|
||||
record_finished = True
|
||||
record_finished_2 = True
|
||||
count_time = time.time()
|
||||
|
||||
if tsconvert_to_m4a:
|
||||
threading.Thread(target=converts_m4a, args=(file,)).start()
|
||||
except subprocess.CalledProcessError as e:
|
||||
# logging.warning(str(e.output))
|
||||
print(f"{e.output} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
warning_count += 1
|
||||
no_error = False
|
||||
|
||||
elif video_save_type == "TS音频":
|
||||
filename = anchor_name + '_' + now + ".ts"
|
||||
print(f'{rec_info}/{filename}')
|
||||
file = full_path + '/' + filename
|
||||
save_file_path = full_path + '/' + filename
|
||||
|
||||
try:
|
||||
command = [
|
||||
"-map", "0:a", # 不同点
|
||||
"-map", "0:a",
|
||||
"-f", "mpegts",
|
||||
"{path}".format(path=file),
|
||||
"{path}".format(path=save_file_path),
|
||||
]
|
||||
ffmpeg_command.extend(command)
|
||||
_output = subprocess.check_output(ffmpeg_command, stderr=subprocess.STDOUT)
|
||||
|
||||
record_finished = True
|
||||
record_finished_2 = True
|
||||
count_time = time.time()
|
||||
|
||||
if tsconvert_to_m4a:
|
||||
threading.Thread(target=converts_m4a, args=(file,)).start()
|
||||
except subprocess.CalledProcessError as e:
|
||||
# logging.warning(str(e.output))
|
||||
print(f"{e.output} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
warning_count += 1
|
||||
no_error = False
|
||||
|
||||
|
||||
|
||||
else:
|
||||
|
||||
if Splitvideobysize: # 这里默认是启用/不启用视频分割功能
|
||||
if split_video_by_size: # 这里默认是启用/不启用视频分割功能
|
||||
while True:
|
||||
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime(time.time()))
|
||||
filename = anchor_name + '_' + now + ".ts"
|
||||
print(f'{rec_info}/{filename}')
|
||||
file = full_path + '/' + filename
|
||||
save_file_path = full_path + '/' + filename
|
||||
|
||||
if create_time_file:
|
||||
filename_gruop = [anchor_name, filename_short]
|
||||
@@ -859,20 +872,16 @@ def start_record(url_tuple, count_variable=-1):
|
||||
try:
|
||||
command = [
|
||||
"-c:v", "copy",
|
||||
"-map", "0", # 不同点
|
||||
"-map", "0",
|
||||
"-f", "mpegts",
|
||||
"-fs", str(Splitsizes), # 不同点
|
||||
"{path}".format(path=file),
|
||||
"-fs", str(split_byte_sizes),
|
||||
"{path}".format(path=save_file_path),
|
||||
]
|
||||
|
||||
ffmpeg_command.extend(command)
|
||||
_output = subprocess.check_output(ffmpeg_command,
|
||||
stderr=subprocess.STDOUT)
|
||||
|
||||
record_finished = True # 这里表示正常录制成功一次
|
||||
record_finished_2 = True
|
||||
count_time = time.time()
|
||||
|
||||
if tsconvert_to_mp4:
|
||||
threading.Thread(target=converts_mp4, args=(file,)).start()
|
||||
if tsconvert_to_m4a:
|
||||
@@ -882,13 +891,15 @@ def start_record(url_tuple, count_variable=-1):
|
||||
logging.warning(str(e.output))
|
||||
logger.warning(
|
||||
f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
warning_count += 1
|
||||
no_error = False
|
||||
break
|
||||
|
||||
else:
|
||||
filename = anchor_name + '_' + now + ".ts"
|
||||
|
||||
print(f'{rec_info}/{filename}')
|
||||
file = full_path + '/' + filename
|
||||
save_file_path = full_path + '/' + filename
|
||||
|
||||
if create_time_file:
|
||||
filename_gruop = [anchor_name, filename_short]
|
||||
@@ -900,16 +911,13 @@ def start_record(url_tuple, count_variable=-1):
|
||||
try:
|
||||
command = [
|
||||
"-c:v", "copy",
|
||||
"-map", "0", # 不同点
|
||||
"-map", "0",
|
||||
"-f", "mpegts",
|
||||
"{path}".format(path=file),
|
||||
"{path}".format(path=save_file_path),
|
||||
]
|
||||
|
||||
ffmpeg_command.extend(command)
|
||||
_output = subprocess.check_output(ffmpeg_command,stderr=subprocess.STDOUT)
|
||||
record_finished = True
|
||||
record_finished_2 = True
|
||||
count_time = time.time()
|
||||
_output = subprocess.check_output(ffmpeg_command, stderr=subprocess.STDOUT)
|
||||
|
||||
if tsconvert_to_mp4:
|
||||
threading.Thread(target=converts_mp4, args=(file,)).start()
|
||||
@@ -920,13 +928,24 @@ def start_record(url_tuple, count_variable=-1):
|
||||
# logging.warning(str(e.output))
|
||||
print(f"{e.output} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
warning_count += 1
|
||||
no_error = False
|
||||
|
||||
record_finished = True
|
||||
record_finished_2 = True
|
||||
count_time = time.time()
|
||||
|
||||
if record_finished_2:
|
||||
if record_name in recording:
|
||||
recording.remove(record_name)
|
||||
if anchor_name in unrecording:
|
||||
unrecording.add(anchor_name)
|
||||
print(f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制完成\n")
|
||||
|
||||
if no_error:
|
||||
print(f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制完成\n")
|
||||
else:
|
||||
print(f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制出错,请检查网络\n")
|
||||
|
||||
record_finished_2 = False
|
||||
|
||||
# 推送通知
|
||||
@@ -939,7 +958,7 @@ def start_record(url_tuple, count_variable=-1):
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误信息:{e}\r\n读取的地址为: {record_url} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
logger.warning("错误信息: " + str(e) + " 发生错误的行数: " + str(e.__traceback__.tb_lineno))
|
||||
logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
warning_count += 1
|
||||
|
||||
num = random.randint(-5, 5) + delay_default # 生成-5到5的随机数,加上delay_default
|
||||
@@ -967,7 +986,7 @@ def start_record(url_tuple, count_variable=-1):
|
||||
while x:
|
||||
x = x - 1
|
||||
if loop_time:
|
||||
print('\r' + anchor_name + ' 循环等待%d秒 ' % x, end="")
|
||||
print(f'\r{anchor_name}循环等待{x}秒 ', end="")
|
||||
time.sleep(1)
|
||||
if loop_time:
|
||||
print('\r检测直播间中...', end="")
|
||||
@@ -981,18 +1000,39 @@ def start_record(url_tuple, count_variable=-1):
|
||||
|
||||
def backup_file(file_path, backup_dir):
|
||||
"""
|
||||
备份配置文件到备份目录
|
||||
备份配置文件到备份目录,分别保留最新 10 个文件
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(backup_dir):
|
||||
os.makedirs(backup_dir)
|
||||
# 拼接备份文件名,年-月-日-时-分-秒
|
||||
|
||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
||||
backup_file_name = os.path.basename(file_path) + '_' + timestamp
|
||||
# 拷贝文件到备份目录
|
||||
|
||||
backup_file_path = os.path.join(backup_dir, backup_file_name)
|
||||
shutil.copy2(file_path, backup_file_path)
|
||||
print(f'\r已备份配置文件 {file_path} 到 {backup_file_path}')
|
||||
|
||||
# 删除多余的备份文件
|
||||
files = os.listdir(backup_dir)
|
||||
url_files = [f for f in files if f.startswith('URL_config.ini')]
|
||||
config_files = [f for f in files if f.startswith('config.ini')]
|
||||
|
||||
url_files.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir, x)))
|
||||
config_files.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir, x)))
|
||||
|
||||
while len(url_files) > 5:
|
||||
oldest_file = url_files[0]
|
||||
os.remove(os.path.join(backup_dir, oldest_file))
|
||||
# print(f'\r已删除最旧的 URL_config.ini 备份文件 {oldest_file}')
|
||||
url_files = url_files[1:]
|
||||
|
||||
while len(config_files) > 5:
|
||||
oldest_file = config_files[0]
|
||||
os.remove(os.path.join(backup_dir, oldest_file))
|
||||
# print(f'\r已删除最旧的 config.ini 备份文件 {oldest_file}')
|
||||
config_files = config_files[1:]
|
||||
|
||||
except Exception as e:
|
||||
print(f'\r备份配置文件 {file_path} 失败:{str(e)}')
|
||||
|
||||
@@ -1020,7 +1060,7 @@ def backup_file_start():
|
||||
|
||||
|
||||
# --------------------------检测是否存在ffmpeg-------------------------------------
|
||||
ffmepg_file_check = subprocess.getoutput(["ffmpeg"])
|
||||
ffmepg_file_check = subprocess.getoutput("ffmpeg")
|
||||
if ffmepg_file_check.find("run") > -1:
|
||||
# print("ffmpeg存在")
|
||||
pass
|
||||
@@ -1050,8 +1090,8 @@ t3.start()
|
||||
# 但强烈建议还是配置一下代理地址,否则非常不稳定
|
||||
try:
|
||||
# 检测电脑是否开启了全局/规则代理
|
||||
print('系统代理检测中...')
|
||||
response_g = urllib.request.urlopen("https://www.tiktok.com", timeout=15)
|
||||
print('系统代理检测中,请耐心等待...')
|
||||
response_g = urllib.request.urlopen("https://www.google.com/", timeout=15)
|
||||
global_proxy = True
|
||||
print('系统代理已开启√ 注意:配置文件中的代理设置也要开启才会生效哦!')
|
||||
|
||||
@@ -1076,6 +1116,8 @@ def read_config_value(config, section, option, default_value):
|
||||
return default_value
|
||||
|
||||
|
||||
options = {"是": True, "否": False}
|
||||
|
||||
while True:
|
||||
# 循环读取配置
|
||||
config = configparser.RawConfigParser()
|
||||
@@ -1089,31 +1131,33 @@ while True:
|
||||
|
||||
if os.path.isfile(url_config_file):
|
||||
with open(url_config_file, 'r', encoding=encoding) as f:
|
||||
inicontent = f.read()
|
||||
ini_content = f.read()
|
||||
else:
|
||||
inicontent = ""
|
||||
ini_content = ""
|
||||
|
||||
if len(inicontent) == 0:
|
||||
inurl = input('请输入要录制的主播直播间网址(尽量使用PC网页端的直播间地址):\n')
|
||||
if len(ini_content) == 0:
|
||||
input_url = input('请输入要录制的主播直播间网址(尽量使用PC网页端的直播间地址):\n')
|
||||
with open(url_config_file, 'a+', encoding=encoding) as f:
|
||||
f.write(inurl)
|
||||
f.write(input_url)
|
||||
|
||||
video_save_path = read_config_value(config, '录制设置', '直播保存路径(不填则默认)', "")
|
||||
video_save_type = read_config_value(config, '录制设置', '视频保存格式TS|MKV|FLV|MP4|TS音频|MKV音频', "mp4")
|
||||
video_quality = read_config_value(config, '录制设置', '原画|超清|高清|标清', "原画")
|
||||
use_proxy = read_config_value(config, '录制设置', '是否使用代理ip(是/否)', "是")
|
||||
use_proxy = options.get(read_config_value(config, '录制设置', '是否使用代理ip(是/否)', "是"), False)
|
||||
proxy_addr = read_config_value(config, '录制设置', '代理地址', "")
|
||||
max_request = int(read_config_value(config, '录制设置', '同一时间访问网络的线程数', 3))
|
||||
semaphore = threading.Semaphore(max_request)
|
||||
delay_default = int(read_config_value(config, '录制设置', '循环时间(秒)', 60))
|
||||
local_delay_default = int(read_config_value(config, '录制设置', '排队读取网址时间(秒)', 0))
|
||||
loop_time = read_config_value(config, '录制设置', '是否显示循环秒数', "否")
|
||||
Splitvideobysize = read_config_value(config, '录制设置', 'TS格式分段录制是否开启', "否")
|
||||
Splitsize = int(read_config_value(config, '录制设置', '视频分段大小(兆)', '1000'))
|
||||
tsconvert_to_mp4 = read_config_value(config, '录制设置', 'TS录制完成后自动增加生成MP4格式', "否")
|
||||
tsconvert_to_m4a = read_config_value(config, '录制设置', 'TS录制完成后自动增加生成m4a格式', "否")
|
||||
delFilebeforeconversion = read_config_value(config, '录制设置', '追加格式后删除原文件', "否")
|
||||
create_time_file = read_config_value(config, '录制设置', '生成时间文件', "否")
|
||||
loop_time = options.get(read_config_value(config, '录制设置', '是否显示循环秒数', "否"), False)
|
||||
split_video_by_size = options.get(read_config_value(config, '录制设置', 'TS格式分段录制是否开启', "否"), False)
|
||||
split_size = int(read_config_value(config, '录制设置', '视频分段大小(兆)', '1000'))
|
||||
tsconvert_to_mp4 = options.get(read_config_value(config, '录制设置', 'TS录制完成后自动增加生成MP4格式', "否"),
|
||||
False)
|
||||
tsconvert_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)
|
||||
live_status_push = read_config_value(config, '推送配置', '直播状态通知(可选微信|钉钉或者两个都填)', "")
|
||||
dingtalk_api_url = read_config_value(config, '推送配置', '钉钉推送接口链接', "")
|
||||
xizhi_api_url = read_config_value(config, '推送配置', '微信推送接口链接', "")
|
||||
@@ -1128,6 +1172,7 @@ while True:
|
||||
xhs_cookie = read_config_value(config, 'Cookie', '小红书cookie', '')
|
||||
bigo_cookie = read_config_value(config, 'Cookie', 'bigo_cookie', '')
|
||||
blued_cookie = read_config_value(config, 'Cookie', 'blued_cookie', '')
|
||||
afreecatv_cookie = read_config_value(config, 'Cookie', 'afreecatv_cookie', '')
|
||||
|
||||
if len(video_save_type) > 0:
|
||||
if video_save_type.upper().lower() == "FLV".lower():
|
||||
@@ -1150,29 +1195,17 @@ while True:
|
||||
print("直播视频保存为TS格式")
|
||||
|
||||
# 这里是控制TS分段大小
|
||||
if Splitsize < 5:
|
||||
Splitsize = 5 # 分段大小最低不能小于5m
|
||||
Splitsizes = Splitsize * 1024 * 1024 # 分割视频大小,转换为字节
|
||||
if split_size < 5:
|
||||
split_size = 5 # 分段大小最低不能小于5m
|
||||
split_byte_sizes = split_size * 1024 * 1024 # 分割视频大小,转换为字节
|
||||
|
||||
|
||||
def tranform_int_to_time(seconds):
|
||||
def transform_int_to_time(seconds):
|
||||
m, s = divmod(seconds, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return ("%d:%02d:%02d" % (h, m, s))
|
||||
return f"{h}:{m:02d}:{s:02d}"
|
||||
|
||||
|
||||
options = {
|
||||
"是": True,
|
||||
"否": False
|
||||
}
|
||||
use_proxy = options.get(use_proxy, False) # 是否使用代理ip
|
||||
loop_time = options.get(loop_time, False) # 是否显示循环秒数
|
||||
Splitvideobysize = options.get(Splitvideobysize, False) # 这里是控制TS是否分段
|
||||
create_time_file = options.get(create_time_file, False) # 这里控制是否生成时间文件
|
||||
delFilebeforeconversion = options.get(delFilebeforeconversion, False) # 追加格式后,是否删除原文件
|
||||
tsconvert_to_m4a = options.get(tsconvert_to_m4a, False) # 这里是控制TS是否追加m4a格式
|
||||
tsconvert_to_mp4 = options.get(tsconvert_to_mp4, False) # 这里是控制TS是否追加mp4格式
|
||||
|
||||
# 读取url_config.ini文件
|
||||
try:
|
||||
with open(url_config_file, "r", encoding=encoding) as file:
|
||||
@@ -1202,7 +1235,9 @@ while True:
|
||||
'live.bilibili.com',
|
||||
'www.xiaohongshu.com',
|
||||
'www.bigo.tv',
|
||||
'app.blued.cn'
|
||||
'app.blued.cn',
|
||||
'play.afreecatv.com',
|
||||
'm.afreecatv.com'
|
||||
]
|
||||
if url_host in host_list:
|
||||
new_line = (url, split_line[1])
|
||||
@@ -1212,26 +1247,26 @@ while True:
|
||||
|
||||
while len(name_list):
|
||||
a = name_list.pop()
|
||||
replacewords = a.split('|')
|
||||
if replacewords[0] != replacewords[1]:
|
||||
update_file(url_config_file, replacewords[0], replacewords[1])
|
||||
replace_words = a.split('|')
|
||||
if replace_words[0] != replace_words[1]:
|
||||
update_file(url_config_file, replace_words[0], replace_words[1])
|
||||
|
||||
if len(url_tuples_list) > 0:
|
||||
textNoRepeatUrl = list(set(url_tuples_list))
|
||||
if len(textNoRepeatUrl) > 0:
|
||||
for url_tuple in textNoRepeatUrl:
|
||||
text_no_repeat_url = list(set(url_tuples_list))
|
||||
if len(text_no_repeat_url) > 0:
|
||||
for url_tuple in text_no_repeat_url:
|
||||
if url_tuple[0] in not_record_list:
|
||||
continue
|
||||
|
||||
if url_tuple[0] not in runing_list:
|
||||
if first_start == False:
|
||||
print("新增链接: " + url_tuple[0])
|
||||
Monitoring = Monitoring + 1
|
||||
args = [url_tuple, Monitoring]
|
||||
if not first_start:
|
||||
print(f"新增链接: {url_tuple[0]}")
|
||||
monitoring += 1
|
||||
args = [url_tuple, monitoring]
|
||||
# TODO: 执行开始录制的操作
|
||||
create_var['thread' + str(Monitoring)] = threading.Thread(target=start_record, args=args)
|
||||
create_var['thread' + str(Monitoring)].daemon = True
|
||||
create_var['thread' + str(Monitoring)].start()
|
||||
create_var['thread' + str(monitoring)] = threading.Thread(target=start_record, args=args)
|
||||
create_var['thread' + str(monitoring)].daemon = True
|
||||
create_var['thread' + str(monitoring)].start()
|
||||
runing_list.append(url_tuple[0])
|
||||
time.sleep(local_delay_default)
|
||||
url_tuples_list = []
|
||||
@@ -1241,12 +1276,12 @@ while True:
|
||||
print(f"错误信息:{e}\r\n发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||||
|
||||
if firstRunOtherLine:
|
||||
if first_run:
|
||||
t = threading.Thread(target=display_info, args=(), daemon=True)
|
||||
t.start()
|
||||
t2 = threading.Thread(target=change_max_connect, args=(), daemon=True)
|
||||
t2.start()
|
||||
|
||||
firstRunOtherLine = False
|
||||
first_run = False
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
144
spider.py
144
spider.py
@@ -4,7 +4,7 @@
|
||||
Author: Hmily
|
||||
GitHub:https://github.com/ihmily
|
||||
Date: 2023-07-15 23:15:00
|
||||
Update: 2023-12-07 23:35:47
|
||||
Update: 2023-12-10 01:01:12
|
||||
Copyright (c) 2023 by Hmily, All Rights Reserved.
|
||||
Function: Get live stream data.
|
||||
"""
|
||||
@@ -40,9 +40,9 @@ def get_douyin_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[s
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
response = opener.open(req, timeout=15)
|
||||
html_str = response.read().decode('utf-8')
|
||||
match_json_str = re.search(r'(\{\\\"state\\\"\:.*?)\]\\n\"\]\)', html_str)
|
||||
match_json_str = re.search(r'(\{\\"state\\":.*?)]\\n"]\)', html_str)
|
||||
if not match_json_str:
|
||||
match_json_str = re.search(r'(\{\\\"common\\\"\:.*?)\]\\n\"\]\)\<\/script\>\<div hidden', html_str)
|
||||
match_json_str = re.search(r'(\{\\"common\\":.*?)]\\n"]\)</script><div hidden', html_str)
|
||||
json_str = match_json_str.group(1)
|
||||
cleaned_string = json_str.replace('\\', '').replace(r'u0026', r'&')
|
||||
room_store = re.search('"roomStore":(.*?),"linkmicStore"', cleaned_string, re.S).group(1)
|
||||
@@ -56,8 +56,8 @@ def get_douyin_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[s
|
||||
print(f'失败地址:{url} 准备切换解析方法{e}')
|
||||
web_rid = re.match('https://live.douyin.com/(\d+)', url).group(1)
|
||||
headers['Cookie'] = 'sessionid=73d300f837f261eaa8ffc69d50162700'
|
||||
url = f'https://live.douyin.com/webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&web_rid={web_rid}'
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
url2 = f'https://live.douyin.com/webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&web_rid={web_rid}'
|
||||
req = urllib.request.Request(url2, headers=headers)
|
||||
response = opener.open(req, timeout=15)
|
||||
json_str = response.read().decode('utf-8')
|
||||
json_data = json.loads(json_str)['data']
|
||||
@@ -68,7 +68,7 @@ def get_douyin_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[s
|
||||
|
||||
@trace_error_decorator
|
||||
def get_tiktok_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> Dict[
|
||||
str, Any]:
|
||||
str, Any]:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.79',
|
||||
'Cookie': 'ttwid=1%7CM-rF193sJugKuNz2RGNt-rh6pAAR9IMceUSzlDnPCNI%7C1683274418%7Cf726d4947f2fc37fecc7aeb0cdaee52892244d04efde6f8a8edd2bb168263269; tiktok_webapp_theme=light; tt_chain_token=VWkygAWDlm1cFg/k8whmOg==; passport_csrf_token=6e422c5a7991f8cec7033a8082921510; passport_csrf_token_default=6e422c5a7991f8cec7033a8082921510; d_ticket=f8c267d4af4523c97be1ccb355e9991e2ae06; odin_tt=320b5f386cdc23f347be018e588873db7f7aea4ea5d1813681c3fbc018ea025dde957b94f74146dbc0e3612426b865ccb95ec8abe4ee36cca65f15dbffec0deff7b0e69e8ea536d46e0f82a4fc37d211; cmpl_token=AgQQAPNSF-RO0rT04baWtZ0T_jUjl4fVP4PZYM2QPw; uid_tt=319b558dbba684bb1557206c92089cd113a875526a89aee30595925d804b81c7; uid_tt_ss=319b558dbba684bb1557206c92089cd113a875526a89aee30595925d804b81c7; sid_tt=ad5e736f4bedb2f6d42ccd849e706b1d; sessionid=ad5e736f4bedb2f6d42ccd849e706b1d; sessionid_ss=ad5e736f4bedb2f6d42ccd849e706b1d; store-idc=useast5; store-country-code=us; store-country-code-src=uid; tt-target-idc=useast5; tt-target-idc-sign=qXNk0bb1pDQ0FbCNF120Pl9WWMLZg9Edv5PkfyCbS4lIk5ieW5tfLP7XWROnN0mEaSlc5hg6Oji1pF-yz_3ZXnUiNMrA9wNMPvI6D9IFKKVmq555aQzwPIGHv0aQC5dNRgKo5Z5LBkgxUMWEojTKclq2_L8lBciw0IGdhFm_XyVJtbqbBKKgybGDLzK8ZyxF4Jl_cYRXaDlshZjc38JdS6wruDueRSHe7YvNbjxCnApEFUv-OwJANSPU_4rvcqpVhq3JI2VCCfw-cs_4MFIPCDOKisk5EhAo2JlHh3VF7_CLuv80FXg_7ZqQ2pJeMOog294rqxwbbQhl3ATvjQV_JsWyUsMd9zwqecpylrPvtySI2u1qfoggx1owLrrUynee1R48QlanLQnTNW_z1WpmZBgVJqgEGLwFoVOmRzJuFFNj8vIqdjM2nDSdWqX8_wX3wplohkzkPSFPfZgjzGnQX28krhgTytLt7BXYty5dpfGtsdb11WOFHM6MZ9R9uLVB; sid_guard=ad5e736f4bedb2f6d42ccd849e706b1d%7C1690990657%7C15525213%7CMon%2C+29-Jan-2024+08%3A11%3A10+GMT; sid_ucp_v1=1.0.0-KGM3YzgwYjZhODgyYWI1NjIwNTA0NjBmOWUxMGRhMjIzYTI2YjMxNDUKGAiqiJ30keKD5WQQwfCppgYYsws4AkDsBxAEGgd1c2Vhc3Q1IiBhZDVlNzM2ZjRiZWRiMmY2ZDQyY2NkODQ5ZTcwNmIxZA; ssid_ucp_v1=1.0.0-KGM3YzgwYjZhODgyYWI1NjIwNTA0NjBmOWUxMGRhMjIzYTI2YjMxNDUKGAiqiJ30keKD5WQQwfCppgYYsws4AkDsBxAEGgd1c2Vhc3Q1IiBhZDVlNzM2ZjRiZWRiMmY2ZDQyY2NkODQ5ZTcwNmIxZA; tt_csrf_token=dD0EIH8q-pe3qDQsCyyD1jLN6KizJDRjOEyk; __tea_cache_tokens_1988={%22_type_%22:%22default%22%2C%22user_unique_id%22:%227229608516049831425%22%2C%22timestamp%22:1683274422659}; ttwid=1%7CM-rF193sJugKuNz2RGNt-rh6pAAR9IMceUSzlDnPCNI%7C1694002151%7Cd89b77afc809b1a610661a9d1c2784d80ebef9efdd166f06de0d28e27f7e4efe; msToken=KfJAVZ7r9D_QVeQlYAUZzDFbc1Yx-nZz6GF33eOxgd8KlqvTg1lF9bMXW7gFV-qW4MCgUwnBIhbiwU9kdaSpgHJCk-PABsHCtTO5J3qC4oCTsrXQ1_E0XtbqiE4OVLZ_jdF1EYWgKNPT2SnwGkQ=; msToken=KfJAVZ7r9D_QVeQlYAUZzDFbc1Yx-nZz6GF33eOxgd8KlqvTg1lF9bMXW7gFV-qW4MCgUwnBIhbiwU9kdaSpgHJCk-PABsHCtTO5J3qC4oCTsrXQ1_E0XtbqiE4OVLZ_jdF1EYWgKNPT2SnwGkQ='
|
||||
@@ -77,6 +77,7 @@ def get_tiktok_stream_data(url: str, proxy_addr: Union[str, None] = None, cookie
|
||||
headers['Cookie'] = cookies
|
||||
|
||||
if proxy_addr:
|
||||
|
||||
proxies = {
|
||||
'http': proxy_addr,
|
||||
'https': proxy_addr
|
||||
@@ -86,12 +87,12 @@ def get_tiktok_stream_data(url: str, proxy_addr: Union[str, None] = None, cookie
|
||||
html_str = html.text
|
||||
|
||||
else:
|
||||
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
response = urllib.request.urlopen(req, timeout=15)
|
||||
html_str = response.read().decode('utf-8')
|
||||
|
||||
json_str = re.findall(
|
||||
'<script id="SIGI_STATE" type="application/json">(.*?)<\/script><script id="SIGI_RETRY" type="application\/json">',
|
||||
'<script id="SIGI_STATE" type="application/json">(.*?)</script><script id="SIGI_RETRY" type="application/json">',
|
||||
html_str)[0]
|
||||
json_data = json.loads(json_str)
|
||||
return json_data
|
||||
@@ -149,8 +150,8 @@ def get_kuaishou_stream_data2(url: str, cookies: Union[str, None] = None) -> Dic
|
||||
eid = url.split('/u/')[1].strip()
|
||||
data = {"source": 5, "eid": eid, "shareMethod": "card", "clientType": "WEB_OUTSIDE_SHARE_H5"}
|
||||
data_encoded = json.dumps(data).encode('utf-8')
|
||||
url2 = 'https://livev.m.chenzhongtech.com/rest/k/live/byUser?kpn=GAME_ZONE&captchaToken='
|
||||
req = urllib.request.Request(url2, headers=headers, data=data_encoded)
|
||||
app_api = 'https://livev.m.chenzhongtech.com/rest/k/live/byUser?kpn=GAME_ZONE&captchaToken='
|
||||
req = urllib.request.Request(app_api, headers=headers, data=data_encoded)
|
||||
response = urllib.request.urlopen(req)
|
||||
json_str = response.read().decode('utf-8')
|
||||
json_data = json.loads(json_str)
|
||||
@@ -243,12 +244,12 @@ def get_douyu_info_data(url: str) -> Dict[str, Any]:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
}
|
||||
url = f'https://m.douyu.com/{rid}'
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
url2 = f'https://m.douyu.com/{rid}'
|
||||
req = urllib.request.Request(url2, headers=headers)
|
||||
response = opener.open(req, timeout=15)
|
||||
|
||||
html_str = response.read().decode('utf-8')
|
||||
json_str = re.search('\<script id\=\"vike_pageContext\" type\=\"application\/json\"\>(.*?)<\/script>',
|
||||
json_str = re.search('<script id="vike_pageContext" type="application/json">(.*?)</script>',
|
||||
html_str).group(1)
|
||||
json_data = json.loads(json_str)
|
||||
return json_data
|
||||
@@ -288,7 +289,6 @@ def get_douyu_stream_data(rid: str, rate: str = '-1', cookies: Union[str, None]
|
||||
|
||||
@trace_error_decorator
|
||||
def get_yy_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[str, Any]:
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
|
||||
@@ -346,8 +346,8 @@ def get_xhs_stream_url(url: str, cookies: Union[str, None] = None) -> Dict[str,
|
||||
|
||||
room_id = url.split('?')[0].rsplit('/', maxsplit=1)[1]
|
||||
appuid = re.search('appuid=(.*?)&', url).group(1)
|
||||
url = f'https://www.xiaohongshu.com/api/sns/red/live/app/v1/ecology/outside/share_info?room_id={room_id}'
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
app_api = f'https://www.xiaohongshu.com/api/sns/red/live/app/v1/ecology/outside/share_info?room_id={room_id}'
|
||||
req = urllib.request.Request(app_api, headers=headers)
|
||||
response = opener.open(req, timeout=15)
|
||||
json_str = response.read().decode('utf-8')
|
||||
json_data = json.loads(json_str)
|
||||
@@ -380,9 +380,9 @@ def get_bigo_stream_url(url: str, cookies: Union[str, None] = None) -> Dict[str,
|
||||
|
||||
room_id = re.search('www.bigo.tv/cn/(\d+)', url).group(1)
|
||||
data = {'siteId': room_id} # roomId
|
||||
url = 'https://ta.bigo.tv/official_website/studio/getInternalStudioInfo'
|
||||
url2 = 'https://ta.bigo.tv/official_website/studio/getInternalStudioInfo'
|
||||
data = urllib.parse.urlencode(data).encode('utf-8')
|
||||
req = urllib.request.Request(url, data=data, headers=headers)
|
||||
req = urllib.request.Request(url2, data=data, headers=headers)
|
||||
response = opener.open(req, timeout=15)
|
||||
json_str = response.read().decode('utf-8')
|
||||
json_data = json.loads(json_str)
|
||||
@@ -414,7 +414,7 @@ def get_blued_stream_url(url: str, cookies: Union[str, None] = None) -> Dict[str
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
response = opener.open(req, timeout=15)
|
||||
html_str = response.read().decode('utf-8')
|
||||
json_str = re.search('decodeURIComponent\(\"(.*?)\"\)\)\,window\.Promise', html_str, re.S).group(1)
|
||||
json_str = re.search('decodeURIComponent\(\"(.*?)\"\)\),window\.Promise', html_str, re.S).group(1)
|
||||
json_str = urllib.parse.unquote(json_str)
|
||||
json_data = json.loads(json_str)
|
||||
anchor_name = json_data['userInfo']['name']
|
||||
@@ -432,9 +432,111 @@ def get_blued_stream_url(url: str, cookies: Union[str, None] = None) -> Dict[str
|
||||
return result
|
||||
|
||||
|
||||
def get_afreecatv_cdn_url(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> Dict[
|
||||
str, Any]:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
|
||||
'Referer': 'https://play.afreecatv.com/oul282/249469582',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
if cookies:
|
||||
headers['Cookie'] = cookies
|
||||
|
||||
broad_no = url.split('/')[-1]
|
||||
|
||||
params = {
|
||||
'return_type': 'gcp_cdn',
|
||||
'use_cors': 'false',
|
||||
'cors_origin_url': 'play.afreecatv.com',
|
||||
'broad_key': f'{broad_no}-common-master-hls',
|
||||
'time': '8361.086329376785',
|
||||
}
|
||||
|
||||
url2 = 'http://livestream-manager.afreecatv.com/broad_stream_assign.html?' + urllib.parse.urlencode(params)
|
||||
|
||||
if proxy_addr:
|
||||
|
||||
proxies = {
|
||||
'http': proxy_addr,
|
||||
'https': proxy_addr
|
||||
}
|
||||
|
||||
response = requests.get(url2, headers=headers, proxies=proxies, timeout=15)
|
||||
json_data = response.json()
|
||||
|
||||
else:
|
||||
|
||||
req = urllib.request.Request(url2, headers=headers)
|
||||
response = urllib.request.urlopen(req, timeout=15)
|
||||
json_str = response.read().decode('utf-8')
|
||||
json_data = json.loads(json_str)
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
@trace_error_decorator
|
||||
def get_afreecatv_stream_url(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> Dict[
|
||||
str, Any]:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
|
||||
'Referer': 'https://m.afreecatv.com/',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
if cookies:
|
||||
headers['Cookie'] = cookies
|
||||
|
||||
bj_id, broad_no = url.split('/')[-2:]
|
||||
|
||||
data = {
|
||||
'bj_id': bj_id,
|
||||
'broad_no': broad_no,
|
||||
'agent': 'web',
|
||||
'confirm_adult': 'true',
|
||||
'player_type': 'webm',
|
||||
'mode': 'live',
|
||||
}
|
||||
|
||||
url2 = 'http://api.m.afreecatv.com/broad/a/watch'
|
||||
|
||||
if proxy_addr:
|
||||
proxies = {
|
||||
'http': proxy_addr,
|
||||
'https': proxy_addr
|
||||
}
|
||||
response = requests.post(url2, data=data, headers=headers, proxies=proxies, timeout=15)
|
||||
json_data = response.json()
|
||||
|
||||
else:
|
||||
|
||||
data = urllib.parse.urlencode(data).encode('utf-8')
|
||||
req = urllib.request.Request(url2, data=data, headers=headers)
|
||||
response = urllib.request.urlopen(req, timeout=15)
|
||||
json_str = response.read().decode('utf-8')
|
||||
json_data = json.loads(json_str)
|
||||
|
||||
anchor_name = json_data['data']['user_nick']
|
||||
result = {
|
||||
"anchor_name": anchor_name,
|
||||
"is_live": False,
|
||||
}
|
||||
|
||||
if json_data['result'] == 1:
|
||||
hls_authentication_key = json_data['data']['hls_authentication_key']
|
||||
view_url = get_afreecatv_cdn_url(url, proxy_addr=proxy_addr)['view_url']
|
||||
m3u8_url = view_url + '?aid=' + hls_authentication_key
|
||||
result['m3u8_url'] = m3u8_url
|
||||
result['is_live'] = True
|
||||
result['record_url'] = m3u8_url
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# 尽量用自己的cookie,以避免默认的不可用导致无法获取数据
|
||||
# 以下示例链接不保证时效性,请自行查看链接是否能正常访问
|
||||
|
||||
url = "https://live.douyin.com/745964462470" # 抖音直播
|
||||
# url = "https://www.tiktok.com/@pearlgaga88/live" # Tiktok直播
|
||||
# url = "https://live.kuaishou.com/u/yall1102" # 快手直播
|
||||
@@ -447,6 +549,8 @@ if __name__ == '__main__':
|
||||
# url = 'https://www.xiaohongshu.com/hina/livestream/568980065082002402?appuid=5f3f478a00000000010005b3&apptime='
|
||||
# url = 'https://www.bigo.tv/cn/716418802' # bigo直播
|
||||
# url = 'https://app.blued.cn/live?id=Mp6G2R' # blued直播
|
||||
# url = 'https://play.afreecatv.com/sw7love/249471484' # afreecatv直播
|
||||
# url = 'https://m.afreecatv.com/#/player/ayoona/249489885' # afreecatv直播
|
||||
|
||||
print(get_douyin_stream_data(url))
|
||||
# print(get_tiktok_stream_data(url,proxy_addr=''))
|
||||
@@ -459,3 +563,5 @@ if __name__ == '__main__':
|
||||
# print(get_xhs_stream_url(url))
|
||||
# print(get_bigo_stream_url(url))
|
||||
# print(get_blued_stream_url(url))
|
||||
# print(get_afreecatv_cdn_url(url,proxy_addr=''))
|
||||
# print(get_afreecatv_stream_url(url, proxy_addr=''))
|
||||
|
||||
Reference in New Issue
Block a user