From 1e9a0759e19da59ce7c9846297311a4e1c17e5ad Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 27 Sep 2025 11:07:42 +0800 Subject: [PATCH] 's' --- config/URL_config.ini | 2 +- config/config.ini | 195 ++++++++++++++++++++++++------------------ main.py | 72 ++++++++++++++-- 3 files changed, 178 insertions(+), 91 deletions(-) diff --git a/config/URL_config.ini b/config/URL_config.ini index f434396..a62e73b 100644 --- a/config/URL_config.ini +++ b/config/URL_config.ini @@ -1,4 +1,4 @@ https://live.douyin.com/956616126418 https://live.douyin.com/754546227599 https://live.douyin.com/382919014818 -https://live.douyin.com/591442402624 +https://live.douyin.com/19153084007 diff --git a/config/config.ini b/config/config.ini index cd02bba..2891c61 100644 --- a/config/config.ini +++ b/config/config.ini @@ -30,102 +30,131 @@ mp4格式重新编码为h264 = 否 额外使用代理录制的平台(逗号分隔) = [推送配置] -# 可选微信|钉钉|tg|邮箱|bark|ntfy|pushplus 可填多个 -直播状态推送渠道 = -钉钉推送接口链接 = -微信推送接口链接 = -bark推送接口链接 = +直播状态推送渠道 = +钉钉推送接口链接 = +微信推送接口链接 = +bark推送接口链接 = bark推送中断级别 = active -bark推送铃声 = -钉钉通知@对象(填手机号) = +bark推送铃声 = +钉钉通知@对象(填手机号) = 钉钉通知@全体(是/否) = 否 -tgapi令牌 = -tg聊天id(个人或者群组id) = -smtp邮件服务器 = -是否使用SMTP服务SSL加密(是/否) = -SMTP邮件服务器端口 = -邮箱登录账号 = -发件人密码(授权码) = -发件人邮箱 = -发件人显示昵称 = -收件人邮箱 = +tgapi令牌 = +tg聊天id(个人或者群组id) = +smtp邮件服务器 = +是否使用smtp服务ssl加密(是/否) = +smtp邮件服务器端口 = +邮箱登录账号 = +发件人密码(授权码) = +发件人邮箱 = +发件人显示昵称 = +收件人邮箱 = ntfy推送地址 = https://ntfy.sh/xxxx ntfy推送标签 = tada -ntfy推送邮箱 = -pushplus推送token = -自定义推送标题 = -自定义开播推送内容 = -自定义关播推送内容 = +ntfy推送邮箱 = +pushplus推送token = +自定义推送标题 = +自定义开播推送内容 = +自定义关播推送内容 = 只推送通知不录制(是/否) = 否 直播推送检测频率(秒) = 1800 开播推送开启(是/否) = 是 -关播推送开启(是/否)= 否 +关播推送开启(是/否) = 否 +只推送通知不直播(是/否) = 否 [Cookie] -# 录制抖音必填 抖音cookie = ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986 -快手cookie = -tiktok_cookie = -虎牙cookie = -斗鱼cookie = -yy_cookie = -b站cookie = -小红书cookie = -bigo_cookie = -blued_cookie = -sooplive_cookie = -netease_cookie = -千度热播_cookie = -pandatv_cookie = -猫耳fm_cookie = -winktv_cookie = -flextv_cookie = -look_cookie = -twitcasting_cookie = -baidu_cookie = -weibo_cookie = -kugou_cookie = -twitch_cookie = -liveme_cookie = -huajiao_cookie = -liuxing_cookie = -showroom_cookie = -acfun_cookie = -changliao_cookie = -yinbo_cookie = -yingke_cookie = -zhihu_cookie = -chzzk_cookie = -haixiu_cookie = -vvxqiu_cookie = -17live_cookie = -langlive_cookie = -pplive_cookie = -6room_cookie = -lehaitv_cookie = -huamao_cookie = -shopee_cookie = -youtube_cookie = -taobao_cookie = -jd_cookie = -faceit_cookie = -migu_cookie = -lianjie_cookie = -laixiu_cookie = -picarto_cookie = - +快手cookie = +tiktok_cookie = +虎牙cookie = +斗鱼cookie = +yy_cookie = +b站cookie = +小红书cookie = +bigo_cookie = +blued_cookie = +sooplive_cookie = +netease_cookie = +千度热播_cookie = +pandatv_cookie = +猫耳fm_cookie = +winktv_cookie = +flextv_cookie = +look_cookie = +twitcasting_cookie = +baidu_cookie = +weibo_cookie = +kugou_cookie = +twitch_cookie = +liveme_cookie = +huajiao_cookie = +liuxing_cookie = +showroom_cookie = +acfun_cookie = +changliao_cookie = +yinbo_cookie = +yingke_cookie = +zhihu_cookie = +chzzk_cookie = +haixiu_cookie = +vvxqiu_cookie = +17live_cookie = +langlive_cookie = +pplive_cookie = +6room_cookie = +lehaitv_cookie = +huamao_cookie = +shopee_cookie = +youtube_cookie = +taobao_cookie = +jd_cookie = +faceit_cookie = +migu_cookie = +lianjie_cookie = +laixiu_cookie = +picarto_cookie = [Authorization] -popkontv_token = +popkontv_token = [账号密码] -sooplive账号 = -sooplive密码 = -flextv账号 = -flextv密码 = -popkontv账号 = +sooplive账号 = +sooplive密码 = +flextv账号 = +flextv密码 = +popkontv账号 = partner_code = P-00001 -popkontv密码 = +popkontv密码 = twitcasting账号类型 = normal -twitcasting账号 = -twitcasting密码 = \ No newline at end of file +twitcasting账号 = +twitcasting密码 = + +[直播设置] +language(zh_cn/en) = zh_cn +是否跳过代理检测(是/否) = 否 +直播保存路径(不填则默认) = +保存文件夹是否以作者区分 = 是 +保存文件夹是否以时间区分 = 否 +保存文件夹是否以标题区分 = 否 +保存文件名是否包含标题 = 否 +是否去除名称中的表情符号 = 是 +视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频 = mp4 +原画|超清|高清|标清|流畅 = 原画 +是否使用代理ip(是/否) = 是 +代理地址 = +同一时间访问网络的线程数 = 3 +循环时间(秒) = 120 +排队读取网址时间(秒) = 0 +是否显示循环秒数 = 否 +是否显示直播源地址 = 否 +分段直播是否开启 = 否 +是否强制启用https直播 = 否 +直播空间剩余阈值(gb) = 1.0 +视频分段时间(秒) = 1800 +直播完成后自动转为mp4格式 = 否 +mp4格式重新编码为h264 = 否 +追加格式后删除原文件 = 否 +生成时间字幕文件 = 否 +是否直播完成后执行自定义脚本 = 否 +使用代理直播的平台(逗号分隔) = tiktok, soop, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu, faceit +额外使用代理直播的平台(逗号分隔) = + diff --git a/main.py b/main.py index 0f60e68..04ec0bf 100644 --- a/main.py +++ b/main.py @@ -781,6 +781,58 @@ def direct_download_stream(source_url: str, save_path: str, record_name: str, li logger.error(f"FLV下载错误: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") return False +import asyncio +import datetime +import subprocess + +async def check_subprocess_with_heartbeat(record_name: str, + record_url: str, + ffmpeg_command: list, + save_type: str, + script_command: str | None = None, + heartbeat_timeout: int = 10) -> bool: + """ + 启动 ffmpeg 子进程并进行心跳检测: + - 每 heartbeat_timeout 秒内必须有输出,否则认为卡死,直接 kill。 + - 进程退出 returncode=0 正常,否则异常。 + """ + process = await asyncio.create_subprocess_exec( + *ffmpeg_command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + last_heartbeat = datetime.datetime.now() + + async def read_stream(stream, name): + nonlocal last_heartbeat + async for line in stream: + decoded = line.decode(errors="ignore").strip() + if decoded: + last_heartbeat = datetime.datetime.now() + # 可根据需要过滤/解析码率、fps + print(f"[{record_name}][{name}] {decoded}") + + # 并行读取 stdout 和 stderr + asyncio.create_task(read_stream(process.stdout, "stdout")) + asyncio.create_task(read_stream(process.stderr, "stderr")) + + while True: + # 检查进程是否退出 + if process.returncode is not None: + break + + # 检查心跳超时 + now = datetime.datetime.now() + if (now - last_heartbeat).total_seconds() > heartbeat_timeout: + print(f"[心跳超时] {record_name}: {heartbeat_timeout}s 无数据,强制重启 ffmpeg") + process.kill() + await process.wait() + return False + + await asyncio.sleep(2) + + return process.returncode == 0 def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, save_type: str, script_command: str | None = None) -> bool: @@ -1237,13 +1289,19 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: ] ffmpeg_command.extend(command) - comment_end = check_subprocess( - record_name, - record_url, - ffmpeg_command, - record_save_type, - custom_script - ) + # comment_end = check_subprocess( + # record_name, + # record_url, + # ffmpeg_command, + # record_save_type, + # custom_script + # ) + comment_end = asyncio.run( + check_subprocess_with_heartbeat( + record_name, record_url, ffmpeg_command, record_save_type, custom_script, heartbeat_timeout=15 + ) +) + if comment_end: return