This commit is contained in:
eric
2026-01-22 02:52:01 -06:00
parent 9263110c08
commit b349f8f405
2 changed files with 522 additions and 0 deletions

295
backup/msg_push.py Normal file
View File

@@ -0,0 +1,295 @@
# -*- coding: utf-8 -*-
"""
Author: Hmily
GitHub: https://github.com/ihmily
Date: 2023-09-03 19:18:36
Update: 2025-01-23 17:16:12
Copyright (c) 2023-2024 by Hmily, All Rights Reserved.
"""
from typing import Dict, Any
import json
import base64
import urllib.request
import urllib.error
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
no_proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(no_proxy_handler)
headers: Dict[str, str] = {'Content-Type': 'application/json'}
def dingtalk(url: str, content: str, number: str = None, is_atall: bool = False) -> Dict[str, Any]:
success = []
error = []
api_list = url.replace('', ',').split(',') if url.strip() else []
for api in api_list:
json_data = {
'msgtype': 'text',
'text': {
'content': content,
},
"at": {
"atMobiles": [
number
],
"isAtAll": is_atall
},
}
try:
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(api, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode('utf-8')
json_data = json.loads(json_str)
if json_data['errcode'] == 0:
success.append(api)
else:
error.append(api)
print(f'钉钉推送失败, 推送地址:{api}, {json_data["errmsg"]}')
except Exception as e:
error.append(api)
print(f'钉钉推送失败, 推送地址:{api}, 错误信息:{e}')
return {"success": success, "error": error}
def xizhi(url: str, title: str, content: str) -> Dict[str, Any]:
success = []
error = []
api_list = url.replace('', ',').split(',') if url.strip() else []
for api in api_list:
json_data = {
'title': title,
'content': content
}
try:
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(api, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode('utf-8')
json_data = json.loads(json_str)
if json_data['code'] == 200:
success.append(api)
else:
error.append(api)
print(f'微信推送失败, 推送地址:{api}, 失败信息:{json_data["msg"]}')
except Exception as e:
error.append(api)
print(f'微信推送失败, 推送地址:{api}, 错误信息:{e}')
return {"success": success, "error": error}
def send_email(email_host: str, login_email: str, email_pass: str, sender_email: str, sender_name: str,
to_email: str, title: str, content: str, smtp_port: str = None, open_ssl: bool = True) -> Dict[str, Any]:
receivers = to_email.replace('', ',').split(',') if to_email.strip() else []
try:
message = MIMEMultipart()
send_name = base64.b64encode(sender_name.encode("utf-8")).decode()
message['From'] = f'=?UTF-8?B?{send_name}?= <{sender_email}>'
message['Subject'] = Header(title, 'utf-8')
if len(receivers) == 1:
message['To'] = receivers[0]
t_apart = MIMEText(content, 'plain', 'utf-8')
message.attach(t_apart)
if open_ssl:
smtp_port = int(smtp_port) or 465
smtp_obj = smtplib.SMTP_SSL(email_host, smtp_port)
else:
smtp_port = int(smtp_port) or 25
smtp_obj = smtplib.SMTP(email_host, smtp_port)
smtp_obj.login(login_email, email_pass)
smtp_obj.sendmail(sender_email, receivers, message.as_string())
return {"success": receivers, "error": []}
except smtplib.SMTPException as e:
print(f'邮件推送失败, 推送邮箱:{to_email}, 错误信息:{e}')
return {"success": [], "error": receivers}
def tg_bot(chat_id: int, token: str, content: str) -> Dict[str, Any]:
try:
json_data = {
"chat_id": chat_id,
'text': content
}
url = f'https://api.telegram.org/bot{token}/sendMessage'
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(url, data=data, headers=headers)
response = urllib.request.urlopen(req, timeout=15)
json_str = response.read().decode('utf-8')
_json_data = json.loads(json_str)
return {"success": [1], "error": []}
except Exception as e:
print(f'tg推送失败, 聊天ID{chat_id}, 错误信息:{e}')
return {"success": [], "error": [1]}
def bark(api: str, title: str = "message", content: str = 'test', level: str = "active",
badge: int = 1, auto_copy: int = 1, sound: str = "", icon: str = "", group: str = "",
is_archive: int = 1, url: str = "") -> Dict[str, Any]:
success = []
error = []
api_list = api.replace('', ',').split(',') if api.strip() else []
for _api in api_list:
json_data = {
"title": title,
"body": content,
"level": level,
"badge": badge,
"autoCopy": auto_copy,
"sound": sound,
"icon": icon,
"group": group,
"isArchive": is_archive,
"url": url
}
try:
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(_api, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode("utf-8")
json_data = json.loads(json_str)
if json_data['code'] == 200:
success.append(_api)
else:
error.append(_api)
print(f'Bark推送失败, 推送地址:{_api}, 失败信息:{json_data["message"]}')
except Exception as e:
error.append(api)
print(f'Bark推送失败, 推送地址:{_api}, 错误信息:{e}')
return {"success": success, "error": error}
def ntfy(api: str, title: str = "message", content: str = 'test', tags: str = 'tada', priority: int = 3,
action_url: str = "", attach: str = "", filename: str = "", click: str = "", icon: str = "",
delay: str = "", email: str = "", call: str = "") -> Dict[str, Any]:
success = []
error = []
api_list = api.replace('', ',').split(',') if api.strip() else []
tags = tags.replace('', ',').split(',') if tags else ['partying_face']
actions = [{"action": "view", "label": "view live", "url": action_url}] if action_url else []
for _api in api_list:
server, topic = _api.rsplit('/', maxsplit=1)
json_data = {
"topic": topic,
"title": title,
"message": content,
"tags": tags,
"priority": priority,
"attach": attach,
"filename": filename,
"click": click,
"actions": actions,
"markdown": False,
"icon": icon,
"delay": delay,
"email": email,
"call": call
}
try:
data = json.dumps(json_data, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(server, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode("utf-8")
json_data = json.loads(json_str)
if "error" not in json_data:
success.append(_api)
else:
error.append(_api)
print(f'ntfy推送失败, 推送地址:{_api}, 失败信息:{json_data["error"]}')
except urllib.error.HTTPError as e:
error.append(_api)
error_msg = e.read().decode("utf-8")
print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{json.loads(error_msg)["error"]}')
except Exception as e:
error.append(api)
print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{e}')
return {"success": success, "error": error}
def pushplus(token: str, title: str, content: str) -> Dict[str, Any]:
"""
PushPlus推送通知
API文档: https://www.pushplus.plus/doc/
"""
success = []
error = []
token_list = token.replace('', ',').split(',') if token.strip() else []
for _token in token_list:
json_data = {
'token': _token,
'title': title,
'content': content
}
try:
url = 'https://www.pushplus.plus/send'
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(url, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode('utf-8')
json_data = json.loads(json_str)
if json_data.get('code') == 200:
success.append(_token)
else:
error.append(_token)
print(f'PushPlus推送失败, Token{_token}, 失败信息:{json_data.get("msg", "未知错误")}')
except Exception as e:
error.append(_token)
print(f'PushPlus推送失败, Token{_token}, 错误信息:{e}')
return {"success": success, "error": error}
if __name__ == '__main__':
send_title = '直播通知' # 标题
send_content = '张三 开播了!' # 推送内容
# 钉钉推送通知
webhook_api = '' # 替换成自己Webhook链接,参考文档https://open.dingtalk.com/document/robots/custom-robot-access
phone_number = '' # 被@用户的手机号码
is_atall = '' # 是否@全体
# dingtalk(webhook_api, send_content, phone_number)
# 微信推送通知
# 替换成自己的单点推送接口,获取地址https://xz.qqoq.net/#/admin/one
# 当然也可以使用其他平台API 如server酱 使用方法一样
xizhi_api = 'https://xizhi.qqoq.net/xxxxxxxxx.send'
# xizhi(xizhi_api, send_content)
# telegram推送通知
tg_token = '' # tg搜索"BotFather"获取的token值
tg_chat_id = 000000 # tg搜索"userinfobot"获取的chat_id值即可发送推送消息给你自己如果下面的是群组id则发送到群
# tg_bot(tg_chat_id, tg_token, send_content)
# email_message(
# email_host="smtp.qq.com",
# login_email="",
# email_pass="",
# sender_email="",
# sender_name="",
# to_email="",
# title="",
# content="",
# )
bark_url = 'https://xxx.xxx.com/key/'
# bark(bark_url, send_title, send_content)
ntfy(
api="https://ntfy.sh/xxxxx",
title="直播推送",
content="xxx已开播",
)
# PushPlus推送通知
pushplus_token = '' # 替换成自己的PushPlus Token获取地址https://www.pushplus.plus/
# pushplus(pushplus_token, send_title, send_content)

227
backup/robust_relay.py Normal file
View File

@@ -0,0 +1,227 @@
# robust_relay.py
# 高稳定性 YouTube 转播模块(防误判下播 + 看门狗 + 自动重连 + token刷新
# 作者:融合 A+B 代码精华,专为抖音/抖音国际版长期无人值守转播设计
import subprocess
import time
import threading
import os
import signal
import datetime
from typing import List, Optional, Callable
# ==================== 配置常量(可全局覆盖) ====================
WATCHDOG_SILENCE = 600 # 10分钟无输出 → 软重启
WATCHDOG_GRACE = 120 # 启动宽限期
REFRESH_URL_EVERY = 300 # 每5分钟强制刷新 real_url防token过期
OFFLINE_DEBOUNCE = 3 # 连续3次API返回下播才认为真下播
OFFLINE_WINDOW = 120 # 最近120秒有活跃就忽略下播信号
SOFT_RESTART_COOLDOWN = 90 # 软重启最小间隔
# ==================== 轻量探活 ====================
def _probe_alive(url: str, timeout: float = 5.0) -> bool:
if not url:
return False
try:
import urllib.request, contextlib
opener = urllib.request.build_opener()
if hasattr(urllib.request, 'getproxies'):
proxy = urllib.request.getproxies().get('http')
if proxy:
opener.add_handler(urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}, method="HEAD")
with contextlib.closing(opener.open(req, timeout=timeout)) as r:
if r.code >= 400:
return False
head = r.read(256)
if url.lower().endswith(".m3u8"):
return b"#EXTM3U" in head.upper()
if url.lower().endswith(".flv"):
return head.startswith(b"FLV")
return len(head) > 0
except:
return False
# ==================== 增强版 ffmpeg 启动器 ====================
def _launch_ffmpeg(
input_url: str,
youtube_rtmp: str,
anchor_name: str,
logger: Callable[[str], None],
extra_cmd: List[str] = None
) -> subprocess.Popen:
cmd = [
"ffmpeg", "-nostdin", "-hide_banner", "-stats", "-stats_period", "60", "-loglevel", "info",
# 输入端超强重连
"-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_at_eof", "1",
"-reconnect_delay_max", "30", "-rw_timeout", "20000000",
# 极致低延迟 + 时间戳修复
"-fflags", "+genpts+igndts+flush_packets", "-flags", "+global_header",
"-thread_queue_size", "4096", "-analyzeduration", "0", "-probesize", "32",
"-i", input_url,
# copy 模式(质量无损)
"-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc",
"-map", "0:v?", "-map", "0:a?",
# YouTube 强制参数
"-f", "mpegts", "-g", "2", "-rtmp_buffer", "1000", "-rtmp_live", "live",
"-max_delay", "0", "-tune", "zerolatency", "-avoid_negative_ts", "make_zero",
"-muxdelay", "0", "-muxpreload", "0",
"-fflags", "+nobuffer+flush_packets", "-avioflags", "direct",
]
if extra_cmd:
cmd.extend(extra_cmd)
cmd.append(youtube_rtmp)
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
creationflags=creationflags
)
# 用于看门狗
proc.last_output_ts = time.time()
def _reader():
try:
for line in proc.stdout: # type: ignore
if not line:
break
text = line.decode(errors="ignore").rstrip()
proc.last_output_ts = time.time()
if any(k in text for k in ["bitrate=", "frame=", "speed=", "fps="]):
logger(f"[{anchor_name}] {text}")
except:
pass
threading.Thread(target=_reader, daemon=True).start()
return proc
# ==================== 主函数:高稳定性转播(替代原 check_subprocess ====================
def robust_youtube_relay(
anchor_name: str,
real_url: str,
youtube_rtmp: str,
logger: Callable[[str], None],
get_latest_stream_url: Callable[[], Optional[str]], # 用于刷新 real_url如有token过期
is_stream_still_live: Callable[[], bool], # 检查主播是否在播API
extra_ffmpeg_args: List[str] = None, # 可选:加 -f segment 分段保存
heartbeat_callback: Callable[[int], None] = None # 可选:每分钟回调已运行秒数
) -> bool:
"""
返回值:
True = 主播已下播(正常结束)
False = 发生未知错误(建议计数错误次数)
"""
proc: Optional[subprocess.Popen] = None
start_time = time.time()
last_refresh = 0.0
last_soft_restart = 0.0
offline_count = 0
last_live_ts = time.time()
try:
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
logger(f"[{anchor_name}] 已开始高稳定性转播 → {youtube_rtmp.split('/')[-1]}")
while True:
now = time.time()
elapsed = int(now - start_time)
silence = now - proc.last_output_ts if proc else 0
# 1. 进程意外退出 → 重启
if proc and proc.poll() is not None:
logger(f"[{anchor_name}] ffmpeg 意外退出3秒后重启...")
time.sleep(3)
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
start_time = time.time()
continue
# 2. 看门狗:长时间无输出 → 软重启
if elapsed > WATCHDOG_GRACE and silence > WATCHDOG_SILENCE:
if now - last_soft_restart > SOFT_RESTART_COOLDOWN:
logger(f"[{anchor_name}] 检测到卡顿({silence:.0f}s无输出软重启 ffmpeg...")
last_soft_restart = now
if proc and proc.stdin:
proc.stdin.write(b'q')
proc.stdin.flush()
time.sleep(4)
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
start_time = time.time()
continue
# 3. 定期刷新播放地址防token过期
if now - last_refresh > REFRESH_URL_EVERY:
new_url = get_latest_stream_url()
if new_url and new_url != real_url:
logger(f"[{anchor_name}] 检测到新的播放地址,切换中...")
real_url = new_url
if proc and proc.stdin:
proc.stdin.write(b'q')
time.sleep(3)
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
start_time = time.time()
last_refresh = now
# 4. 下播防抖判定(核心防误判)
try:
still_live = is_stream_still_live()
except:
still_live = True # 接口异常不判下播
if not still_live:
# 交叉验证:流还能访问 → 忽略API误判
if _probe_alive(real_url):
logger(f"[{anchor_name}] API判下播但流仍活跃忽略本次误判")
still_live = True
last_live_ts = now
if still_live:
offline_count = 0
last_live_ts = now
else:
if now - last_live_ts < OFFLINE_WINDOW:
logger(f"[{anchor_name}] 短期掉线,忽略")
else:
offline_count += 1
if offline_count >= OFFLINE_DEBOUNCE:
logger(f"[{anchor_name}] 连续{offline_count}次确认下播,结束转播")
if proc and proc.stdin:
proc.stdin.write(b'q')
return True
else:
logger(f"[{anchor_name}] 疑似下播 {offline_count}/{OFFLINE_DEBOUNCE},继续观察")
# 5. 心跳日志 & 回调
if elapsed % 60 < 2:
h, m = divmod(elapsed // 60, 60)
logger(f"[{anchor_name}] 正在转播 {h:02d}:{m:02d}")
if heartbeat_callback:
heartbeat_callback(elapsed)
time.sleep(8)
except KeyboardInterrupt:
logger(f"[{anchor_name}] 手动终止转播")
return True
except Exception as e:
logger(f"[{anchor_name}] 严重错误: {e}")
return False
finally:
if proc:
try:
proc.stdin.write(b'q') if proc.stdin else None
proc.wait(timeout=8)
except:
proc.kill()