Compare commits

7 Commits
dev ... main

Author SHA1 Message Date
eric
e6760dc99f s 2025-09-13 04:40:41 +00:00
eric
88c53c37ff 's'' 2025-09-13 10:21:42 +08:00
eric
64abdc03be 's'' 2025-09-13 10:15:00 +08:00
eric
348bc71862 ' 2025-09-13 08:49:26 +08:00
eric
64e266ecf1 '' 2025-09-13 08:23:29 +08:00
eric
49f3845b37 '' 2025-09-13 08:21:39 +08:00
eric
9c09960431 's' 2025-09-13 08:13:47 +08:00
28 changed files with 9121 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
cache/
__pycache__/

14
__init__.py Normal file
View File

@@ -0,0 +1,14 @@
import os
import sys
from pathlib import Path
from initializer import check_node
current_file_path = Path(__file__).resolve()
current_dir = current_file_path.parent
JS_SCRIPT_PATH = current_dir / 'javascript'
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
node_execute_dir = Path(execute_dir) / 'node'
current_env_path = os.environ.get('PATH')
os.environ['PATH'] = str(node_execute_dir) + os.pathsep + current_env_path
check_node()

162
back Normal file
View File

@@ -0,0 +1,162 @@
import os
import subprocess
import threading
import asyncio
import uuid
import queue
import time
import signal
import sys
import spider
import stream
class AdaptiveHLS2YouTube:
def __init__(self, max_bitrate=6000, min_bitrate=3000, audio_bitrate=192,
delay_sec=30, segment_time=2, check_interval=5):
self.processes = {}
self.stop_flag = threading.Event()
self.delay_sec = delay_sec
self.segment_time = segment_time
self.check_interval = check_interval
self.buffer_queue = queue.Queue()
self.max_bitrate = max_bitrate
self.min_bitrate = min_bitrate
self.audio_bitrate = audio_bitrate
self.current_bitrate = max_bitrate
def start_stream(self, real_url, youtube_key, room_name=None):
room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}"
print(f"[{room_name}] 启动推流,目标 YouTube Key: {youtube_key}")
threading.Thread(target=self._pull_thread, args=(real_url,), daemon=True).start()
threading.Thread(target=self._push_thread, args=(youtube_key, room_name), daemon=True).start()
threading.Thread(target=self._abr_thread, daemon=True).start()
def _pull_thread(self, real_url):
cmd = [
"ffmpeg",
"-rw_timeout", "50000000",
"-user_agent", "Mozilla/5.0",
"-headers", "Referer: https://www.douyin.com/",
"-fflags", "+nobuffer+genpts",
"-flags", "low_delay",
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "10",
"-i", real_url,
"-c", "copy",
"-f", "mpegts",
"-"
]
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
while not self.stop_flag.is_set():
data = proc.stdout.read(188*50)
if not data:
time.sleep(0.05)
continue
self.buffer_queue.put(data)
except Exception as e:
print(f"拉流异常: {e}")
def _push_thread(self, youtube_key, room_name):
min_buffer = max(1, self.delay_sec // self.segment_time)
while not self.stop_flag.is_set():
if self.buffer_queue.qsize() < min_buffer:
time.sleep(0.05)
continue
cmd = [
"ffmpeg",
"-re",
"-f", "mpegts",
"-i", "-",
"-c:v", "libx264",
"-preset", "veryfast",
"-tune", "zerolatency",
"-b:v", f"{self.current_bitrate}k",
"-maxrate", f"{self.current_bitrate}k",
"-bufsize", f"{self.current_bitrate*2}k",
"-r", "30",
"-g", "60",
"-c:a", "aac",
"-b:a", f"{self.audio_bitrate}k",
"-f", "flv",
f"rtmp://a.rtmp.youtube.com/live2/{youtube_key}"
]
try:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
self.processes[room_name] = proc
while not self.stop_flag.is_set():
if not self.buffer_queue.empty():
data = self.buffer_queue.get()
proc.stdin.write(data)
proc.stdin.flush()
else:
time.sleep(0.01)
except Exception as e:
print(f"[{room_name}] 推流异常: {e}")
def _abr_thread(self):
# 动态码率调节
while not self.stop_flag.is_set():
queue_len = self.buffer_queue.qsize()
if queue_len > 100: # 队列积压大 → 网络慢 → 降低码率
new_bitrate = max(self.min_bitrate, self.current_bitrate - 500)
elif queue_len < 30: # 队列空 → 网络好 → 提高码率
new_bitrate = min(self.max_bitrate, self.current_bitrate + 500)
else:
new_bitrate = self.current_bitrate
if new_bitrate != self.current_bitrate:
print(f"[ABR] 调整码率: {self.current_bitrate}k -> {new_bitrate}k")
self.current_bitrate = new_bitrate
time.sleep(self.check_interval)
def stop_all(self):
print("停止所有推流...")
self.stop_flag.set()
for proc in self.processes.values():
if proc.poll() is None:
proc.terminate()
# ----------------- 抖音解析逻辑 -----------------
async def get_douyin_real_url(record_url, record_quality="原画"):
if 'v.douyin.com' not in record_url and '/user/' not in record_url:
json_data = await spider.get_douyin_stream_data(url=record_url)
else:
json_data = await spider.get_douyin_app_stream_data(url=record_url)
port_info = await stream.get_douyin_stream_url(json_data, record_quality, None)
return port_info.get("record_url")
# ----------------- 启动推流 -----------------
def start_douyin_to_youtube(record_url, youtube_key, room_name=None, delay_sec=30):
try:
real_url = asyncio.run(get_douyin_real_url(record_url))
except Exception as e:
print(f"获取真实直播流失败: {e}")
return
if not real_url:
print("获取真实直播流失败")
return
print(f"实际直播流: {real_url}")
hls2yt = AdaptiveHLS2YouTube(delay_sec=delay_sec)
hls2yt.start_stream(real_url, youtube_key, room_name)
return hls2yt
# ----------------- 示例 -----------------
if __name__ == "__main__":
DOUYIN_URL = "https://live.douyin.com/640087893839"
YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=30)
def signal_handler(sig, frame):
if hls_instance:
hls_instance.stop_all()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
while True:
time.sleep(60)

203
back2 Normal file
View File

@@ -0,0 +1,203 @@
import os
import subprocess
import threading
import asyncio
import uuid
import queue
import time
import signal
import sys
import shutil
import spider
import stream
class AdaptiveHLS2YouTube:
def __init__(self, max_bitrate=6000, min_bitrate=3000, audio_bitrate=192,
delay_sec=30, segment_time=2, check_interval=5):
self.processes = {}
self.stop_flag = threading.Event()
self.delay_sec = delay_sec
self.segment_time = segment_time
self.check_interval = check_interval
self.buffer_queue = queue.Queue()
self.max_bitrate = max_bitrate
self.min_bitrate = min_bitrate
self.audio_bitrate = audio_bitrate
self.current_bitrate = max_bitrate
def start_stream(self, real_url, youtube_key, room_name=None):
room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}"
print(f"[{room_name}] 启动推流,目标 YouTube Key: {youtube_key}")
threading.Thread(target=self._pull_thread, args=(real_url,), daemon=True).start()
threading.Thread(target=self._push_thread, args=(youtube_key, room_name), daemon=True).start()
threading.Thread(target=self._abr_thread, daemon=True).start()
def _pull_thread(self, real_url):
cmd = [
"ffmpeg",
"-rw_timeout", "50000000",
"-user_agent", "Mozilla/5.0",
"-headers", "Referer: https://www.douyin.com/",
"-fflags", "+nobuffer+genpts",
"-flags", "low_delay",
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "10",
"-i", real_url,
"-c", "copy",
"-f", "mpegts",
"-"
]
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
print("[拉流] FFmpeg 启动成功")
while not self.stop_flag.is_set():
data = proc.stdout.read(188*50)
if not data:
time.sleep(0.05)
continue
self.buffer_queue.put(data)
except Exception as e:
print(f"拉流异常: {e}")
def _push_thread(self, youtube_key, room_name):
min_buffer = max(1, self.delay_sec // self.segment_time)
while not self.stop_flag.is_set():
if self.buffer_queue.qsize() < min_buffer:
time.sleep(0.05)
continue
cmd = [
"ffmpeg",
"-re",
"-f", "mpegts",
"-i", "-",
"-c:v", "libx264",
"-preset", "veryfast",
"-tune", "zerolatency",
"-b:v", f"{self.current_bitrate}k",
"-maxrate", f"{self.current_bitrate}k",
"-bufsize", f"{self.current_bitrate*4}k",
"-r", "30",
"-g", "60",
"-c:a", "aac",
"-b:a", f"{self.audio_bitrate}k",
"-ar", "44100",
"-ac", "2",
"-f", "flv",
"-rtmp_live", "live",
f"rtmp://a.rtmp.youtube.com/live2/{youtube_key}"
]
try:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
self.processes[room_name] = proc
# 读取 stderr 打印日志
def read_stderr():
while proc.poll() is None:
line = proc.stderr.readline()
if line:
print(f"[{room_name} FFmpeg]: {line.decode(errors='ignore').strip()}")
threading.Thread(target=read_stderr, daemon=True).start()
while not self.stop_flag.is_set():
if not self.buffer_queue.empty():
data = self.buffer_queue.get()
proc.stdin.write(data)
proc.stdin.flush()
else:
time.sleep(0.01)
except Exception as e:
print(f"[{room_name}] 推流异常: {e}")
def _abr_thread(self):
while not self.stop_flag.is_set():
queue_len = self.buffer_queue.qsize()
if queue_len > 100:
new_bitrate = max(self.min_bitrate, self.current_bitrate - 500)
elif queue_len < 30:
new_bitrate = min(self.max_bitrate, self.current_bitrate + 500)
else:
new_bitrate = self.current_bitrate
if new_bitrate != self.current_bitrate:
print(f"[ABR] 调整码率: {self.current_bitrate}k -> {new_bitrate}k")
self.current_bitrate = new_bitrate
time.sleep(self.check_interval)
def stop_all(self):
print("停止所有推流...")
self.stop_flag.set()
for proc in self.processes.values():
if proc.poll() is None:
proc.terminate()
# ----------------- 抖音解析逻辑 -----------------
async def get_douyin_real_url(record_url, record_quality="原画"):
if 'v.douyin.com' not in record_url and '/user/' not in record_url:
json_data = await spider.get_douyin_stream_data(url=record_url)
else:
json_data = await spider.get_douyin_app_stream_data(url=record_url)
port_info = await stream.get_douyin_stream_url(json_data, record_quality, None)
return port_info.get("record_url")
# ----------------- 启动推流 -----------------
def start_douyin_to_youtube(record_url, youtube_key, room_name=None, delay_sec=30):
try:
real_url = asyncio.run(get_douyin_real_url(record_url))
except Exception as e:
print(f"获取真实直播流失败: {e}")
return
if not real_url:
print("获取真实直播流失败")
return
print(f"实际直播流: {real_url}")
hls2yt = AdaptiveHLS2YouTube(delay_sec=delay_sec)
hls2yt.start_stream(real_url, youtube_key, room_name)
return hls2yt
# ----------------- 控制台监控面板 -----------------
def monitor_panel_thread(hls_instance, interval=2, max_width=50):
term_width, _ = shutil.get_terminal_size()
bar_width = min(max_width, term_width - 30)
while not hls_instance.stop_flag.is_set():
queue_len = hls_instance.buffer_queue.qsize()
bitrate = hls_instance.current_bitrate
active_push = any(proc and proc.poll() is None for proc in hls_instance.processes.values())
queue_bar_len = min(bar_width, queue_len)
queue_bar = "#" * queue_bar_len + "-" * (bar_width - queue_bar_len)
print("\r", end="")
print(f"[监控] 队列: [{queue_bar}] {queue_len:4} | 码率: {bitrate:4}k | 推流活跃: {active_push}", end="")
time.sleep(interval)
# ----------------- 示例 -----------------
if __name__ == "__main__":
DOUYIN_URL = "https://live.douyin.com/848698179845"
YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=30)
if not hls_instance:
print("推流实例创建失败,程序退出")
sys.exit(1)
# 启动实时监控面板
threading.Thread(target=monitor_panel_thread, args=(hls_instance,), daemon=True).start()
def signal_handler(sig, frame):
if hls_instance:
hls_instance.stop_all()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
print("程序已启动,按 Ctrl+C 停止...")
while True:
time.sleep(60)

71
douyin_to_youtube.log Normal file
View File

@@ -0,0 +1,71 @@
2025-09-13 09:42:21,441 [INFO] httpx: HTTP Request: GET https://live.douyin.com/848698179845 "HTTP/2 200 OK"
2025-09-13 09:42:22,592 [INFO] httpx: HTTP Request: HEAD http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332541&major_anchor_level=common&sign=5d71c5252347afa2a3b465f133ed58f2&t_id=037-20250913094221234B680EF32A4439DD87-GEORFp&codec=h264 "HTTP/1.1 200 OK"
2025-09-13 09:42:22,597 [INFO] __main__: 实际直播流: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332541&major_anchor_level=common&sign=5d71c5252347afa2a3b465f133ed58f2&t_id=037-20250913094221234B680EF32A4439DD87-GEORFp&codec=h264
2025-09-13 09:42:22,602 [INFO] __main__: [抖音_7dbe64a7] 启动推流,目标 YouTube Key: qxvb-r47b-r5ju-6ud3-6k7z
2025-09-13 09:42:22,604 [INFO] __main__: [抖音_7dbe64a7] 实际直播流: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332541&major_anchor_level=common&sign=5d71c5252347afa2a3b465f133ed58f2&t_id=037-20250913094221234B680EF32A4439DD87-GEORFp&codec=h264
2025-09-13 09:42:25,655 [INFO] __main__: [抖音_7dbe64a7] 拉流进程启动PID: 7728
2025-09-13 09:42:40,372 [INFO] __main__: [抖音_7dbe64a7] 推流进程启动PID: 19568
2025-09-13 09:42:44,609 [INFO] __main__: [抖音_7dbe64a7] [ABR] 调整码率: 6000k -> 5500k (缓冲比例: 12.80)
2025-09-13 09:42:49,613 [INFO] __main__: [抖音_7dbe64a7] [ABR] 调整码率: 5500k -> 5000k (缓冲比例: 13.13)
2025-09-13 09:42:54,616 [INFO] __main__: [抖音_7dbe64a7] [ABR] 调整码率: 5000k -> 4500k (缓冲比例: 11.67)
2025-09-13 09:42:59,619 [INFO] __main__: [抖音_7dbe64a7] [ABR] 调整码率: 4500k -> 4000k (缓冲比例: 12.93)
2025-09-13 09:43:04,620 [INFO] __main__: [抖音_7dbe64a7] [ABR] 调整码率: 4000k -> 3500k (缓冲比例: 13.33)
2025-09-13 09:43:09,622 [INFO] __main__: [抖音_7dbe64a7] [ABR] 调整码率: 3500k -> 3000k (缓冲比例: 13.33)
2025-09-13 09:44:04,081 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:04,583 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:05,085 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:05,586 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:06,088 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:06,589 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:07,091 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:07,593 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:08,095 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:08,597 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:09,098 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:09,602 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:10,107 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:10,615 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:11,166 [ERROR] __main__: [抖音_7dbe64a7] 推流写入异常: [Errno 22] Invalid argument
2025-09-13 09:44:11,725 [INFO] __main__: 接收到信号 2停止推流
2025-09-13 09:44:11,726 [INFO] __main__: 停止所有推流...
2025-09-13 09:44:11,738 [INFO] __main__: 拉流进程已停止
2025-09-13 09:46:22,098 [INFO] httpx: HTTP Request: GET https://live.douyin.com/848698179845 "HTTP/2 200 OK"
2025-09-13 09:46:24,076 [INFO] httpx: HTTP Request: HEAD http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332782&major_anchor_level=common&sign=4c8db99e7a0c26a419faf2a1e34eae74&t_id=037-20250913094621681A1B914C390282BA5E-Rxo2Sa&codec=h264 "HTTP/1.1 200 OK"
2025-09-13 09:46:24,081 [INFO] __main__: 实际直播流: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332782&major_anchor_level=common&sign=4c8db99e7a0c26a419faf2a1e34eae74&t_id=037-20250913094621681A1B914C390282BA5E-Rxo2Sa&codec=h264
2025-09-13 09:46:24,083 [INFO] __main__: [抖音_ea368365] 启动推流,目标 YouTube Key: qxvb-r47b-r5ju-6ud3-6k7z
2025-09-13 09:46:24,084 [INFO] __main__: [抖音_ea368365] 实际直播流: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332782&major_anchor_level=common&sign=4c8db99e7a0c26a419faf2a1e34eae74&t_id=037-20250913094621681A1B914C390282BA5E-Rxo2Sa&codec=h264
2025-09-13 09:46:26,500 [INFO] __main__: [抖音_ea368365] 拉流进程启动PID: 9880
2025-09-13 09:46:29,093 [WARNING] __main__: [抖音_ea368365] 初始缓冲不足,推流延迟启动
2025-09-13 09:46:39,096 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 6000k -> 5800k (缓冲比例: 13.60)
2025-09-13 09:46:49,098 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 5800k -> 5600k (缓冲比例: 19.27)
2025-09-13 09:46:59,100 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 5600k -> 5400k (缓冲比例: 27.73)
2025-09-13 09:47:09,102 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 5400k -> 5200k (缓冲比例: 33.30)
2025-09-13 09:47:19,104 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 5200k -> 5000k (缓冲比例: 33.33)
2025-09-13 09:47:29,105 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 5000k -> 4800k (缓冲比例: 33.33)
2025-09-13 09:47:39,107 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 4800k -> 4600k (缓冲比例: 33.33)
2025-09-13 09:47:49,109 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 4600k -> 4400k (缓冲比例: 33.33)
2025-09-13 09:47:59,112 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 4400k -> 4200k (缓冲比例: 33.33)
2025-09-13 09:48:09,115 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 4200k -> 4000k (缓冲比例: 33.33)
2025-09-13 09:48:19,120 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 4000k -> 3800k (缓冲比例: 33.33)
2025-09-13 09:48:29,123 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 3800k -> 3600k (缓冲比例: 33.33)
2025-09-13 09:48:39,127 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 3600k -> 3400k (缓冲比例: 33.33)
2025-09-13 09:48:49,129 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 3400k -> 3200k (缓冲比例: 33.33)
2025-09-13 09:48:59,132 [INFO] __main__: [抖音_ea368365] [ABR] 调整码率: 3200k -> 3000k (缓冲比例: 33.33)
2025-09-13 09:49:35,107 [INFO] __main__: 接收到信号 2停止推流
2025-09-13 09:49:35,108 [INFO] __main__: 停止所有推流...
2025-09-13 09:49:35,119 [INFO] __main__: 拉流进程已停止
2025-09-13 09:49:39,251 [INFO] httpx: HTTP Request: GET https://live.douyin.com/848698179845 "HTTP/2 200 OK"
2025-09-13 09:49:40,464 [INFO] httpx: HTTP Request: HEAD http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332979&major_anchor_level=common&sign=1b74463ce878bc0174a578edcfa719c1&t_id=037-202509130949385D15825682C4A5F8F47C-iIt4V6&codec=h264 "HTTP/1.1 200 OK"
2025-09-13 09:49:40,469 [INFO] __main__: 实际直播流: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332979&major_anchor_level=common&sign=1b74463ce878bc0174a578edcfa719c1&t_id=037-202509130949385D15825682C4A5F8F47C-iIt4V6&codec=h264
2025-09-13 09:49:40,470 [INFO] __main__: [抖音_d46e59a8] 启动推流,目标 YouTube Key: qxvb-r47b-r5ju-6ud3-6k7z
2025-09-13 09:49:40,470 [INFO] __main__: [抖音_d46e59a8] 实际直播流: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332979&major_anchor_level=common&sign=1b74463ce878bc0174a578edcfa719c1&t_id=037-202509130949385D15825682C4A5F8F47C-iIt4V6&codec=h264
2025-09-13 09:49:42,555 [INFO] __main__: [抖音_d46e59a8] 拉流进程启动PID: 20372
2025-09-13 09:49:50,472 [WARNING] __main__: [抖音_d46e59a8] 初始缓冲不足(队列大小: 0推流延迟启动
2025-09-13 09:50:00,475 [INFO] __main__: [抖音_d46e59a8] [ABR] 调整码率: 6000k -> 5900k (缓冲比例: 7.03)
2025-09-13 09:50:10,476 [INFO] __main__: [抖音_d46e59a8] [ABR] 调整码率: 5900k -> 5800k (缓冲比例: 18.30)
2025-09-13 09:50:20,478 [INFO] __main__: [抖音_d46e59a8] [ABR] 调整码率: 5800k -> 5700k (缓冲比例: 23.90)
2025-09-13 09:50:30,480 [INFO] __main__: [抖音_d46e59a8] [ABR] 调整码率: 5700k -> 5600k (缓冲比例: 30.57)
2025-09-13 09:50:40,481 [INFO] __main__: [抖音_d46e59a8] [ABR] 调整码率: 5600k -> 5500k (缓冲比例: 33.33)
2025-09-13 09:50:42,864 [INFO] __main__: 接收到信号 2停止推流
2025-09-13 09:50:42,866 [INFO] __main__: 停止所有推流...
2025-09-13 09:50:42,877 [INFO] __main__: 拉流进程已停止

0
http_clients/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
import httpx
from typing import Dict, Any
import utils
OptionalStr = str | None
OptionalDict = Dict[str, Any] | None
async def async_req(
url: str,
proxy_addr: OptionalStr = None,
headers: OptionalDict = None,
data: dict | bytes | None = None,
json_data: dict | list | None = None,
timeout: int = 20,
redirect_url: bool = False,
return_cookies: bool = False,
include_cookies: bool = False,
abroad: bool = False,
content_conding: str = 'utf-8',
verify: bool = False,
http2: bool = True
) -> OptionalDict | OptionalStr | tuple:
if headers is None:
headers = {}
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
if data or json_data:
async with httpx.AsyncClient(proxy=proxy_addr, timeout=timeout, verify=verify, http2=http2) as client:
response = await client.post(url, data=data, json=json_data, headers=headers)
else:
async with httpx.AsyncClient(proxy=proxy_addr, timeout=timeout, verify=verify, http2=http2) as client:
response = await client.get(url, headers=headers, follow_redirects=True)
if redirect_url:
return str(response.url)
elif return_cookies:
cookies_dict = {name: value for name, value in response.cookies.items()}
return (response.text, cookies_dict) if include_cookies else cookies_dict
else:
resp_str = response.text
except Exception as e:
resp_str = str(e)
return resp_str
async def get_response_status(url: str, proxy_addr: OptionalStr = None, headers: OptionalDict = None,
timeout: int = 10, abroad: bool = False, verify: bool = False, http2=False) -> bool:
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
async with httpx.AsyncClient(proxy=proxy_addr, timeout=timeout, verify=verify) as client:
response = await client.head(url, headers=headers, follow_redirects=True)
return response.status_code == 200
except Exception as e:
print(e)
return False

88
http_clients/sync_http.py Normal file
View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
import gzip
import urllib.parse
import urllib.error
import requests
import ssl
import json
import urllib.request
no_proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(no_proxy_handler)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
OptionalStr = str | None
OptionalDict = dict | None
def sync_req(
url: str,
proxy_addr: OptionalStr = None,
headers: OptionalDict = None,
data: dict | bytes | None = None,
json_data: dict | list | None = None,
timeout: int = 20,
redirect_url: bool = False,
abroad: bool = False,
content_conding: str = 'utf-8'
) -> str:
if headers is None:
headers = {}
try:
if proxy_addr:
proxies = {
'http': proxy_addr,
'https': proxy_addr
}
if data or json_data:
response = requests.post(
url, data=data, json=json_data, headers=headers, proxies=proxies, timeout=timeout
)
else:
response = requests.get(url, headers=headers, proxies=proxies, timeout=timeout)
if redirect_url:
return response.url
resp_str = response.text
else:
if data and not isinstance(data, bytes):
data = urllib.parse.urlencode(data).encode(content_conding)
if json_data and isinstance(json_data, (dict, list)):
data = json.dumps(json_data).encode(content_conding)
req = urllib.request.Request(url, data=data, headers=headers)
try:
if abroad:
response = urllib.request.urlopen(req, timeout=timeout)
else:
response = opener.open(req, timeout=timeout)
if redirect_url:
return response.url
content_encoding = response.info().get('Content-Encoding')
try:
if content_encoding == 'gzip':
with gzip.open(response, 'rt', encoding=content_conding) as gzipped:
resp_str = gzipped.read()
else:
resp_str = response.read().decode(content_conding)
finally:
response.close()
except urllib.error.HTTPError as e:
if e.code == 400:
resp_str = e.read().decode(content_conding)
else:
raise
except urllib.error.URLError as e:
print(f"URL Error: {e}")
raise
except Exception as e:
print(f"An error occurred: {e}")
raise
except Exception as e:
resp_str = str(e)
return resp_str

220
initializer.py Normal file
View File

@@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
"""
Author: Hmily
GitHub:https://github.com/ihmily
Copyright (c) 2024 by Hmily, All Rights Reserved.
"""
import os
import subprocess
import sys
import platform
import zipfile
from pathlib import Path
import requests
import re
import distro
from tqdm import tqdm
from logger import logger
current_platform = platform.system()
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
current_env_path = os.environ.get('PATH')
def unzip_file(zip_path: str | Path, extract_to: str | Path, delete: bool = True) -> None:
if not os.path.exists(extract_to):
os.makedirs(extract_to)
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_to)
if delete and os.path.exists(zip_path):
os.remove(zip_path)
def install_nodejs_windows():
try:
logger.warning("Node.js is not installed.")
logger.debug("Installing the stable version of Node.js for Windows...")
response = requests.get('https://nodejs.cn/download/')
if response.status_code == 200:
match = re.search('https://npmmirror.com/mirrors/node/(v.*?)/node-(v.*?)-x64.msi',
response.text)
if match:
version = match.group(1)
system_bit = 'x64' if '32' not in platform.machine() else 'x86'
url = f'https://npmmirror.com/mirrors/node/{version}/node-{version}-win-{system_bit}.zip'
else:
logger.error("Failed to retrieve the download URL for the latest version of Node.js...")
return
full_file_name = url.rsplit('/', maxsplit=1)[-1]
zip_file_path = Path(execute_dir) / full_file_name
if Path(zip_file_path).exists():
logger.debug("Node.js installation file already exists, start install...")
else:
response = requests.get(url, stream=True)
total_size = int(response.headers.get('Content-Length', 0))
block_size = 1024
with tqdm(total=total_size, unit="B", unit_scale=True,
ncols=100, desc=f'Downloading Node.js ({version})') as t:
with open(zip_file_path, 'wb') as f:
for data in response.iter_content(block_size):
t.update(len(data))
f.write(data)
unzip_file(zip_file_path, execute_dir)
extract_dir_path = str(zip_file_path).rsplit('.', maxsplit=1)[0]
f_path, f_name = os.path.splitext(zip_file_path)
new_extract_dir_path = Path(f_path).parent / 'node'
if Path(extract_dir_path).exists() and not Path(new_extract_dir_path).exists():
os.rename(extract_dir_path, new_extract_dir_path)
os.environ['PATH'] = execute_dir + '/node' + os.pathsep + current_env_path
result = subprocess.run(["node", "-v"], capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect')
else:
logger.debug('Node.js installation failed')
return True
else:
logger.error("Failed to retrieve the Node.js version page")
except Exception as e:
logger.error(f"type: {type(e).__name__}, Node.js installation failed {e}")
def install_nodejs_centos():
try:
logger.warning("Node.js is not installed.")
logger.debug("Installing the latest version of Node.js for CentOS...")
result = subprocess.run('curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/nodesource/rpm/setup_lts.x | '
'bash -', shell=True, capture_output=True)
if result.returncode != 0:
logger.error("Failed to run NodeSource installation script")
return
result = subprocess.run(['yum', 'install', '-y', 'epel-release'], capture_output=True)
if result.returncode != 0:
logger.error("Failed to install EPEL repository")
return
result = subprocess.run(['yum', 'install', '-y', 'nodejs'], capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
return True
else:
logger.error("Node.js installation failed")
except Exception as e:
logger.error(f"type: {type(e).__name__}, Node.js installation failed {e}")
def install_nodejs_ubuntu():
try:
logger.warning("Node.js is not installed.")
logger.debug("Installing the latest version of Node.js for Ubuntu...")
install_script = 'curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -'
result = subprocess.run(install_script, shell=True, capture_output=True)
if result.returncode != 0:
logger.error("Failed to run NodeSource installation script")
return
install_command = ['apt', 'install', '-y', 'nodejs']
result = subprocess.run(install_command, capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
return True
else:
logger.error("Node.js installation failed")
except Exception as e:
logger.error(f"type: {type(e).__name__}, Node.js installation failed, {e}")
def install_nodejs_mac():
logger.warning("Node.js is not installed.")
logger.debug("Installing the latest version of Node.js for macOS...")
try:
result = subprocess.run(["brew", "install", "node"], capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
return True
else:
logger.error("Node.js installation failed")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to install Node.js using Homebrew. {e}")
logger.error("Please install Node.js manually or check your Homebrew installation.")
except Exception as e:
logger.error(f"An unexpected error occurred: {e}")
def get_package_manager():
dist_id = distro.id()
if dist_id in ["centos", "fedora", "rhel", "amzn", "oracle", "scientific", "opencloudos", "alinux"]:
return "RHS"
else:
return "DBS"
def install_nodejs() -> bool:
if current_platform == "Windows":
return install_nodejs_windows()
elif current_platform == "Linux":
os_type = get_package_manager()
if os_type == "RHS":
return install_nodejs_centos()
else:
return install_nodejs_ubuntu()
elif current_platform == "Darwin":
return install_nodejs_mac()
else:
logger.debug(f"Node.js auto installation is not supported on this platform: {current_platform}. "
f"Please install Node.js manually.")
return False
def ensure_nodejs_installed(func):
def wrapper(*args, **kwargs):
try:
result = subprocess.run(['node', '-v'], capture_output=True)
version = result.stdout.strip()
if result.returncode == 0 and version:
return func(*args, **kwargs)
except FileNotFoundError:
pass
return False
def wrapped_func(*args, **kwargs):
if sys.version_info >= (3, 7):
res = wrapper(*args, **kwargs)
else:
res = wrapper(*args, **kwargs)
if not res:
install_nodejs()
res = wrapper(*args, **kwargs)
if not res:
raise RuntimeError("Node.js is not installed.")
return func(*args, **kwargs)
return wrapped_func
def check_nodejs_installed() -> bool:
try:
result = subprocess.run(['node', '-v'], capture_output=True)
version = result.stdout.strip()
if result.returncode == 0 and version:
return True
except FileNotFoundError:
pass
return False
def check_node() -> bool:
if not check_nodejs_installed():
return install_nodejs()

1
javascript/crypto-js.min.js vendored Normal file

File diff suppressed because one or more lines are too long

539
javascript/haixiu.js Normal file
View File

@@ -0,0 +1,539 @@
var closeGeetest = !1, _a123 = "haija1c7", _b2x = "xiuhc2a6", _c3y = "anchc3a5", _dx34 = "famic7a2", _hf_constants1 = "sowh1e", _hf_constants2 = "1000ha", _hf_constants3 = "butr12", _hf_constants4 = "2000h5", _gf_constants1 = "lehaaj", _gf_constants2 = "1000ax", _gf_constants3 = "lehaData"
let CryptoJS = null;
function EnmoliParamter() {
this._a123 = eval("_hf_constants1"),
this._b2x = eval("_hf_constants2"),
this._c3y = eval("_hf_constants3"),
this._dx34 = eval("_hf_constants4"),
this.getA123 = function() {
return this._a123
}
,
this.getB2X = function() {
return this._b2x
}
,
this.getC3Y = function() {
return this._c3y
}
,
this.getDX34 = function() {
return this._dx34
}
}
EnmoliParamter.prototype = {
aa: function(e, t) {
if (e === t)
return e;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.arity === t.arity && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ac: function(e, t) {
if (e === t)
return e;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.id === t.id && (e = CryptoJS.MD5(e) + ""),
e.arity1 === t.arity && e.string2 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ad: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.arity2 === t.arity && e.string3 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ae: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity3 === t.arity && e.string4 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
af: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity4 === t.arity && e.string5 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ah: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.arity6 === t.arity && e.string9 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ai: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.arity2 === t.arity5 && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
aj: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity44 === t.arity42 && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ak: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity21 === t.arity322 && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ax: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity22 === t.arity32 && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
az: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity42 === t.arity57 && e.string2 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
are_similar: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return !0;
return !0
}
return e
}
if (Array.isArray(t))
return e;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity === t.arity && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third);
case "function":
case "regexp":
return e;
default:
return !0
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e.second.string === t.second.string && "(string)" === t.second.id;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return e.second.string === t.second.string && "(string)" === e.second.id
}
return !1
},
ayz: function(e, t) {
if (e === t)
return e;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity42 === t.arity57 && e.string2 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return this.getA123().substring(4) + this.getB2X().substring(4) + this.getC3Y().substring(4) + this.getDX34().substring(4)
}
}
function EnmoliSubmiter() {}
EnmoliSubmiter.prototype = {
bsq: function(e) {
var t = this.pf(e)
, i = this.as(t);
return this.brm(i)
},
pf: function(e) {
var t = {};
for (var i in e)
"" !== e[i] && (t[i] = e[i]);
return t
},
as: function(e) {
for (var t = {}, i = Object.keys(e).sort(), o = 0; o < i.length; o++) {
var n = i[o];
t[n] = e[n]
}
return t
},
brm: function(e) {
var t = this.cls(e)
, i = new EnmoliParamter;
return this.pt(t, i.ayz(t, "showselfAnchorVisitorParameters"))
},
cls: function(e) {
var t = "";
for (var i in e)
t = t + i + "=" + e[i] + "&";
return t = t.substring(0, t.length - 1)
},
pt: function(e, t) {
var i = new EnmoliParamter;
return e += t,
i.az(i.ax(i.ak(i.aj(i.ai(i.ah(i.af(i.ae(i.ad(i.ac(i.aa(e, e + "01" + t), e + "escape" + t), e + "same"), e + "visitor"), "anchor"), e + "person"), e + "ax" + t), "ae" + t), e + "ax" + t), e + "inspect" + t), "af" + t)
},
bnu: function(e, t) {
for (var i = e.split("&"), o = 0; o < i.length; o++) {
var n = i[o].split("=");
2 == n.length && (t[n[0]] = encodeURIComponent($.trim(n[1])).toString())
}
},
bn: function(e, t) {
for (var i in e)
"object" == typeof e[i] ? t[i] = encodeURIComponent($.trim(JSON.stringify(e[i]))).toString() : t[i] = encodeURIComponent($.trim(e[i])).toString()
}
}
var enmoliSubmiter = new EnmoliSubmiter();
function sign(options, cryptoJSPath){
CryptoJS = require(cryptoJSPath);
return enmoliSubmiter.bsq(options);
}
module.exports = {
sign
};
// const options = {
// "accessToken": "pLXSC%252FXJ0asc1I21tVL5FYZhNJn2Zg6d7m94umCnpgL%252BuVm31GQvyw%253D%253D",
// "tku": "3000006",
// "c": "10138100100000",
// "_st1": "1728621076958"
// }
// const cryptoJSPath = './crypto-js.min.js'
// console.log(sign(options, cryptoJSPath))

425
javascript/liveme.js Normal file
View File

@@ -0,0 +1,425 @@
/**
* @author Hmily
* @createTime 2024-10-10
*/
const id = 1;
const r = `${new Date().getTime()}${id}`
const Am = "LM6000101139961122666757";
const rl = "undefined"
function createRandom(length = 32) {
let result = "";
const characters = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
for (let i = 0; i < length; ++i) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters.charAt(randomIndex);
}
return result;
}
function createSignature(input = "4l4m5") {
let signature = "";
let number = 0;
for (let i = 0; i < input.length; ++i) {
const charCode = input.charCodeAt(i);
if (charCode >= 48 && charCode <= 57) {
number = number * 10 + (charCode - 48);
} else {
if (number !== 0) {
signature += createRandom(number);
number = 0;
}
signature += String.fromCharCode(charCode);
}
}
if (number !== 0) {
signature += createRandom(number);
}
return signature;
}
function oC(e) {
return e && e.__esModule && Object.prototype.hasOwnProperty.call(e, "default") ? e.default : e
}
var Tm = {
exports: {}
}
, Sm = {
exports: {}
};
(function() {
var e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
, t = {
rotl: function(n, r) {
return n << r | n >>> 32 - r
},
rotr: function(n, r) {
return n << 32 - r | n >>> r
},
endian: function(n) {
if (n.constructor == Number)
return t.rotl(n, 8) & 16711935 | t.rotl(n, 24) & 4278255360;
for (var r = 0; r < n.length; r++)
n[r] = t.endian(n[r]);
return n
},
randomBytes: function(n) {
for (var r = []; n > 0; n--)
r.push(Math.floor(Math.random() * 256));
return r
},
bytesToWords: function(n) {
for (var r = [], s = 0, o = 0; s < n.length; s++,
o += 8)
r[o >>> 5] |= n[s] << 24 - o % 32;
return r
},
wordsToBytes: function(n) {
for (var r = [], s = 0; s < n.length * 32; s += 8)
r.push(n[s >>> 5] >>> 24 - s % 32 & 255);
return r
},
bytesToHex: function(n) {
for (var r = [], s = 0; s < n.length; s++)
r.push((n[s] >>> 4).toString(16)),
r.push((n[s] & 15).toString(16));
return r.join("")
},
hexToBytes: function(n) {
for (var r = [], s = 0; s < n.length; s += 2)
r.push(parseInt(n.substr(s, 2), 16));
return r
},
bytesToBase64: function(n) {
for (var r = [], s = 0; s < n.length; s += 3)
for (var o = n[s] << 16 | n[s + 1] << 8 | n[s + 2], i = 0; i < 4; i++)
s * 8 + i * 6 <= n.length * 8 ? r.push(e.charAt(o >>> 6 * (3 - i) & 63)) : r.push("=");
return r.join("")
},
base64ToBytes: function(n) {
n = n.replace(/[^A-Z0-9+\/]/ig, "");
for (var r = [], s = 0, o = 0; s < n.length; o = ++s % 4)
o != 0 && r.push((e.indexOf(n.charAt(s - 1)) & Math.pow(2, -2 * o + 8) - 1) << o * 2 | e.indexOf(n.charAt(s)) >>> 6 - o * 2);
return r
}
};
Sm.exports = t
}
)();
var iC = Sm.exports
, nl = {
utf8: {
stringToBytes: function(e) {
return nl.bin.stringToBytes(unescape(encodeURIComponent(e)))
},
bytesToString: function(e) {
return decodeURIComponent(escape(nl.bin.bytesToString(e)))
}
},
bin: {
stringToBytes: function(e) {
for (var t = [], n = 0; n < e.length; n++)
t.push(e.charCodeAt(n) & 255);
return t
},
bytesToString: function(e) {
for (var t = [], n = 0; n < e.length; n++)
t.push(String.fromCharCode(e[n]));
return t.join("")
}
}
}, sd = nl;
var aC = function(e) {
return e != null && (Cm(e) || lC(e) || !!e._isBuffer)
};
function Cm(e) {
return !!e.constructor && typeof e.constructor.isBuffer == "function" && e.constructor.isBuffer(e)
}
function lC(e) {
return typeof e.readFloatLE == "function" && typeof e.slice == "function" && Cm(e.slice(0, 0))
}
(function() {
var e = iC
, t = sd.utf8
, n = aC
, r = sd.bin
, s = function(o, i) {
o.constructor == String ? i && i.encoding === "binary" ? o = r.stringToBytes(o) : o = t.stringToBytes(o) : n(o) ? o = Array.prototype.slice.call(o, 0) : !Array.isArray(o) && o.constructor !== Uint8Array && (o = o.toString());
for (var a = e.bytesToWords(o), l = o.length * 8, c = 1732584193, u = -271733879, f = -1732584194, d = 271733878, m = 0; m < a.length; m++)
a[m] = (a[m] << 8 | a[m] >>> 24) & 16711935 | (a[m] << 24 | a[m] >>> 8) & 4278255360;
a[l >>> 5] |= 128 << l % 32,
a[(l + 64 >>> 9 << 4) + 14] = l;
for (var v = s._ff, w = s._gg, R = s._hh, y = s._ii, m = 0; m < a.length; m += 16) {
var b = c
, _ = u
, g = f
, C = d;
c = v(c, u, f, d, a[m + 0], 7, -680876936),
d = v(d, c, u, f, a[m + 1], 12, -389564586),
f = v(f, d, c, u, a[m + 2], 17, 606105819),
u = v(u, f, d, c, a[m + 3], 22, -1044525330),
c = v(c, u, f, d, a[m + 4], 7, -176418897),
d = v(d, c, u, f, a[m + 5], 12, 1200080426),
f = v(f, d, c, u, a[m + 6], 17, -1473231341),
u = v(u, f, d, c, a[m + 7], 22, -45705983),
c = v(c, u, f, d, a[m + 8], 7, 1770035416),
d = v(d, c, u, f, a[m + 9], 12, -1958414417),
f = v(f, d, c, u, a[m + 10], 17, -42063),
u = v(u, f, d, c, a[m + 11], 22, -1990404162),
c = v(c, u, f, d, a[m + 12], 7, 1804603682),
d = v(d, c, u, f, a[m + 13], 12, -40341101),
f = v(f, d, c, u, a[m + 14], 17, -1502002290),
u = v(u, f, d, c, a[m + 15], 22, 1236535329),
c = w(c, u, f, d, a[m + 1], 5, -165796510),
d = w(d, c, u, f, a[m + 6], 9, -1069501632),
f = w(f, d, c, u, a[m + 11], 14, 643717713),
u = w(u, f, d, c, a[m + 0], 20, -373897302),
c = w(c, u, f, d, a[m + 5], 5, -701558691),
d = w(d, c, u, f, a[m + 10], 9, 38016083),
f = w(f, d, c, u, a[m + 15], 14, -660478335),
u = w(u, f, d, c, a[m + 4], 20, -405537848),
c = w(c, u, f, d, a[m + 9], 5, 568446438),
d = w(d, c, u, f, a[m + 14], 9, -1019803690),
f = w(f, d, c, u, a[m + 3], 14, -187363961),
u = w(u, f, d, c, a[m + 8], 20, 1163531501),
c = w(c, u, f, d, a[m + 13], 5, -1444681467),
d = w(d, c, u, f, a[m + 2], 9, -51403784),
f = w(f, d, c, u, a[m + 7], 14, 1735328473),
u = w(u, f, d, c, a[m + 12], 20, -1926607734),
c = R(c, u, f, d, a[m + 5], 4, -378558),
d = R(d, c, u, f, a[m + 8], 11, -2022574463),
f = R(f, d, c, u, a[m + 11], 16, 1839030562),
u = R(u, f, d, c, a[m + 14], 23, -35309556),
c = R(c, u, f, d, a[m + 1], 4, -1530992060),
d = R(d, c, u, f, a[m + 4], 11, 1272893353),
f = R(f, d, c, u, a[m + 7], 16, -155497632),
u = R(u, f, d, c, a[m + 10], 23, -1094730640),
c = R(c, u, f, d, a[m + 13], 4, 681279174),
d = R(d, c, u, f, a[m + 0], 11, -358537222),
f = R(f, d, c, u, a[m + 3], 16, -722521979),
u = R(u, f, d, c, a[m + 6], 23, 76029189),
c = R(c, u, f, d, a[m + 9], 4, -640364487),
d = R(d, c, u, f, a[m + 12], 11, -421815835),
f = R(f, d, c, u, a[m + 15], 16, 530742520),
u = R(u, f, d, c, a[m + 2], 23, -995338651),
c = y(c, u, f, d, a[m + 0], 6, -198630844),
d = y(d, c, u, f, a[m + 7], 10, 1126891415),
f = y(f, d, c, u, a[m + 14], 15, -1416354905),
u = y(u, f, d, c, a[m + 5], 21, -57434055),
c = y(c, u, f, d, a[m + 12], 6, 1700485571),
d = y(d, c, u, f, a[m + 3], 10, -1894986606),
f = y(f, d, c, u, a[m + 10], 15, -1051523),
u = y(u, f, d, c, a[m + 1], 21, -2054922799),
c = y(c, u, f, d, a[m + 8], 6, 1873313359),
d = y(d, c, u, f, a[m + 15], 10, -30611744),
f = y(f, d, c, u, a[m + 6], 15, -1560198380),
u = y(u, f, d, c, a[m + 13], 21, 1309151649),
c = y(c, u, f, d, a[m + 4], 6, -145523070),
d = y(d, c, u, f, a[m + 11], 10, -1120210379),
f = y(f, d, c, u, a[m + 2], 15, 718787259),
u = y(u, f, d, c, a[m + 9], 21, -343485551),
c = c + b >>> 0,
u = u + _ >>> 0,
f = f + g >>> 0,
d = d + C >>> 0
}
return e.endian([c, u, f, d])
};
s._ff = function(o, i, a, l, c, u, f) {
var d = o + (i & a | ~i & l) + (c >>> 0) + f;
return (d << u | d >>> 32 - u) + i
}
,
s._gg = function(o, i, a, l, c, u, f) {
var d = o + (i & l | a & ~l) + (c >>> 0) + f;
return (d << u | d >>> 32 - u) + i
}
,
s._hh = function(o, i, a, l, c, u, f) {
var d = o + (i ^ a ^ l) + (c >>> 0) + f;
return (d << u | d >>> 32 - u) + i
}
,
s._ii = function(o, i, a, l, c, u, f) {
var d = o + (a ^ (i | ~l)) + (c >>> 0) + f;
return (d << u | d >>> 32 - u) + i
}
,
s._blocksize = 16,
s._digestsize = 16,
Tm.exports = function(o, i) {
if (o == null)
throw new Error("Illegal argument " + o);
var a = e.wordsToBytes(s(o, i));
return i && i.asBytes ? a : i && i.asString ? r.bytesToString(a) : e.bytesToHex(a)
}
}
)();
var cC = Tm.exports;
var t = {
utf8: {
stringToBytes: function(e) {
return nl.bin.stringToBytes(unescape(encodeURIComponent(e)))
},
bytesToString: function(e) {
return decodeURIComponent(escape(nl.bin.bytesToString(e)))
}
},
bin: {
stringToBytes: function(e) {
for (var t = [], n = 0; n < e.length; n++)
t.push(e.charCodeAt(n) & 255);
return t
},
bytesToString: function(e) {
for (var t = [], n = 0; n < e.length; n++)
t.push(String.fromCharCode(e[n]));
return t.join("")
}
}
};
const hC = (e, t, n=!1) => {
if (t.params) {
const o = {};
Object.keys(t.params).forEach(i => {
t.params[i] !== void 0 && t.params[i] !== null && (o[i] = t.params[i])
}
),
t.params = o
}
let r = {};
const s = t.method.toLowerCase();
if (s === "get")
t.params = Object.assign({}, e, t.params || {});
else if (s === "post")
if (typeof t.data == "string") {
let o;
t.data.split("&").forEach(i => {
o = i.split("="),
r[o[0]] = o[1]
}
),
r = Object.assign({}, e, r),
t.data = Object.keys(r).map(i => `${i}=${r[i]}`).join("&")
} else
r = Object.assign(r, e, t.data || {}),
t.data = r;
return n ? t : (r = Object.assign({}, t.params, r),
r)
}
const Rm = oC(cC);
const s = Rm(r);
pC = e => {
let t = Object.keys(e).sort().map(n => {
function r(s) {
return Array.isArray(s) ? s.join(",") : typeof s === "object" ? JSON.stringify(s) : s
}
return n + r(e[n])
}
).join("");
return t += Am + e.lm_s_ts + rl,
Rm(t)
}
// final encryption function
let CryptoJS = null;
lm_s_key = atob('ZGQ0NmRiYjQ0MmI2ZTRiYTgxN2Q2MzQ3ZDJkZGY0OTM=');
function requestSign(signParams, cryptoJSPath) {
let sKey = Object.keys(signParams).sort().map(key => {
function getValue(val) {
if (Array.isArray(val)) {
return val.join(',');
}
if (typeof val === 'object') {
return JSON.stringify(val);
}
return val;
}
return key + getValue(signParams[key]);
}).join('');
sKey += signParams.lm_s_id + signParams.lm_s_ts + lm_s_key;
console.log(`sKey: ${sKey}`);
CryptoJS = require(cryptoJSPath);
return CryptoJS.MD5(sKey).toString();
}
function sign(videoid, cryptoJSPath, platform='web'){
const vali = createSignature();
const data_e = {
lm_s_id: Am,
lm_s_ts: r,
lm_s_str: s,
lm_s_ver: 1,
h5: 1
};
/* data_e example value
const data_e = {
lm_s_id: Am,
lm_s_ts: "17284909009151",
lm_s_str: "88f9777231dc2d6ac462a1d7ebf5f54e",
lm_s_ver: 1,
h5: 1
};
*/
console.log("data_e:",data_e);
data_i = {
...data_e,
_time: new Date().valueOf(),
thirdchannel: 6,
videoid: videoid,
area: 'zh',
vali: vali
}
console.log("data_i:",data_i);
// fake lm_s_sign param value
let lm_s_sign = pC(data_i);
console.log(`fake lm_s_sign: ${lm_s_sign}`);
//finnal request params
/*
signParams = {
"alias": "liveme",
"tongdun_black_box": "iWPU21728483558afruvSVo6x0",
"os": "android",
"lm_s_id": "LM6000101139961122666757",
"lm_s_ts": "17284909009151",
"lm_s_str": "88f9777231dc2d6ac462a1d7ebf5f54e",
"lm_s_ver": 1,
"h5": 1,
"_time": 1728490664651,
"thirdchannel": 6,
"videoid": "17284844223282059697",
"area": "zh",
"vali": "zH8SlBwnCm4AZWp"
}#
//result: 4eaf71a1ec19b49b7267e4d16e007105
*/
signParams = {
"alias": "liveme",
"tongdun_black_box": "",
"os": platform,
...data_i
}
console.log("signParams: ", signParams);
lm_s_sign = requestSign(signParams, cryptoJSPath);
console.log(`\x1b[32mfinal lm_s_sign: \x1b[0m${lm_s_sign}\n`);
data = {
...signParams,
lm_s_sign
}
return data;
}
module.exports = {
sign
};

143
javascript/migu.js Normal file
View File

@@ -0,0 +1,143 @@
/**
* Function to get the ddCalcu parameter value
* @param {string} inputUrl - The original URL before encryption
* @returns {Promise<string>} - Returns the calculated ddCalcu value
*/
async function getDdCalcu(inputUrl) {
let wasmInstance = null;
let memory_p = null; // Uint8Array view
let memory_h = null; // Uint32Array view
// Fixed parameter
const f = 'PBTxuWiTEbUPPFcpyxs0ww==';
// Utility function: Convert string to UTF-8 in memory
function stringToUTF8(string, offset) {
const encoder = new TextEncoder();
const encoded = encoder.encode(string);
for (let i = 0; i < encoded.length; i++) {
memory_p[offset + i] = encoded[i];
}
memory_p[offset + encoded.length] = 0; // Null-terminate
}
// Utility function: Read UTF-8 string from memory address
function UTF8ToString(offset) {
let s = '';
let i = 0;
while (memory_p[offset + i]) {
s += String.fromCharCode(memory_p[offset + i]);
i++;
}
return s;
}
// WASM import function stubs
function a(e, t, r, n) {
let s = 0;
for (let i = 0; i < r; i++) {
const d = memory_h[t + 4 >> 2];
t += 8;
s += d;
}
memory_h[n >> 2] = s;
return 0;
}
function b() {}
function c() {}
// Step 1: Retrieve playerVersion
const settingsResp = await fetch('https://app-sc.miguvideo.com/common/v1/settings/H5_DetailPage');
const settingsData = await settingsResp.json();
const playerVersion = JSON.parse(settingsData.body.paramValue).playerVersion;
// Step 2: Load WASM module
const wasmUrl = `https://www.miguvideo.com/mgs/player/prd/${playerVersion}/dist/mgprtcl.wasm`;
const wasmResp = await fetch(wasmUrl);
if (!wasmResp.ok) throw new Error("Failed to download WASM");
const wasmBuffer = await wasmResp.arrayBuffer();
const importObject = {
a: { a, b, c }
};
const { instance } = await WebAssembly.instantiate(wasmBuffer, importObject);
wasmInstance = instance;
const memory = wasmInstance.exports.d;
memory_p = new Uint8Array(memory.buffer);
memory_h = new Uint32Array(memory.buffer);
const exports = {
CallInterface1: wasmInstance.exports.h,
CallInterface2: wasmInstance.exports.i,
CallInterface3: wasmInstance.exports.j,
CallInterface4: wasmInstance.exports.k,
CallInterface6: wasmInstance.exports.m,
CallInterface7: wasmInstance.exports.n,
CallInterface8: wasmInstance.exports.o,
CallInterface9: wasmInstance.exports.p,
CallInterface10: wasmInstance.exports.q,
CallInterface11: wasmInstance.exports.r,
CallInterface14: wasmInstance.exports.t,
malloc: wasmInstance.exports.u,
};
const parsedUrl = new URL(inputUrl);
const query = Object.fromEntries(parsedUrl.searchParams);
const o = query.userid || '';
const a_val = query.timestamp || '';
const s = query.ProgramID || '';
const u = query.Channel_ID || '';
const v = query.puData || '';
// Allocate memory
const d = exports.malloc(o.length + 1);
const h = exports.malloc(a_val.length + 1);
const y = exports.malloc(s.length + 1);
const m = exports.malloc(u.length + 1);
const g = exports.malloc(v.length + 1);
const b_val = exports.malloc(f.length + 1);
const E = exports.malloc(128);
const T = exports.malloc(128);
// Write data to memory
stringToUTF8(o, d);
stringToUTF8(a_val, h);
stringToUTF8(s, y);
stringToUTF8(u, m);
stringToUTF8(v, g);
stringToUTF8(f, b_val);
// Call interface functions
const S = exports.CallInterface6(); // Create context
exports.CallInterface1(S, y, s.length);
exports.CallInterface10(S, h, a_val.length);
exports.CallInterface9(S, d, o.length);
exports.CallInterface3(S, 0, 0);
exports.CallInterface11(S, 0, 0);
exports.CallInterface8(S, g, v.length);
exports.CallInterface2(S, m, u.length);
exports.CallInterface14(S, b_val, f.length, T, 128);
const w = UTF8ToString(T);
const I = exports.malloc(w.length + 1);
stringToUTF8(w, I);
exports.CallInterface7(S, I, w.length);
exports.CallInterface4(S, E, 128);
return UTF8ToString(E);
}
const url = process.argv[2];
getDdCalcu(url).then(result => {
console.log(result);
}).catch(err => {
console.error(err);
process.exit(1);
});

78
javascript/taobao-sign.js Normal file
View File

@@ -0,0 +1,78 @@
function sign(e) {
function t(e, t) {
return e << t | e >>> 32 - t
}
function o(e, t) {
var o, n, r, i, a;
return r = 2147483648 & e,
i = 2147483648 & t,
a = (1073741823 & e) + (1073741823 & t),
(o = 1073741824 & e) & (n = 1073741824 & t) ? 2147483648 ^ a ^ r ^ i : o | n ? 1073741824 & a ? 3221225472 ^ a ^ r ^ i : 1073741824 ^ a ^ r ^ i : a ^ r ^ i
}
function n(e, n, r, i, a, s, u) {
return o(t(e = o(e, o(o(function(e, t, o) {
return e & t | ~e & o
}(n, r, i), a), u)), s), n)
}
function r(e, n, r, i, a, s, u) {
return o(t(e = o(e, o(o(function(e, t, o) {
return e & o | t & ~o
}(n, r, i), a), u)), s), n)
}
function i(e, n, r, i, a, s, u) {
return o(t(e = o(e, o(o(function(e, t, o) {
return e ^ t ^ o
}(n, r, i), a), u)), s), n)
}
function a(e, n, r, i, a, s, u) {
return o(t(e = o(e, o(o(function(e, t, o) {
return t ^ (e | ~o)
}(n, r, i), a), u)), s), n)
}
function s(e) {
var t, o = "", n = "";
for (t = 0; 3 >= t; t++)
o += (n = "0" + (e >>> 8 * t & 255).toString(16)).substr(n.length - 2, 2);
return o
}
var u, l, d, c, p, f, h, m, y, g;
for (g = function(e) {
for (var t = e.length, o = t + 8, n = 16 * ((o - o % 64) / 64 + 1), r = Array(n - 1), i = 0, a = 0; t > a; )
i = a % 4 * 8,
r[(a - a % 4) / 4] |= e.charCodeAt(a) << i,
a++;
return i = a % 4 * 8,
r[(a - a % 4) / 4] |= 128 << i,
r[n - 2] = t << 3,
r[n - 1] = t >>> 29,
r
}(e = function(e) {
var t = String.fromCharCode;
e = e.replace(/\r\n/g, "\n");
for (var o, n = "", r = 0; r < e.length; r++)
128 > (o = e.charCodeAt(r)) ? n += t(o) : o > 127 && 2048 > o ? (n += t(o >> 6 | 192),
n += t(63 & o | 128)) : (n += t(o >> 12 | 224),
n += t(o >> 6 & 63 | 128),
n += t(63 & o | 128));
return n
}(e)),
f = 1732584193,
h = 4023233417,
m = 2562383102,
y = 271733878,
u = 0; u < g.length; u += 16)
l = f,
d = h,
c = m,
p = y,
h = a(h = a(h = a(h = a(h = i(h = i(h = i(h = i(h = r(h = r(h = r(h = r(h = n(h = n(h = n(h = n(h, m = n(m, y = n(y, f = n(f, h, m, y, g[u + 0], 7, 3614090360), h, m, g[u + 1], 12, 3905402710), f, h, g[u + 2], 17, 606105819), y, f, g[u + 3], 22, 3250441966), m = n(m, y = n(y, f = n(f, h, m, y, g[u + 4], 7, 4118548399), h, m, g[u + 5], 12, 1200080426), f, h, g[u + 6], 17, 2821735955), y, f, g[u + 7], 22, 4249261313), m = n(m, y = n(y, f = n(f, h, m, y, g[u + 8], 7, 1770035416), h, m, g[u + 9], 12, 2336552879), f, h, g[u + 10], 17, 4294925233), y, f, g[u + 11], 22, 2304563134), m = n(m, y = n(y, f = n(f, h, m, y, g[u + 12], 7, 1804603682), h, m, g[u + 13], 12, 4254626195), f, h, g[u + 14], 17, 2792965006), y, f, g[u + 15], 22, 1236535329), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 1], 5, 4129170786), h, m, g[u + 6], 9, 3225465664), f, h, g[u + 11], 14, 643717713), y, f, g[u + 0], 20, 3921069994), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 5], 5, 3593408605), h, m, g[u + 10], 9, 38016083), f, h, g[u + 15], 14, 3634488961), y, f, g[u + 4], 20, 3889429448), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 9], 5, 568446438), h, m, g[u + 14], 9, 3275163606), f, h, g[u + 3], 14, 4107603335), y, f, g[u + 8], 20, 1163531501), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 13], 5, 2850285829), h, m, g[u + 2], 9, 4243563512), f, h, g[u + 7], 14, 1735328473), y, f, g[u + 12], 20, 2368359562), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 5], 4, 4294588738), h, m, g[u + 8], 11, 2272392833), f, h, g[u + 11], 16, 1839030562), y, f, g[u + 14], 23, 4259657740), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 1], 4, 2763975236), h, m, g[u + 4], 11, 1272893353), f, h, g[u + 7], 16, 4139469664), y, f, g[u + 10], 23, 3200236656), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 13], 4, 681279174), h, m, g[u + 0], 11, 3936430074), f, h, g[u + 3], 16, 3572445317), y, f, g[u + 6], 23, 76029189), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 9], 4, 3654602809), h, m, g[u + 12], 11, 3873151461), f, h, g[u + 15], 16, 530742520), y, f, g[u + 2], 23, 3299628645), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 0], 6, 4096336452), h, m, g[u + 7], 10, 1126891415), f, h, g[u + 14], 15, 2878612391), y, f, g[u + 5], 21, 4237533241), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 12], 6, 1700485571), h, m, g[u + 3], 10, 2399980690), f, h, g[u + 10], 15, 4293915773), y, f, g[u + 1], 21, 2240044497), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 8], 6, 1873313359), h, m, g[u + 15], 10, 4264355552), f, h, g[u + 6], 15, 2734768916), y, f, g[u + 13], 21, 1309151649), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 4], 6, 4149444226), h, m, g[u + 11], 10, 3174756917), f, h, g[u + 2], 15, 718787259), y, f, g[u + 9], 21, 3951481745),
f = o(f, l),
h = o(h, d),
m = o(m, c),
y = o(y, p);
return (s(f) + s(h) + s(m) + s(y)).toLowerCase()
}
// 正确sign值05748e8359cd3e6deaab02d15caafc11
// var sg =sign('5655b7041ca049730330701082886efd&1719411639403&12574478&{"componentKey":"wp_pc_shop_basic_info","params":"{\\"memberId\\":\\"b2b-22133374292418351a\\"}"}')
// console.log(sg)

564
javascript/x-bogus.js Normal file

File diff suppressed because one or more lines are too long

201
live.py Normal file
View File

@@ -0,0 +1,201 @@
import os
import subprocess
import threading
import asyncio
import uuid
import queue
import time
import signal
import sys
import shutil
import spider
import stream
class AdaptiveHLS2YouTube:
def __init__(self, max_bitrate=2500, min_bitrate=1500, audio_bitrate=128,
delay_sec=30, segment_time=2, check_interval=5):
self.processes = {}
self.stop_flag = threading.Event()
self.delay_sec = delay_sec
self.segment_time = segment_time
self.check_interval = check_interval
self.buffer_queue = queue.Queue()
self.max_bitrate = max_bitrate
self.min_bitrate = min_bitrate
self.audio_bitrate = audio_bitrate
self.current_bitrate = max_bitrate
def start_stream(self, real_url, youtube_key, room_name=None):
room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}"
print(f"[{room_name}] 启动推流,目标 YouTube Key: {youtube_key}")
threading.Thread(target=self._pull_thread, args=(real_url,), daemon=True).start()
threading.Thread(target=self._push_thread, args=(youtube_key, room_name), daemon=True).start()
threading.Thread(target=self._abr_thread, daemon=True).start()
def _pull_thread(self, real_url):
cmd = [
"ffmpeg",
"-rw_timeout", "50000000",
"-user_agent", "Mozilla/5.0",
"-headers", "Referer: https://www.douyin.com/",
"-fflags", "+nobuffer+genpts",
"-flags", "low_delay",
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "10",
"-i", real_url,
"-c", "copy",
"-f", "mpegts",
"-"
]
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
print("[拉流] FFmpeg 启动成功")
while not self.stop_flag.is_set():
data = proc.stdout.read(188*50)
if not data:
time.sleep(0.05)
continue
self.buffer_queue.put(data)
except Exception as e:
print(f"拉流异常: {e}")
def _push_thread(self, youtube_key, room_name):
min_buffer = max(1, self.delay_sec // self.segment_time)
while not self.stop_flag.is_set():
if self.buffer_queue.qsize() < min_buffer:
time.sleep(0.05)
continue
cmd = [
"ffmpeg",
"-re",
"-f", "mpegts",
"-i", "-",
"-c:v", "libx264",
"-preset", "veryfast",
"-tune", "zerolatency",
"-b:v", f"{self.current_bitrate}k",
"-maxrate", f"{self.current_bitrate}k",
"-bufsize", f"{self.current_bitrate*2}k",
"-r", "30",
"-g", "60",
"-c:a", "aac",
"-b:a", f"{self.audio_bitrate}k",
"-ar", "44100",
"-ac", "2",
"-f", "flv",
"-rtmp_live", "live",
f"rtmp://a.rtmp.youtube.com/live2/{youtube_key}"
]
try:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
self.processes[room_name] = proc
def read_stderr():
while proc.poll() is None:
line = proc.stderr.readline()
if line:
print(f"[{room_name} FFmpeg]: {line.decode(errors='ignore').strip()}")
threading.Thread(target=read_stderr, daemon=True).start()
while not self.stop_flag.is_set():
if not self.buffer_queue.empty():
data = self.buffer_queue.get()
proc.stdin.write(data)
proc.stdin.flush()
else:
time.sleep(0.01)
except Exception as e:
print(f"[{room_name}] 推流异常: {e}")
def _abr_thread(self):
while not self.stop_flag.is_set():
queue_len = self.buffer_queue.qsize()
if queue_len > 100:
new_bitrate = max(self.min_bitrate, self.current_bitrate - 300)
elif queue_len < 30:
new_bitrate = min(self.max_bitrate, self.current_bitrate + 300)
else:
new_bitrate = self.current_bitrate
if new_bitrate != self.current_bitrate:
print(f"[ABR] 调整码率: {self.current_bitrate}k -> {new_bitrate}k")
self.current_bitrate = new_bitrate
time.sleep(self.check_interval)
def stop_all(self):
print("停止所有推流...")
self.stop_flag.set()
for proc in self.processes.values():
if proc.poll() is None:
proc.terminate()
# ----------------- 抖音解析逻辑 -----------------
async def get_douyin_real_url(record_url, record_quality="原画"):
if 'v.douyin.com' not in record_url and '/user/' not in record_url:
json_data = await spider.get_douyin_stream_data(url=record_url)
else:
json_data = await spider.get_douyin_app_stream_data(url=record_url)
port_info = await stream.get_douyin_stream_url(json_data, record_quality, None)
return port_info.get("record_url")
# ----------------- 启动推流 -----------------
def start_douyin_to_youtube(record_url, youtube_key, room_name=None, delay_sec=30):
try:
real_url = asyncio.run(get_douyin_real_url(record_url))
except Exception as e:
print(f"获取真实直播流失败: {e}")
return
if not real_url:
print("获取真实直播流失败")
return
print(f"实际直播流: {real_url}")
hls2yt = AdaptiveHLS2YouTube(delay_sec=delay_sec)
hls2yt.start_stream(real_url, youtube_key, room_name)
return hls2yt
# ----------------- 控制台监控面板 -----------------
def monitor_panel_thread(hls_instance, interval=2, max_width=50):
term_width, _ = shutil.get_terminal_size()
bar_width = min(max_width, term_width - 30)
while not hls_instance.stop_flag.is_set():
queue_len = hls_instance.buffer_queue.qsize()
bitrate = hls_instance.current_bitrate
active_push = any(proc and proc.poll() is None for proc in hls_instance.processes.values())
queue_bar_len = min(bar_width, queue_len)
queue_bar = "#" * queue_bar_len + "-" * (bar_width - queue_bar_len)
print("\r", end="")
print(f"[监控] 队列: [{queue_bar}] {queue_len:4} | 码率: {bitrate:4}k | 推流活跃: {active_push}", end="")
time.sleep(interval)
# ----------------- 示例 -----------------
if __name__ == "__main__":
DOUYIN_URL = "https://live.douyin.com/848698179845"
YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=30)
if not hls_instance:
print("推流实例创建失败,程序退出")
sys.exit(1)
threading.Thread(target=monitor_panel_thread, args=(hls_instance,), daemon=True).start()
def signal_handler(sig, frame):
if hls_instance:
hls_instance.stop_all()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
print("程序已启动,按 Ctrl+C 停止...")
while True:
time.sleep(60)

109
liveback Normal file
View File

@@ -0,0 +1,109 @@
import os
import subprocess
import asyncio
import uuid
import signal
import sys
import spider
import stream
import threading
import time
class HLS2YouTube:
def __init__(self):
self.processes = {}
self.stop_flag = threading.Event()
def start_stream(self, real_url, youtube_key, room_name=None):
room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}"
print(f"[{room_name}] 启动推流,目标 YouTube Key: {youtube_key}")
threading.Thread(target=self._stream_thread, args=(real_url, youtube_key, room_name), daemon=True).start()
def _stream_thread(self, real_url, youtube_key, room_name):
while not self.stop_flag.is_set():
cmd = [
"ffmpeg",
"-rw_timeout", "50000000",
"-user_agent", "Mozilla/5.0",
"-headers", "Referer: https://www.douyin.com/",
"-fflags", "+nobuffer+genpts",
"-flags", "low_delay",
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "10",
"-i", real_url,
"-c:v", "libx264",
"-preset", "veryfast",
"-tune", "zerolatency",
"-b:v", "3000k",
"-maxrate", "3000k",
"-bufsize", "6000k",
"-r", "30",
"-g", "60",
"-c:a", "aac",
"-b:a", "128k",
"-f", "flv",
f"rtmp://a.rtmp.youtube.com/live2/{youtube_key}"
]
try:
proc = subprocess.Popen(cmd)
self.processes[room_name] = proc
while proc.poll() is None and not self.stop_flag.is_set():
time.sleep(1)
if proc.poll() is None:
proc.terminate()
print(f"[{room_name}] 推流中断5秒后重连")
time.sleep(5)
except Exception as e:
print(f"[{room_name}] 推流异常: {e}, 5秒后重试")
time.sleep(5)
def stop_all(self):
print("停止所有推流...")
self.stop_flag.set()
for room_name, proc in self.processes.items():
if proc.poll() is None:
proc.terminate()
# ----------------- 抖音解析逻辑 -----------------
async def get_douyin_real_url(record_url, record_quality="原画"):
if 'v.douyin.com' not in record_url and '/user/' not in record_url:
json_data = await spider.get_douyin_stream_data(url=record_url)
else:
json_data = await spider.get_douyin_app_stream_data(url=record_url)
port_info = await stream.get_douyin_stream_url(json_data, record_quality, None)
return port_info.get("record_url")
# ----------------- 启动推流 -----------------
def start_douyin_to_youtube(record_url, youtube_key, room_name=None):
try:
real_url = asyncio.run(get_douyin_real_url(record_url))
except Exception as e:
print(f"获取真实直播流失败: {e}")
return
if not real_url:
print("获取真实直播流失败")
return
print(f"实际直播流: {real_url}")
hls2yt = HLS2YouTube()
hls2yt.start_stream(real_url, youtube_key, room_name)
return hls2yt
# ----------------- 示例 -----------------
if __name__ == "__main__":
DOUYIN_URL = "https://live.douyin.com/104914538985" # 抖音直播 URL
YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" # YouTube 直播 Key
hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY)
def signal_handler(sig, frame):
if hls_instance:
hls_instance.stop_all()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
while True:
time.sleep(60)

43
logger.py Normal file
View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
import os
import sys
from loguru import logger
logger.remove()
custom_format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> - <level>{message}</level>"
logger.add(
sink=sys.stderr,
format=custom_format,
level="DEBUG",
colorize=True,
enqueue=True
)
script_path = os.path.split(os.path.realpath(sys.argv[0]))[0]
logger.add(
f"{script_path}/logs/streamget.log",
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
filter=lambda i: i["level"].name != "INFO",
serialize=False,
enqueue=True,
retention=1,
rotation="300 KB",
encoding='utf-8'
)
logger.add(
f"{script_path}/logs/PlayURL.log",
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {message}",
filter=lambda i: i["level"].name == "INFO",
serialize=False,
enqueue=True,
retention=1,
rotation="300 KB",
encoding='utf-8'
)

0
logs/PlayURL.log Normal file
View File

0
logs/streamget.log Normal file
View File

2078
main.py Normal file

File diff suppressed because it is too large Load Diff

92
proxy.py Normal file
View File

@@ -0,0 +1,92 @@
import os
import sys
from enum import Enum, auto
from dataclasses import dataclass, field
from utils import logger
class ProxyType(Enum):
HTTP = auto()
HTTPS = auto()
SOCKS = auto()
@dataclass(frozen=True)
class ProxyInfo:
ip: str = field(default="", repr=True)
port: str = field(default="", repr=True)
def __post_init__(self):
if (self.ip and not self.port) or (not self.ip and self.port):
raise ValueError("IP or port cannot be empty")
if (self.ip and self.port) and (not self.port.isdigit() or not (1 <= int(self.port) <= 65535)):
raise ValueError("Port must be a digit between 1 and 65535")
class ProxyDetector:
def __init__(self):
if sys.platform.startswith('win'):
import winreg
self.winreg = winreg
self.__path = r'Software\Microsoft\Windows\CurrentVersion\Internet Settings'
with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as key_user:
self.__INTERNET_SETTINGS = winreg.OpenKeyEx(key_user, self.__path, 0, winreg.KEY_ALL_ACCESS)
else:
self.__is_windows = False
def get_proxy_info(self) -> ProxyInfo:
if sys.platform.startswith('win'):
ip, port = self._get_proxy_info_windows()
else:
ip, port = self._get_proxy_info_linux()
return ProxyInfo(ip, port)
def is_proxy_enabled(self) -> bool:
if sys.platform.startswith('win'):
return self._is_proxy_enabled_windows()
else:
return self._is_proxy_enabled_linux()
def _get_proxy_info_windows(self) -> tuple[str, str]:
ip, port = "", ""
if self._is_proxy_enabled_windows():
try:
ip_port = self.winreg.QueryValueEx(self.__INTERNET_SETTINGS, "ProxyServer")[0]
if ip_port:
ip, port = ip_port.split(":")
except FileNotFoundError as err:
logger.warning("No proxy information found: " + str(err))
except Exception as err:
logger.error("An error occurred: " + str(err))
else:
logger.debug("No proxy is enabled on the system")
return ip, port
def _is_proxy_enabled_windows(self) -> bool:
try:
if self.winreg.QueryValueEx(self.__INTERNET_SETTINGS, "ProxyEnable")[0] == 1:
return True
except FileNotFoundError as err:
print("No proxy information found: " + str(err))
except Exception as err:
print("An error occurred: " + str(err))
return False
@staticmethod
def _get_proxy_info_linux() -> tuple[str, str]:
proxies = {
'http': os.getenv('http_proxy'),
'https': os.getenv('https_proxy'),
'ftp': os.getenv('ftp_proxy')
}
ip = port = ""
for proto, proxy in proxies.items():
if proxy:
ip, port = proxy.split(':')
break
return ip, port
def _is_proxy_enabled_linux(self) -> bool:
proxies = self._get_proxy_info_linux()
return any(proxy != '' for proxy in proxies)

158
room.py Normal file
View File

@@ -0,0 +1,158 @@
# -*- encoding: utf-8 -*-
"""
Author: Hmily
GitHub:https://github.com/ihmily
Date: 2023-07-17 23:52:05
Update: 2025-02-04 04:57:00
Copyright (c) 2023 by Hmily, All Rights Reserved.
"""
import re
import urllib.parse
import execjs
import httpx
import urllib.request
import utils
# from . import JS_SCRIPT_PATH, utils
import os
import sys
from pathlib import Path
from initializer import check_node
current_file_path = Path(__file__).resolve()
current_dir = current_file_path.parent
JS_SCRIPT_PATH = current_dir / 'javascript'
no_proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(no_proxy_handler)
class UnsupportedUrlError(Exception):
pass
HEADERS = {
'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',
'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',
'Cookie': 's_v_web_id=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf'
}
HEADERS_PC = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
'Cookie': 'sessionid=7494ae59ae06784454373ce25761e864; __ac_nonce=0670497840077ee4c9eb2; '
'__ac_signature=_02B4Z6wo00f012DZczQAAIDCJJBb3EjnINdg-XeAAL8-db; '
's_v_web_id=verify_m1ztgtjj_vuHnMLZD_iwZ9_4YO4_BdN1_7wLP3pyqXsf2; '
}
# X-bogus算法
async def get_xbogus(url: str, headers: dict | None = None) -> str:
if not headers or 'user-agent' not in (k.lower() for k in headers):
headers = HEADERS
query = urllib.parse.urlparse(url).query
xbogus = execjs.compile(open(f'{JS_SCRIPT_PATH}/x-bogus.js').read()).call(
'sign', query, headers.get("User-Agent", "user-agent"))
return xbogus
# 获取房间ID和用户secID
async def get_sec_user_id(url: str, proxy_addr: str | None = None, headers: dict | None = None) -> tuple | None:
if not headers or all(k.lower() not in ['user-agent', 'cookie'] for k in headers):
headers = HEADERS
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
async with httpx.AsyncClient(proxy=proxy_addr, timeout=15) as client:
response = await client.get(url, headers=headers, follow_redirects=True)
redirect_url = response.url
if 'reflow/' in str(redirect_url):
match = re.search(r'sec_user_id=([\w_\-]+)&', str(redirect_url))
if match:
sec_user_id = match.group(1)
room_id = str(redirect_url).split('?')[0].rsplit('/', maxsplit=1)[1]
return room_id, sec_user_id
else:
raise RuntimeError("Could not find sec_user_id in the URL.")
else:
raise UnsupportedUrlError("The redirect URL does not contain 'reflow/'.")
except UnsupportedUrlError as e:
raise e
except Exception as e:
raise RuntimeError(f"An error occurred: {e}")
# 获取抖音号
async def get_unique_id(url: str, proxy_addr: str | None = None, headers: dict | None = None) -> str | None:
if not headers or all(k.lower() not in ['user-agent', 'cookie'] for k in headers):
headers = HEADERS
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
async with httpx.AsyncClient(proxy=proxy_addr, timeout=15) as client:
response = await client.get(url, headers=headers, follow_redirects=True)
redirect_url = str(response.url)
if 'reflow/' in str(redirect_url):
raise UnsupportedUrlError("Unsupported URL")
sec_user_id = redirect_url.split('?')[0].rsplit('/', maxsplit=1)[1]
headers['Cookie'] = ('ttwid=1%7C4ejCkU2bKY76IySQENJwvGhg1IQZrgGEupSyTKKfuyk%7C1740470403%7Cbc9a'
'd2ee341f1a162f9e27f4641778030d1ae91e31f9df6553a8f2efa3bdb7b4; __ac_nonce=06'
'83e59f3009cc48fbab0; __ac_signature=_02B4Z6wo00f01mG6waQAAIDB9JUCzFb6.TZhmsU'
'AAPBf34; __ac_referer=__ac_blank')
user_page_response = await client.get(f'https://www.iesdouyin.com/share/user/{sec_user_id}',
headers=headers, follow_redirects=True)
matches = re.findall(r'unique_id":"(.*?)","verification_type', user_page_response.text)
if matches:
unique_id = matches[-1]
return unique_id
else:
raise RuntimeError("Could not find unique_id in the response.")
except UnsupportedUrlError as e:
raise e
except Exception as e:
raise RuntimeError(f"An error occurred: {e}")
# 获取直播间webID
async def get_live_room_id(room_id: str, sec_user_id: str, proxy_addr: str | None = None, params: dict | None = None,
headers: dict | None = None) -> str:
if not headers or all(k.lower() not in ['user-agent', 'cookie'] for k in headers):
headers = HEADERS
if not params:
params = {
"verifyFp": "verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf",
"type_id": "0",
"live_id": "1",
"room_id": room_id,
"sec_user_id": sec_user_id,
"app_id": "1128",
"msToken": "wrqzbEaTlsxt52-vxyZo_mIoL0RjNi1ZdDe7gzEGMUTVh_HvmbLLkQrA_1HKVOa2C6gkxb6IiY6TY2z8enAkPEwGq--gM"
"-me3Yudck2ailla5Q4osnYIHxd9dI4WtQ==",
}
api = f'https://webcast.amemv.com/webcast/room/reflow/info/?{urllib.parse.urlencode(params)}'
xbogus = await get_xbogus(api)
api = api + "&X-Bogus=" + xbogus
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
async with httpx.AsyncClient(proxy=proxy_addr,
timeout=15) as client:
response = await client.get(api, headers=headers)
response.raise_for_status()
json_data = response.json()
return json_data['data']['room']['owner']['web_rid']
except httpx.HTTPStatusError as e:
print(f"HTTP status error occurred: {e.response.status_code}")
raise
except Exception as e:
print(f"An exception occurred during get_live_room_id: {e}")
raise
if __name__ == '__main__':
room_url = "https://v.douyin.com/iQLgKSj/"
_room_id, sec_uid = get_sec_user_id(room_url)
web_rid = get_live_room_id(_room_id, sec_uid)
print("return web_rid:", web_rid)

3232
spider.py Normal file

File diff suppressed because it is too large Load Diff

446
stream.py Normal file
View File

@@ -0,0 +1,446 @@
# -*- encoding: utf-8 -*-
"""
Author: Hmily
GitHub: https://github.com/ihmily
Date: 2023-07-15 23:15:00
Update: 2025-02-06 02:28:00
Copyright (c) 2023-2025 by Hmily, All Rights Reserved.
Function: Get live stream data.
"""
import base64
import hashlib
import json
import time
import random
import re
from operator import itemgetter
import urllib.parse
import urllib.request
from utils import trace_error_decorator
from spider import (
get_douyu_stream_data, get_bilibili_stream_data
)
from http_clients.async_http import get_response_status
QUALITY_MAPPING = {"OD": 0, "BD": 0, "UHD": 1, "HD": 2, "SD": 3, "LD": 4}
def get_quality_index(quality) -> tuple:
if not quality:
return list(QUALITY_MAPPING.items())[0]
quality_str = str(quality).upper()
if quality_str.isdigit():
quality_int = int(quality_str[0])
quality_str = list(QUALITY_MAPPING.keys())[quality_int]
return quality_str, QUALITY_MAPPING.get(quality_str, 0)
@trace_error_decorator
async def get_douyin_stream_url(json_data: dict, video_quality: str, proxy_addr: str) -> dict:
anchor_name = json_data.get('anchor_name')
result = {
"anchor_name": anchor_name,
"is_live": False,
}
status = json_data.get("status", 4)
if status == 2:
stream_url = json_data['stream_url']
flv_url_dict = stream_url['flv_pull_url']
flv_url_list: list = list(flv_url_dict.values())
m3u8_url_dict = stream_url['hls_pull_url_map']
m3u8_url_list: list = list(m3u8_url_dict.values())
while len(flv_url_list) < 5:
flv_url_list.append(flv_url_list[-1])
m3u8_url_list.append(m3u8_url_list[-1])
video_quality, quality_index = get_quality_index(video_quality)
m3u8_url = m3u8_url_list[quality_index]
flv_url = flv_url_list[quality_index]
ok = await get_response_status(url=m3u8_url, proxy_addr=proxy_addr)
if not ok:
index = quality_index + 1 if quality_index < 4 else quality_index - 1
m3u8_url = m3u8_url_list[index]
flv_url = flv_url_list[index]
result |= {
'is_live': True,
'title': json_data['title'],
'quality': video_quality,
'm3u8_url': m3u8_url,
'flv_url': flv_url,
'record_url': m3u8_url or flv_url,
}
return result
@trace_error_decorator
async def get_tiktok_stream_url(json_data: dict, video_quality: str, proxy_addr: str) -> dict:
if not json_data:
return {"anchor_name": None, "is_live": False}
def get_video_quality_url(stream, q_key) -> list:
play_list = []
for key in stream:
url_info = stream[key]['main']
sdk_params = url_info['sdk_params']
sdk_params = json.loads(sdk_params)
vbitrate = int(sdk_params['vbitrate'])
v_codec = sdk_params.get('VCodec', '')
play_url = ''
if url_info.get(q_key):
if url_info[q_key].endswith(".flv") or url_info[q_key].endswith(".m3u8"):
play_url = url_info[q_key] + '?codec=' + v_codec
else:
play_url = url_info[q_key] + '&codec=' + v_codec
resolution = sdk_params['resolution']
if vbitrate != 0 and resolution:
width, height = map(int, resolution.split('x'))
play_list.append({'url': play_url, 'vbitrate': vbitrate, 'resolution': (width, height)})
play_list.sort(key=itemgetter('vbitrate'), reverse=True)
play_list.sort(key=lambda x: (-x['vbitrate'], -x['resolution'][0], -x['resolution'][1]))
return play_list
live_room = json_data['LiveRoom']['liveRoomUserInfo']
user = live_room['user']
anchor_name = f"{user['nickname']}-{user['uniqueId']}"
status = user.get("status", 4)
result = {
"anchor_name": anchor_name,
"is_live": False,
}
if status == 2:
stream_data = live_room['liveRoom']['streamData']['pull_data']['stream_data']
stream_data = json.loads(stream_data).get('data', {})
flv_url_list = get_video_quality_url(stream_data, 'flv')
m3u8_url_list = get_video_quality_url(stream_data, 'hls')
while len(flv_url_list) < 5:
flv_url_list.append(flv_url_list[-1])
while len(m3u8_url_list) < 5:
m3u8_url_list.append(m3u8_url_list[-1])
video_quality, quality_index = get_quality_index(video_quality)
flv_dict: dict = flv_url_list[quality_index]
m3u8_dict: dict = m3u8_url_list[quality_index]
check_url = m3u8_dict.get('url') or flv_dict.get('url')
ok = await get_response_status(url=check_url, proxy_addr=proxy_addr, http2=False)
if not ok:
index = quality_index + 1 if quality_index < 4 else quality_index - 1
flv_dict: dict = flv_url_list[index]
m3u8_dict: dict = m3u8_url_list[index]
flv_url = flv_dict['url']
m3u8_url = m3u8_dict['url']
result |= {
'is_live': True,
'title': live_room['liveRoom']['title'],
'quality': video_quality,
'm3u8_url': m3u8_url,
'flv_url': flv_url,
'record_url': m3u8_url or flv_url,
}
return result
@trace_error_decorator
async def get_kuaishou_stream_url(json_data: dict, video_quality: str) -> dict:
if json_data['type'] == 1 and not json_data["is_live"]:
return json_data
live_status = json_data['is_live']
result = {
"type": 2,
"anchor_name": json_data['anchor_name'],
"is_live": live_status,
}
if live_status:
quality_mapping_bit = {'OD': 99999, 'BD': 4000, 'UHD': 2000, 'HD': 1000, 'SD': 800, 'LD': 600}
if video_quality in QUALITY_MAPPING:
quality, quality_index = get_quality_index(video_quality)
if 'm3u8_url_list' in json_data:
m3u8_url_list = json_data['m3u8_url_list'][::-1]
while len(m3u8_url_list) < 5:
m3u8_url_list.append(m3u8_url_list[-1])
m3u8_url = m3u8_url_list[quality_index]['url']
result['m3u8_url'] = m3u8_url
if 'flv_url_list' in json_data:
if 'bitrate' in json_data['flv_url_list'][0]:
flv_url_list = json_data['flv_url_list']
flv_url_list = sorted(flv_url_list, key=lambda x: x['bitrate'], reverse=True)
quality_str = str(video_quality).upper()
if quality_str.isdigit():
video_quality, quality_index_bitrate_value = list(quality_mapping_bit.items())[int(quality_str)]
else:
quality_index_bitrate_value = quality_mapping_bit.get(quality_str, 99999)
video_quality = quality_str
quality_index = next(
(i for i, x in enumerate(flv_url_list) if x['bitrate'] <= quality_index_bitrate_value), None)
if quality_index is None:
quality_index = len(flv_url_list) - 1
flv_url = flv_url_list[quality_index]['url']
result['flv_url'] = flv_url
result['record_url'] = flv_url
else:
flv_url_list = json_data['flv_url_list'][::-1]
while len(flv_url_list) < 5:
flv_url_list.append(flv_url_list[-1])
flv_url = flv_url_list[quality_index]['url']
result |= {'flv_url': flv_url, 'record_url': flv_url}
result['is_live'] = True
result['quality'] = video_quality
return result
@trace_error_decorator
async def get_huya_stream_url(json_data: dict, video_quality: str) -> dict:
game_live_info = json_data['data'][0]['gameLiveInfo']
live_title = game_live_info['introduction']
stream_info_list = json_data['data'][0]['gameStreamInfoList']
anchor_name = game_live_info.get('nick', '')
result = {
"anchor_name": anchor_name,
"is_live": False,
}
if stream_info_list:
select_cdn = stream_info_list[0]
flv_url = select_cdn.get('sFlvUrl')
stream_name = select_cdn.get('sStreamName')
flv_url_suffix = select_cdn.get('sFlvUrlSuffix')
hls_url = select_cdn.get('sHlsUrl')
hls_url_suffix = select_cdn.get('sHlsUrlSuffix')
flv_anti_code = select_cdn.get('sFlvAntiCode')
def get_anti_code(old_anti_code: str) -> str:
# js地址https://hd.huya.com/cdn_libs/mobile/hysdk-m-202402211431.js
params_t = 100
sdk_version = 2403051612
# sdk_id是13位数毫秒级时间戳
t13 = int(time.time()) * 1000
sdk_sid = t13
# 计算uuid和uid参数值
init_uuid = (int(t13 % 10 ** 10 * 1000) + int(1000 * random.random())) % 4294967295 # 直接初始化
uid = random.randint(1400000000000, 1400009999999) # 经过测试uid也可以使用init_uuid代替
seq_id = uid + sdk_sid # 移动端请求的直播流地址中包含seqId参数
# 计算ws_time参数值(16进制) 可以是当前毫秒时间戳当然也可以直接使用url_query['wsTime'][0]
# 原始最大误差不得慢240000毫秒
target_unix_time = (t13 + 110624) // 1000
ws_time = f"{target_unix_time:x}".lower()
# fm参数值是经过url编码然后base64编码得到的解码结果类似 DWq8BcJ3h6DJt6TY_$0_$1_$2_$3
# 具体细节在上面js中查看大概在32657行代码开始有base64混淆代码请自行替换
url_query = urllib.parse.parse_qs(old_anti_code)
ws_secret_pf = base64.b64decode(urllib.parse.unquote(url_query['fm'][0]).encode()).decode().split("_")[0]
ws_secret_hash = hashlib.md5(f'{seq_id}|{url_query["ctype"][0]}|{params_t}'.encode()).hexdigest()
ws_secret = f'{ws_secret_pf}_{uid}_{stream_name}_{ws_secret_hash}_{ws_time}'
ws_secret_md5 = hashlib.md5(ws_secret.encode()).hexdigest()
anti_code = (
f'wsSecret={ws_secret_md5}&wsTime={ws_time}&seqid={seq_id}&ctype={url_query["ctype"][0]}&ver=1'
f'&fs={url_query["fs"][0]}&uuid={init_uuid}&u={uid}&t={params_t}&sv={sdk_version}'
f'&sdk_sid={sdk_sid}&codec=264'
)
return anti_code
new_anti_code = get_anti_code(flv_anti_code)
flv_url = f'{flv_url}/{stream_name}.{flv_url_suffix}?{new_anti_code}&ratio='
m3u8_url = f'{hls_url}/{stream_name}.{hls_url_suffix}?{new_anti_code}&ratio='
quality_list = flv_anti_code.split('&exsphd=')
if len(quality_list) > 1 and video_quality not in ["OD", "BD"]:
pattern = r"(?<=264_)\d+"
quality_list = list(re.findall(pattern, quality_list[1]))[::-1]
while len(quality_list) < 5:
quality_list.append(quality_list[-1])
video_quality_options = {
"UHD": quality_list[0],
"HD": quality_list[1],
"SD": quality_list[2],
"LD": quality_list[3]
}
if video_quality not in video_quality_options:
raise ValueError(
f"Invalid video quality. Available options are: {', '.join(video_quality_options.keys())}")
flv_url = flv_url + str(video_quality_options[video_quality])
m3u8_url = m3u8_url + str(video_quality_options[video_quality])
result |= {
'is_live': True,
'title': live_title,
'quality': video_quality,
'm3u8_url': m3u8_url,
'flv_url': flv_url,
'record_url': flv_url or m3u8_url
}
return result
@trace_error_decorator
async def get_douyu_stream_url(json_data: dict, video_quality: str, cookies: str, proxy_addr: str) -> dict:
if not json_data["is_live"]:
return json_data
video_quality_options = {
"OD": '0',
"BD": '0',
"UHD": '3',
"HD": '2',
"SD": '1',
"LD": '1'
}
rid = str(json_data["room_id"])
json_data.pop("room_id")
rate = video_quality_options.get(video_quality, '0')
flv_data = await get_douyu_stream_data(rid, rate, cookies=cookies, proxy_addr=proxy_addr)
rtmp_url = flv_data['data'].get('rtmp_url')
rtmp_live = flv_data['data'].get('rtmp_live')
if rtmp_live:
flv_url = f'{rtmp_url}/{rtmp_live}'
json_data |= {'quality': video_quality, 'flv_url': flv_url, 'record_url': flv_url}
return json_data
@trace_error_decorator
async def get_yy_stream_url(json_data: dict) -> dict:
anchor_name = json_data.get('anchor_name', '')
result = {
"anchor_name": anchor_name,
"is_live": False,
}
if 'avp_info_res' in json_data:
stream_line_addr = json_data['avp_info_res']['stream_line_addr']
cdn_info = list(stream_line_addr.values())[0]
flv_url = cdn_info['cdn_info']['url']
result |= {
'is_live': True,
'title': json_data['title'],
'quality': 'OD',
'flv_url': flv_url,
'record_url': flv_url
}
return result
@trace_error_decorator
async def get_bilibili_stream_url(json_data: dict, video_quality: str, proxy_addr: str, cookies: str) -> dict:
anchor_name = json_data["anchor_name"]
if not json_data["live_status"]:
return {
"anchor_name": anchor_name,
"is_live": False
}
room_url = json_data['room_url']
video_quality_options = {
"OD": '10000',
"BD": '400',
"UHD": '250',
"HD": '150',
"SD": '80',
"LD": '80'
}
select_quality = video_quality_options[video_quality]
play_url = await get_bilibili_stream_data(
room_url, qn=select_quality, platform='web', proxy_addr=proxy_addr, cookies=cookies)
return {
'anchor_name': json_data['anchor_name'],
'is_live': True,
'title': json_data['title'],
'quality': video_quality,
'record_url': play_url
}
@trace_error_decorator
async def get_netease_stream_url(json_data: dict, video_quality: str) -> dict:
if not json_data['is_live']:
return json_data
m3u8_url = json_data['m3u8_url']
flv_url = None
if json_data.get('stream_list'):
stream_list = json_data['stream_list']['resolution']
order = ['blueray', 'ultra', 'high', 'standard']
sorted_keys = [key for key in order if key in stream_list]
while len(sorted_keys) < 5:
sorted_keys.append(sorted_keys[-1])
video_quality, quality_index = get_quality_index(video_quality)
selected_quality = sorted_keys[quality_index]
flv_url_list = stream_list[selected_quality]['cdn']
selected_cdn = list(flv_url_list.keys())[0]
flv_url = flv_url_list[selected_cdn]
return {
"is_live": True,
"anchor_name": json_data['anchor_name'],
"title": json_data['title'],
'quality': video_quality,
"m3u8_url": m3u8_url,
"flv_url": flv_url,
"record_url": flv_url or m3u8_url
}
async def get_stream_url(json_data: dict, video_quality: str, url_type: str = 'm3u8', spec: bool = False,
hls_extra_key: str | int = None, flv_extra_key: str | int = None) -> dict:
if not json_data['is_live']:
return json_data
play_url_list = json_data['play_url_list']
while len(play_url_list) < 5:
play_url_list.append(play_url_list[-1])
video_quality, selected_quality = get_quality_index(video_quality)
data = {
"anchor_name": json_data['anchor_name'],
"is_live": True
}
def get_url(key):
play_url = play_url_list[selected_quality]
return play_url[key] if key else play_url
if url_type == 'all':
m3u8_url = get_url(hls_extra_key)
flv_url = get_url(flv_extra_key)
data |= {
"m3u8_url": json_data['m3u8_url'] if spec else m3u8_url,
"flv_url": json_data['flv_url'] if spec else flv_url,
"record_url": m3u8_url
}
elif url_type == 'm3u8':
m3u8_url = get_url(hls_extra_key)
data |= {"m3u8_url": json_data['m3u8_url'] if spec else m3u8_url, "record_url": m3u8_url}
else:
flv_url = get_url(flv_extra_key)
data |= {"flv_url": flv_url, "record_url": flv_url}
data['title'] = json_data.get('title')
data['quality'] = video_quality
return data

193
utils.py Normal file
View File

@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
import json
import os
import random
import shutil
import string
from pathlib import Path
import functools
import hashlib
import re
import traceback
from typing import Any
from collections import OrderedDict
import execjs
import logger
import configparser
OptionalStr = str | None
OptionalDict = dict | None
class Color:
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
RESET = "\033[0m"
@staticmethod
def print_colored(text, color):
print(f"{color}{text}{Color.RESET}")
def trace_error_decorator(func: callable) -> callable:
@functools.wraps(func)
def wrapper(*args: list, **kwargs: dict) -> Any:
try:
return func(*args, **kwargs)
except execjs.ProgramError:
logger.warning('Failed to execute JS code. Please check if the Node.js environment')
except Exception as e:
error_line = traceback.extract_tb(e.__traceback__)[-1].lineno
error_info = f"message: type: {type(e).__name__}, {str(e)} in function {func.__name__} at line: {error_line}"
logger.error(error_info)
return []
return wrapper
def check_md5(file_path: str | Path) -> str:
with open(file_path, 'rb') as fp:
file_md5 = hashlib.md5(fp.read()).hexdigest()
return file_md5
def dict_to_cookie_str(cookies_dict: dict) -> str:
cookie_str = '; '.join([f"{key}={value}" for key, value in cookies_dict.items()])
return cookie_str
def read_config_value(file_path: str | Path, section: str, key: str) -> str | None:
config = configparser.ConfigParser()
try:
config.read(file_path, encoding='utf-8-sig')
except Exception as e:
print(f"Error occurred while reading the configuration file: {e}")
return None
if section in config:
if key in config[section]:
return config[section][key]
else:
print(f"Key [{key}] does not exist in section [{section}].")
else:
print(f"Section [{section}] does not exist in the file.")
return None
def update_config(file_path: str | Path, section: str, key: str, new_value: str) -> None:
config = configparser.ConfigParser()
try:
config.read(file_path, encoding='utf-8-sig')
except Exception as e:
print(f"An error occurred while reading the configuration file: {e}")
return
if section not in config:
print(f"Section [{section}] does not exist in the file.")
return
# 转义%字符
escaped_value = new_value.replace('%', '%%')
config[section][key] = escaped_value
try:
with open(file_path, 'w', encoding='utf-8-sig') as configfile:
config.write(configfile)
print(f"The value of {key} under [{section}] in the configuration file has been updated.")
except Exception as e:
print(f"Error occurred while writing to the configuration file: {e}")
def get_file_paths(directory: str) -> list:
file_paths = []
for root, dirs, files in os.walk(directory):
for file in files:
file_paths.append(os.path.join(root, file))
return file_paths
def remove_emojis(text: str, replace_text: str = '') -> str:
emoji_pattern = re.compile(
"["
"\U0001F1E0-\U0001F1FF" # flags (iOS)
"\U0001F300-\U0001F5FF" # symbols & pictographs
"\U0001F600-\U0001F64F" # emoticons
"\U0001F680-\U0001F6FF" # transport & map symbols
"\U0001F700-\U0001F77F" # alchemical symbols
"\U0001F780-\U0001F7FF" # Geometric Shapes Extended
"\U0001F800-\U0001F8FF" # Supplemental Arrows-C
"\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
"\U0001FA00-\U0001FA6F" # Chess Symbols
"\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A
"\U00002702-\U000027B0" # Dingbats
"]+",
flags=re.UNICODE
)
return emoji_pattern.sub(replace_text, text)
def remove_duplicate_lines(file_path: str | Path) -> None:
unique_lines = OrderedDict()
text_encoding = 'utf-8-sig'
with open(file_path, 'r', encoding=text_encoding) as input_file:
for line in input_file:
unique_lines[line.strip()] = None
with open(file_path, 'w', encoding=text_encoding) as output_file:
for line in unique_lines:
output_file.write(line + '\n')
def check_disk_capacity(file_path: str | Path, show: bool = False) -> float:
absolute_path = os.path.abspath(file_path)
directory = os.path.dirname(absolute_path)
disk_usage = shutil.disk_usage(directory)
disk_root = Path(directory).anchor
free_space_gb = disk_usage.free / (1024 ** 3)
if show:
print(f"{disk_root} Total: {disk_usage.total / (1024 ** 3):.2f} GB "
f"Used: {disk_usage.used / (1024 ** 3):.2f} GB "
f"Free: {free_space_gb:.2f} GB\n")
return free_space_gb
def handle_proxy_addr(proxy_addr):
if proxy_addr:
if not proxy_addr.startswith('http'):
proxy_addr = 'http://' + proxy_addr
else:
proxy_addr = None
return proxy_addr
def generate_random_string(length: int) -> str:
characters = string.ascii_uppercase + string.digits
random_string = ''.join(random.choices(characters, k=length))
return random_string
def jsonp_to_json(jsonp_str: str) -> OptionalDict:
pattern = r'(\w+)\((.*)\);?$'
match = re.search(pattern, jsonp_str)
if match:
_, json_str = match.groups()
json_obj = json.loads(json_str)
return json_obj
else:
raise Exception("No JSON data found in JSONP response.")
def replace_url(file_path: str | Path, old: str, new: str) -> None:
with open(file_path, 'r', encoding='utf-8-sig') as f:
content = f.read()
if old in content:
with open(file_path, 'w', encoding='utf-8-sig') as f:
f.write(content.replace(old, new))