This commit is contained in:
eric
2025-09-27 10:56:56 +08:00
parent 32df67b284
commit da57df6a93

114
main.py
View File

@@ -171,7 +171,7 @@ def yt_single_coordinator(record_quality, proxy_address):
- 轮询 URL_config.ini找“第一条在播”的源
- 找到就启动 ffmpeg 推到同一条 YouTube live
- 源断/下播 -> 立刻切到下一路在播;
- 没有任何在播源 -> 不在面板里显示录制项(保持空)。
- 没有任何在播源 -> 不在面板里显示直播项(保持空)。
"""
key = (_yt_get("youtube", "key", fallback="", cast=str) or "").strip()
if not key:
@@ -437,7 +437,7 @@ def display_info() -> None:
stat_parts = []
stat_parts.append(f"监测中: {monitoring}")
stat_parts.append(f"并发线程: {max_request}")
stat_parts.append(f"代理录制: {'' if use_proxy else ''}")
stat_parts.append(f"代理直播: {'' if use_proxy else ''}")
stat_parts.append(f"分段: {f'{split_time}s' if split_video_by_time else ''}")
if create_time_file:
stat_parts.append("时间字幕: 是")
@@ -454,14 +454,14 @@ def display_info() -> None:
if monitoring == 0:
print("暂无监测任务。")
else:
print(f"暂无正在录制的直播。下次检测间隔:{delay_default}")
print(f"暂无正在直播的直播。下次检测间隔:{delay_default}")
else:
no_repeat = list(set(recording))
print(f"正在录制{len(no_repeat)}")
print(f"正在直播{len(no_repeat)}")
print(_hr("·"))
# 标题行
print(f"{'序号/主播名':<28} {'清晰度':<6} {'开始时间':<19} {'录制':<10}")
print(f"{'序号/主播名':<28} {'清晰度':<6} {'开始时间':<19} {'直播':<10}")
print(_hr("·"))
for name in sorted(no_repeat):
@@ -743,7 +743,7 @@ def clear_record_info(record_name: str, record_url: str) -> None:
if record_url in url_comments and record_url in running_list:
running_list.remove(record_url)
monitoring -= 1
color_obj.print_colored(f"[{record_name}]已经从录制列表中移除\n", color_obj.YELLOW)
color_obj.print_colored(f"[{record_name}]已经从直播列表中移除\n", color_obj.YELLOW)
def direct_download_stream(source_url: str, save_path: str, record_name: str, live_url: str, platform: str) -> bool:
@@ -768,7 +768,7 @@ def direct_download_stream(source_url: str, save_path: str, record_name: str, li
for chunk in response.iter_bytes(chunk_size):
if live_url in url_comments or exit_recording:
color_obj.print_colored(f"[{record_name}]录制时已被注释或请求停止,下载中断", color_obj.YELLOW)
color_obj.print_colored(f"[{record_name}]直播时已被注释或请求停止,下载中断", color_obj.YELLOW)
clear_record_info(record_name, live_url)
return False
@@ -800,7 +800,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa
while process.poll() is None:
if record_url in url_comments or exit_recording:
color_obj.print_colored(f"[{record_name}]录制时已被注释,本条线程将会退出", color_obj.YELLOW)
color_obj.print_colored(f"[{record_name}]直播时已被注释,本条线程将会退出", color_obj.YELLOW)
clear_record_info(record_name, record_url)
# process.terminate()
if os.name == 'nt':
@@ -825,7 +825,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa
threading.Thread(target=converts_mp4, args=(path, delete_origin_file)).start()
else:
threading.Thread(target=converts_mp4, args=(save_file_path, delete_origin_file)).start()
print(f"\n{record_name} {stop_time} 直播录制完成\n")
print(f"\n{record_name} {stop_time} 直播直播完成\n")
if script_command:
logger.debug("开始执行脚本命令!")
@@ -850,7 +850,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa
logger.debug("脚本命令执行结束!")
else:
color_obj.print_colored(f"\n{record_name} {stop_time} 直播录制出错,返回码: {return_code}\n", color_obj.RED)
color_obj.print_colored(f"\n{record_name} {stop_time} 直播直播出错,返回码: {return_code}\n", color_obj.RED)
recording.discard(record_name)
return False
@@ -972,7 +972,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
elif record_url.find(".m3u8") > -1 or record_url.find(".flv") > -1:
platform = '自定义录制直播'
platform = '自定义直播直播'
port_info = {
"anchor_name": platform + '_' + str(uuid.uuid4())[:8],
"is_live": True,
@@ -1094,7 +1094,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
except Exception as e:
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
if platform != '自定义录制直播':
if platform != '自定义直播直播':
if enable_https_recording and real_url.startswith("http://"):
real_url = real_url.replace("http://", "https://")
@@ -1154,7 +1154,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
recording.add(record_name)
start_record_time = datetime.datetime.now()
recording_time_list[record_name] = [start_record_time, record_quality_zh]
rec_info = f"\r{anchor_name} 准备开始录制视频: {full_path}"
rec_info = f"\r{anchor_name} 准备开始直播视频: {full_path}"
if show_url:
re_plat = ('WinkTV', 'PandaTV', 'ShowRoom', 'CHZZK', 'Youtube')
if platform in re_plat:
@@ -1167,7 +1167,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
only_flv_record = False
only_flv_platform_list = ['shopee', '花椒直播']
if platform in only_flv_platform_list:
logger.debug(f"提示: {platform} 将强制使用FLV格式录制")
logger.debug(f"提示: {platform} 将强制使用FLV格式直播")
only_flv_record = True
only_audio_record = False
@@ -1192,7 +1192,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
f"{name_format}.{extension}")
if split_video_by_time:
print(f'\r{anchor_name} 准备开始录制音频: {save_file_path}')
print(f'\r{anchor_name} 准备开始直播音频: {save_file_path}')
if "MP3" in record_save_type:
command = [
@@ -1281,22 +1281,22 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
if download_success:
record_finished = True
print(f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制完成\n")
print(f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播直播完成\n")
recording.discard(record_name)
else:
logger.debug("未找到FLV直播流跳过录制")
logger.debug("未找到FLV直播流跳过直播")
except Exception as e:
clear_record_info(record_name, record_url)
color_obj.print_colored(
f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制出错,请检查网络\n",
f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播直播出错,请检查网络\n",
color_obj.RED)
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
with max_request_lock:
error_count += 1
error_window.append(1)
elif record_save_type == "MP4录制":
elif record_save_type == "MP4直播":
print('---mp4------------')
filename = anchor_name + f'_{title_in_name}' + now + ".mp4"
print(f'{rec_info}/{filename}')
@@ -1664,7 +1664,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
x = x + 60
color_obj.print_colored("\r瞬时错误太多,延迟加60秒", color_obj.YELLOW)
# 这里是.如果录制结束后,循环时间会暂时变成30s后检测一遍. 这样一定程度上防止主播卡顿造成少录
# 这里是.如果直播结束后,循环时间会暂时变成30s后检测一遍. 这样一定程度上防止主播卡顿造成少录
# 当30秒过后检测一遍后. 会回归正常设置的循环秒数
if record_finished:
count_time_end = time.time() - count_time
@@ -1766,7 +1766,7 @@ print("GitHub: https://github.com/ihmily/DouyinLiveRecorder")
print(f'支持平台: {platforms}')
print('.....................................................')
if not check_ffmpeg_existence():
logger.error("缺少ffmpeg无法进行录制,程序退出")
logger.error("缺少ffmpeg无法进行直播,程序退出")
sys.exit(1)
os.makedirs(os.path.dirname(config_file), exist_ok=True)
t3 = threading.Thread(target=backup_file_start, args=(), daemon=True)
@@ -1779,8 +1779,8 @@ def read_config_value(config_parser: configparser.RawConfigParser, section: str,
try:
config_parser.read(config_file, encoding=text_encoding)
if '录制设置' not in config_parser.sections():
config_parser.add_section('录制设置')
if '直播设置' not in config_parser.sections():
config_parser.add_section('直播设置')
if '推送配置' not in config_parser.sections():
config_parser.add_section('推送配置')
if 'Cookie' not in config_parser.sections():
@@ -1799,8 +1799,8 @@ def read_config_value(config_parser: configparser.RawConfigParser, section: str,
options = {"": True, "": False}
config = configparser.RawConfigParser()
language = read_config_value(config, '录制设置', 'language(zh_cn/en)', "zh_cn")
skip_proxy_check = options.get(read_config_value(config, '录制设置', '是否跳过代理检测(是/否)', ""), False)
language = read_config_value(config, '直播设置', 'language(zh_cn/en)', "zh_cn")
skip_proxy_check = options.get(read_config_value(config, '直播设置', '是否跳过代理检测(是/否)', ""), False)
if language and 'en' not in language.lower():
from i18n import translated_print
@@ -1821,7 +1821,7 @@ try:
except HTTPError as err:
print(f"HTTP error occurred: {err.code} - {err.reason}")
except URLError:
color_obj.print_colored("INFO未检测到全局/规则网络代理,请检查代理配置(若无需录制海外直播请忽略此条提示)",
color_obj.print_colored("INFO未检测到全局/规则网络代理,请检查代理配置(若无需直播海外直播请忽略此条提示)",
color_obj.YELLOW)
except Exception as err:
print("An unexpected error occurred:", err)
@@ -1839,45 +1839,45 @@ while True:
ini_URL_content = file.read().strip()
if not ini_URL_content.strip():
input_url = input('请输入要录制的主播直播间网址尽量使用PC网页端的直播间地址:\n')
input_url = input('请输入要直播的主播直播间网址尽量使用PC网页端的直播间地址:\n')
with open(url_config_file, 'w', encoding=text_encoding) as file:
file.write(input_url)
except OSError as err:
logger.error(f"发生 I/O 错误: {err}")
video_save_path = read_config_value(config, '录制设置', '直播保存路径(不填则默认)', "")
folder_by_author = options.get(read_config_value(config, '录制设置', '保存文件夹是否以作者区分', ""), False)
folder_by_time = options.get(read_config_value(config, '录制设置', '保存文件夹是否以时间区分', ""), False)
folder_by_title = options.get(read_config_value(config, '录制设置', '保存文件夹是否以标题区分', ""), False)
filename_by_title = options.get(read_config_value(config, '录制设置', '保存文件名是否包含标题', ""), False)
clean_emoji = options.get(read_config_value(config, '录制设置', '是否去除名称中的表情符号', ""), True)
video_save_type = read_config_value(config, '录制设置', '视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频', "mp4")
video_record_quality = read_config_value(config, '录制设置', '原画|超清|高清|标清|流畅', "原画")
use_proxy = options.get(read_config_value(config, '录制设置', '是否使用代理ip(是/否)', ""), False)
proxy_addr_bak = read_config_value(config, '录制设置', '代理地址', "")
video_save_path = read_config_value(config, '直播设置', '直播保存路径(不填则默认)', "")
folder_by_author = options.get(read_config_value(config, '直播设置', '保存文件夹是否以作者区分', ""), False)
folder_by_time = options.get(read_config_value(config, '直播设置', '保存文件夹是否以时间区分', ""), False)
folder_by_title = options.get(read_config_value(config, '直播设置', '保存文件夹是否以标题区分', ""), False)
filename_by_title = options.get(read_config_value(config, '直播设置', '保存文件名是否包含标题', ""), False)
clean_emoji = options.get(read_config_value(config, '直播设置', '是否去除名称中的表情符号', ""), True)
video_save_type = read_config_value(config, '直播设置', '视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频', "mp4")
video_record_quality = read_config_value(config, '直播设置', '原画|超清|高清|标清|流畅', "原画")
use_proxy = options.get(read_config_value(config, '直播设置', '是否使用代理ip(是/否)', ""), False)
proxy_addr_bak = read_config_value(config, '直播设置', '代理地址', "")
proxy_addr = None if not use_proxy else proxy_addr_bak
max_request = int(read_config_value(config, '录制设置', '同一时间访问网络的线程数', 3))
max_request = int(read_config_value(config, '直播设置', '同一时间访问网络的线程数', 3))
semaphore = threading.Semaphore(max_request)
delay_default = int(read_config_value(config, '录制设置', '循环时间(秒)', 120))
local_delay_default = int(read_config_value(config, '录制设置', '排队读取网址时间(秒)', 0))
loop_time = options.get(read_config_value(config, '录制设置', '是否显示循环秒数', ""), False)
show_url = options.get(read_config_value(config, '录制设置', '是否显示直播源地址', ""), False)
split_video_by_time = options.get(read_config_value(config, '录制设置', '分段录制是否开启', ""), False)
enable_https_recording = options.get(read_config_value(config, '录制设置', '是否强制启用https录制', ""), False)
disk_space_limit = float(read_config_value(config, '录制设置', '录制空间剩余阈值(gb)', 1.0))
split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 1800))
converts_to_mp4 = options.get(read_config_value(config, '录制设置', '录制完成后自动转为mp4格式', ""), False)
converts_to_h264 = options.get(read_config_value(config, '录制设置', 'mp4格式重新编码为h264', ""), False)
delete_origin_file = options.get(read_config_value(config, '录制设置', '追加格式后删除原文件', ""), False)
create_time_file = options.get(read_config_value(config, '录制设置', '生成时间字幕文件', ""), False)
is_run_script = options.get(read_config_value(config, '录制设置', '是否录制完成后执行自定义脚本', ""), False)
custom_script = read_config_value(config, '录制设置', '自定义脚本执行命令', "") if is_run_script else None
delay_default = int(read_config_value(config, '直播设置', '循环时间(秒)', 120))
local_delay_default = int(read_config_value(config, '直播设置', '排队读取网址时间(秒)', 0))
loop_time = options.get(read_config_value(config, '直播设置', '是否显示循环秒数', ""), False)
show_url = options.get(read_config_value(config, '直播设置', '是否显示直播源地址', ""), False)
split_video_by_time = options.get(read_config_value(config, '直播设置', '分段直播是否开启', ""), False)
enable_https_recording = options.get(read_config_value(config, '直播设置', '是否强制启用https直播', ""), False)
disk_space_limit = float(read_config_value(config, '直播设置', '直播空间剩余阈值(gb)', 1.0))
split_time = str(read_config_value(config, '直播设置', '视频分段时间(秒)', 1800))
converts_to_mp4 = options.get(read_config_value(config, '直播设置', '直播完成后自动转为mp4格式', ""), False)
converts_to_h264 = options.get(read_config_value(config, '直播设置', 'mp4格式重新编码为h264', ""), False)
delete_origin_file = options.get(read_config_value(config, '直播设置', '追加格式后删除原文件', ""), False)
create_time_file = options.get(read_config_value(config, '直播设置', '生成时间字幕文件', ""), False)
is_run_script = options.get(read_config_value(config, '直播设置', '是否直播完成后执行自定义脚本', ""), False)
custom_script = read_config_value(config, '直播设置', '自定义脚本执行命令', "") if is_run_script else None
enable_proxy_platform = read_config_value(
config, '录制设置', '使用代理录制的平台(逗号分隔)',
config, '直播设置', '使用代理直播的平台(逗号分隔)',
'tiktok, soop, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu, faceit'
)
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 = 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, '推送配置', '直播状态推送渠道', "")
dingtalk_api_url = read_config_value(config, '推送配置', '钉钉推送接口链接', "")
@@ -1904,7 +1904,7 @@ while True:
push_message_title = read_config_value(config, '推送配置', '自定义推送标题', "直播间状态更新通知")
begin_push_message_text = read_config_value(config, '推送配置', '自定义开播推送内容', "")
over_push_message_text = read_config_value(config, '推送配置', '自定义关播推送内容', "")
disable_record = options.get(read_config_value(config, '推送配置', '只推送通知不录制(是/否)', ""), False)
disable_record = options.get(read_config_value(config, '推送配置', '只推送通知不直播(是/否)', ""), False)
push_check_seconds = int(read_config_value(config, '推送配置', '直播推送检测频率(秒)', 1800))
begin_show_push = options.get(read_config_value(config, '推送配置', '开播推送开启(是/否)', ""), True)
over_show_push = options.get(read_config_value(config, '推送配置', '关播推送开启(是/否)', ""), False)
@@ -2171,7 +2171,7 @@ while True:
# 保序去重(文件顺序 = 备源优先级)
text_no_repeat_url = list(dict.fromkeys(url_tuples_list))
# 监测数量显示为“URL 条数”(哪怕不启动录制线程)
# 监测数量显示为“URL 条数”(哪怕不启动直播线程)
monitoring = len(text_no_repeat_url)
if yt_single_mode and video_save_type.upper() == "MP4":
@@ -2186,7 +2186,7 @@ while True:
t_yt.start()
yt_thread_started = True
else:
# ===== 传统模式:按原先逻辑为每个 URL 启动录制(本地落地 / 其他保存格式)=====
# ===== 传统模式:按原先逻辑为每个 URL 启动直播(本地落地 / 其他保存格式)=====
if len(text_no_repeat_url) > 0:
for url_tuple in text_no_repeat_url:
if url_tuple[1] in not_record_list: