This commit is contained in:
eric
2025-10-18 22:28:06 +08:00
parent 019f68a91a
commit 5d0134b86e
5 changed files with 2351 additions and 26 deletions

1035
back/mult6 Normal file

File diff suppressed because it is too large Load Diff

1035
back/mult7 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -158,3 +158,14 @@ mp4格式重新编码为h264 = 否
使用代理直播的平台(逗号分隔) = tiktok, soop, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu, faceit
额外使用代理直播的平台(逗号分隔) =
[监控]
启用webui(是/否) =
webui监听ip = 0.0.0.0
webui端口 = 8787
[稳定性]
ffmpeg长时间无输出阈值秒 = 600
码率低重启比率(0-1) = 0.5
软重启冷却秒 = 90
url刷新间隔秒 = 300

89
main.py
View File

@@ -407,52 +407,89 @@ def probe_stream_url(real_url: Optional[str], timeout: float = 5.0) -> bool:
# =============== 解析与选源(抖音/TikTok/自定义) ===============
# 修复点①:增强 fetch_port_info —— 重试 + 指数退避(不改签名/调用处)
import random, json, contextlib, urllib.request, urllib.error, re, time
async def fetch_port_info(url: str, record_quality_code: str = "OD") -> dict:
"""抖音/TikTok 直播解析(带防风控与 JSON fallback"""
proxy = proxy_addr
delay = 1.5 # 初始退避
for attempt in range(1, 4): # 最多 3 次
delay = 1.2
for attempt in range(1, 4):
try:
if "douyin.com/" in url:
if "douyin.com" in url:
# ======= 原始接口 =======
if 'v.douyin.com' not in url and '/user/' not in url:
j = await spider.get_douyin_stream_data(url=url, proxy_addr=proxy, cookies=dy_cookie)
else:
j = await spider.get_douyin_app_stream_data(url=url, proxy_addr=proxy, cookies=dy_cookie)
port_info = await stream.get_douyin_stream_url(j, record_quality_code, proxy)
elif url.startswith("https://www.tiktok.com/"):
# ======= fallback接口空时解析 HTML 中 JSON =======
if not port_info.get("is_live"):
handlers = []
if proxy:
handlers.append(urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
opener = urllib.request.build_opener(*handlers) if handlers else urllib.request.build_opener()
headers = {
"User-Agent": random.choice([
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile Safari/604.1",
]),
"Referer": f"https://live.douyin.com/{re.sub(r'\\D','',url)}",
"Accept": "*/*",
}
req = urllib.request.Request(url, headers=headers)
with contextlib.closing(opener.open(req, timeout=6)) as resp:
html = resp.read(512*1024).decode("utf-8", errors="ignore")
# 解析 roomStore JSON
m = re.search(r'"roomStore"\s*:\s*({.*?})\s*,\s*"userStore"', html)
if m:
try:
data = json.loads(m.group(1))
stream_data = data.get("roomInfo", {}).get("stream_url", {})
flv = (stream_data.get("flv_pull_url", {}).get("FULL_HD1")
or stream_data.get("flv_pull_url", {}).get("HD1"))
m3u8 = (stream_data.get("hls_pull_url", {}).get("FULL_HD1")
or stream_data.get("hls_pull_url", {}).get("HD1"))
if flv or m3u8:
port_info = {"anchor_name": "JSONFallback", "is_live": True}
if flv: port_info["flv_url"] = flv
else: port_info["m3u8_url"] = m3u8
CLOG.warn("⚠️ JSON fallback 成功解析流地址", worker=None)
except Exception as e:
CLOG.debug(f"JSON fallback 解析失败: {e}")
elif "tiktok.com" in url:
if proxy:
j = await spider.get_tiktok_stream_data(url=url, proxy_addr=proxy, cookies=tiktok_cookie)
port_info = await stream.get_tiktok_stream_url(j, record_quality_code, proxy)
else:
logger.error("TikTok 需要代理,请在 config.ini 开启并配置代理。")
port_info = {"anchor_name": "", "is_live": False}
logger.error("TikTok 需要代理。")
elif (".m3u8" in url) or (".flv" in url):
port_info = {
"anchor_name": "自定义_" + str(uuid.uuid4())[:8],
"is_live": True,
}
if url.endswith(".flv"):
port_info["flv_url"] = url
else:
port_info["m3u8_url"] = url
port_info = {"anchor_name": "自定义", "is_live": True}
port_info["flv_url" if url.endswith(".flv") else "m3u8_url"] = url
else:
port_info = {"anchor_name": "", "is_live": False}
# 返回前补充:若 is_live True 但未给出真实播放地址,也作为暂不可用(但不判下播)
if port_info.get("is_live") and not select_source_url(url, port_info):
logger.warning(f"[fetch_port_info] 在播但暂未拿到播放 URL可能接口延迟{url}")
return port_info
except Exception as e:
logger.warning(f"[fetch_port_info] 第{attempt}次解析失败: {e}")
if attempt < 3:
try:
await asyncio.sleep(delay)
except Exception:
time.sleep(delay)
except urllib.error.HTTPError as e:
if e.code == 403:
CLOG.warn(f"[fetch_port_info] Douyin 403 第{attempt}次延时重试")
time.sleep(delay)
delay *= 2
else:
logger.error(f"[fetch_port_info] 多次失败,返回空状态: {url}")
return {"anchor_name": "", "is_live": False}
continue
CLOG.error(f"[fetch_port_info] HTTP {e.code}: {url}")
break
except Exception as e:
CLOG.warn(f"[fetch_port_info] 异常: {e}")
time.sleep(delay)
delay *= 2
return {"anchor_name": "", "is_live": False}
# =============== 启动/停止 ffmpeg ===============
def launch_ffmpeg(input_url: str,

207
out.html Normal file
View File

@@ -0,0 +1,207 @@
<html>
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
<title>验证码中间页</title>
<script charset="utf-8" nonce="argus-csp-token" src="https://lf-cdn-tos.bytescm.com/obj/static/sec_sdk_build/3.5.2/captcha/index.js"></script>
<style>
.middle_page_loading{
width: 150px;
height: 15px;
margin: 0 auto;
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
}
.middle_page_loading span{
display: inline-block;
width: 15px;
height: 100%;
margin-right: 5px;
background: #323332;
-webkit-transform-origin: right bottom;
-moz-transform-origin: right bottom;
-ms-transform-origin: right bottom;
-o-transform-origin: right bottom;
transform-origin: right bottom;
-webkit-animation: load 1s ease infinite;
-moz-animation: load 1s ease infinite;
-ms-animation: load 1s ease infinite;
-o-animation: load 1s ease infinite;
animation: load 1s ease infinite;
}
.middle_page_loading span:last-child{
margin-right: 0px;
}
@keyframes load {
0%{
opacity: 1;
}
100%{
opacity: 0;
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
}
}
@-webkit-keyframes load {
0%{
opacity: 1;
}
100%{
opacity: 0;
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
}
}
@-moz-keyframes load {
0%{
opacity: 1;
}
100%{
opacity: 0;
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
}
}
@-o-keyframes load {
0%{
opacity: 1;
}
100%{
opacity: 0;
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
}
}
@-ms-keyframes load {
0%{
opacity: 1;
}
100%{
opacity: 0;
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
}
}
.middle_page_loading span:nth-child(1){
-webkit-animation-delay: 0.13s;
-moz-animation-delay: 0.13s;
-ms-animation-delay: 0.13s;
-o-animation-delay: 0.13s;
animation-delay: 0.13s;
}
.middle_page_loading span:nth-child(2){
-webkit-animation-delay: 0.26s;
-moz-animation-delay: 0.26s;
-ms-animation-delay: 0.26s;
-o-animation-delay: 0.26s;
animation-delay: 0.26s;
}
.middle_page_loading span:nth-child(3){
-webkit-animation-delay: 0.39s;
-moz-animation-delay: 0.39s;
-ms-animation-delay: 0.39s;
-o-animation-delay: 0.39s;
animation-delay: 0.39s;
}
.middle_page_loading span:nth-child(4){
-webkit-animation-delay: 0.52s;
-moz-animation-delay: 0.52s;
-ms-animation-delay: 0.52s;
-o-animation-delay: 0.52s;
animation-delay: 0.52s;
}
.middle_page_loading span:nth-child(5){
-webkit-animation-delay: 0.65s;
-moz-animation-delay: 0.65s;
-ms-animation-delay: 0.65s;
-o-animation-delay: 0.65s;
animation-delay: 0.65s;
}
</style>
</head>
<body>
<div id="root"></div>
<div class="middle_page_loading">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</body>
<script nonce="argus-csp-token">
function parseCookie() {
var obj = document.cookie.split(';')
.filter(function(item) {
return !!item.trim();
})
.reduce(function(obj, item) {
var pairs = item.split('=');
var key = pairs[0] && pairs[0].trim();
var val = pairs[1] && pairs[1].trim();
obj[key] = val;
return obj;
}, {});
return obj;
}
function setCookie(name, value) {
try {
if (window.sessionStorage) {
if (!window.sessionStorage.getItem(name)) {
window.sessionStorage.setItem(name, value)
}
}
if (window.localStorage) {
if (!window.localStorage.getItem(name)) {
window.localStorage.setItem(name, value)
}
}
if (!!!parseCookie()[name]) {
document.cookie = name + "=" + value + ";";
}
} catch (e) { }
}
setCookie("__ac_referer", document.referrer || "__ac_blank");
const options = {
commonOptions: {
aid: 15623,
iid: '0',
did: '0',
},
captchaOptions: {
ele: 'root',
hideCloseBtn: true,
showMode: 'mask',
successCb: function(){
setTimeout(() => {
location.reload();
}, 2000)
},
},
}
const verify_data = {"code":"10000","from":"","type":"verify","version":"","region":"cn","subtype":"slide","ui_type":"","detail":"w5YhWL-cg9tHnsLcpIVjj-pqrjbnkFClfSeHUG6gEdJBKAhWs5cwR8fiY6IrrVDQVW8Ox597kD6MYeV3UsDa9bWCtz1C7xOMAyUat5oTTzqxHW8q2YRHJSXVhnPXLArIAiIdN0vPYloSMSL9Ahexn4bVXZI6G7XWsmN0yFcyXMS2B05cdwRG7EIRkZ2Fa9VQplkiIgPp2CpyApLD*rVSdiUPiJ-1YWwLAu4GC8Ux2YXFPMv07wG1XQGGjtLKigOLmnJVlY2gf7A3mKyfWkJc2mV0BZwO-l69ARJtTMc*BD*0OBGhI43FMTTC2taIMmwP7QM4gXloe-LSUE0CBG3815WdlIUdZXQP8569nfHkjbtY5g..","verify_event":"","fp":"verify_mgvprn20_kEBbdkkA_AE16_4gpX_9NAu_OepLidaY9UcL","server_sdk_env":"{\"idc\":\"lf\",\"region\":\"CN\",\"server_type\":\"whale\"}","log_id":"20251018112418B86F5C29924FAD8C3C6D","is_assist_mobile":false,"is_complex_sms":false,"identity_action":"","identity_scene":"","login_status":0,"aid":0}
window.TTGCaptcha.init(options)
window.TTGCaptcha.render({
verify_data
})
</script>
</html>