diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60cdb0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +cache/ +__pycache__/ +*.pyc +records/ +*.ts \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d7060c9 --- /dev/null +++ b/__init__.py @@ -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() diff --git a/backup/back b/backup/back new file mode 100644 index 0000000..8342828 --- /dev/null +++ b/backup/back @@ -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/743565594721" + 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) diff --git a/backup/back10 b/backup/back10 new file mode 100644 index 0000000..78273d2 --- /dev/null +++ b/backup/back10 @@ -0,0 +1,250 @@ +import os +import subprocess +import threading +import asyncio +import uuid +import time +import signal +import sys +import logging +import platform +import spider +import stream +import aiohttp + +# ----------------- Logging Setup ----------------- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S" +) + +IS_WINDOWS = platform.system() == "Windows" + +# ----------------- Douyin Recorder (推流已删除,增加9:16裁16:9) ----------------- +class DouyinRecorder: + def __init__(self, audio_bitrate=128, max_minutes=8): + self.record_proc = None + self.stop_flag = threading.Event() + self.audio_bitrate = audio_bitrate + self.start_time = None + self.room_name = None + self.out_file = None + self.max_minutes = max_minutes + + os.makedirs("records", exist_ok=True) + + + def _build_record_cmd(self, real_url): + base_name = f"{self.room_name}_{int(time.time())}.mp4" + self.out_file = os.path.join("records", base_name).replace("\\", "/") + + # 处理竖屏视频裁切成16:9,并两边使用模糊背景填充 + # 输入假设是720x1280竖屏,输出16:9 1280x720 + # 原理:1) 原视频缩放高度720,保持宽度自适应 + # 2) 原视频模糊填充16:9背景 + vf_filter = ( + "split [a][b];" + "[a] scale=-1:720 [main];" + "[b] scale=1280:720, boxblur=luma_radius=20:luma_power=1:chroma_radius=20:chroma_power=1 [bg];" + "[bg][main] overlay=(W-w)/2:(H-h)/2" + ) + + cmd = [ + 'ffmpeg', '-y', + '-i', real_url, + '-c:v', 'libx264', + '-preset', 'veryfast', + '-tune', 'zerolatency', + '-vf', vf_filter, + '-c:a', 'aac', + '-b:a', f'{self.audio_bitrate}k', + '-ar', '44100', + '-ac', '2', + '-movflags', '+faststart+frag_keyframe', + self.out_file + ] + return cmd + + + # ----------------- 录制循环 ----------------- + def start_record(self, real_url, room_name=None): + self.room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}" + logging.info(f"[{self.room_name}] 启动本地录制") + threading.Thread(target=self._run_record_loop, args=(real_url,), daemon=True).start() + threading.Thread(target=self._monitor_thread, daemon=True).start() + + def _run_record_loop(self, real_url): + while not self.stop_flag.is_set(): + try: + self.start_time = time.time() + + # ---- 启动本地录制 ---- + record_cmd = self._build_record_cmd(real_url) + self.record_proc = subprocess.Popen( + record_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + logging.info(f"[{self.room_name}] 本地录制开始: {self.out_file}") + + # ---- 监控 stderr ---- + def monitor_proc(proc): + try: + for line in iter(proc.stderr.readline, b''): + if self.stop_flag.is_set(): + break + if line: + decoded = line.decode('utf-8', errors='ignore').strip() + logging.debug(f"[{self.room_name} record]: {decoded}") + except Exception as e: + logging.error(f"[{self.room_name} record stderr异常: {e}") + + threading.Thread(target=monitor_proc, args=(self.record_proc,), daemon=True).start() + + # ---- 循环检查 ---- + while not self.stop_flag.is_set(): + elapsed = time.time() - self.start_time + if elapsed >= self.max_minutes * 60: + logging.warning(f"[{self.room_name}] 超过 {self.max_minutes} 分钟,自动停止录制") + self.stop() + break + if self.record_proc.poll() is not None: + logging.warning(f"[{self.room_name}] FFmpeg 录制进程退出,1秒后重启...") + break + time.sleep(2) + + except Exception as e: + logging.error(f"[{self.room_name}] FFmpeg录制异常: {e}") + finally: + self._terminate_proc() + self._finalize_recording() + if not self.stop_flag.is_set(): + time.sleep(1) + + def _terminate_proc(self): + if self.record_proc and self.record_proc.poll() is None: + self.record_proc.terminate() + try: + self.record_proc.wait(timeout=2) + except subprocess.TimeoutExpired: + self.record_proc.kill() + self.record_proc = None + + # ----------------- 监控线程 ----------------- + def _monitor_thread(self): + while not self.stop_flag.is_set(): + uptime = int(time.time() - self.start_time) if self.start_time else 0 + m, s = divmod(uptime, 60) + status = "录制中" if self.record_proc and self.record_proc.poll() is None else "启动中" + print(f"\r[{self.room_name}] {status} | 运行时间: {m:02d}:{s:02d} | 音频码率: {self.audio_bitrate}k", end="") + time.sleep(2) + + # ----------------- 停止 ----------------- + def stop(self): + logging.info(f"[{self.room_name}] 停止录制...") + self.stop_flag.set() + self._terminate_proc() + self._finalize_recording() + + def _finalize_recording(self): + if self.out_file and os.path.exists(self.out_file): + try: + if self.record_proc and self.record_proc.poll() is None: + sig = signal.CTRL_BREAK_EVENT if IS_WINDOWS else signal.SIGINT + self.record_proc.send_signal(sig) + try: + self.record_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self.record_proc.kill() + except Exception: + pass + + elapsed = int(time.time() - self.start_time) if self.start_time else 0 + suffix = f"_{elapsed // 60}min" if elapsed >= 60 else f"_{elapsed}s" + new_name = self.out_file.replace(".mp4", f"{suffix}.mp4") + try: + os.rename(self.out_file, new_name) + except FileExistsError: + new_name = self.out_file.replace(".mp4", f"_{uuid.uuid4().hex[:4]}.mp4") + os.rename(self.out_file, new_name) + size_mb = os.path.getsize(new_name) / (1024 * 1024) + logging.info(f"[{self.room_name}] 本地录制已结束: {new_name} | 大小: {size_mb:.2f} MB") + + +# ----------------- Douyin Stream Parsing ----------------- +async def get_douyin_real_url(record_url, record_quality="原画", proxy_addr=None, cookies=''): + headers = { + "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"), + "Referer": "https://www.douyin.com/", + "Origin": "https://www.douyin.com", + "Accept": "application/json, text/javascript, */*; q=0.01", + } + + session_args = {"headers": headers} + if proxy_addr: + session_args["proxy"] = proxy_addr + + try: + async with aiohttp.ClientSession(**session_args) as session: + for attempt in range(3): + try: + async with session.get(record_url, cookies=cookies) as resp: + text = await resp.text() + if not text or "captcha" in text.lower(): + logging.warning(f"第{attempt+1}次尝试获取流失败,返回内容为空或验证码,重试...") + await asyncio.sleep(1) + continue + 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, proxy_addr=proxy_addr, cookies=cookies) + else: + json_data = await spider.get_douyin_app_stream_data( + url=record_url, proxy_addr=proxy_addr, cookies=cookies) + + port_info = await stream.get_douyin_stream_url( + json_data, record_quality, proxy_addr) + if port_info and port_info.get("record_url"): + return port_info.get("record_url") + else: + logging.warning(f"第{attempt+1}次尝试解析失败,port_info为空") + await asyncio.sleep(1) + except Exception as e: + logging.warning(f"第{attempt+1}次尝试抓取抖音流异常: {e}") + await asyncio.sleep(1) + logging.error("多次尝试后仍获取抖音真实流失败") + return None + except Exception as e: + logging.error(f"初始化 aiohttp session 失败: {e}") + return None + +def start_douyin_record(record_url, room_name=None, proxy_addr=None, cookies='', audio_bitrate=128): + real_url = asyncio.run(get_douyin_real_url(record_url, proxy_addr=proxy_addr, cookies=cookies)) + if not real_url: + logging.error("获取真实直播流失败") + return None + logging.info(f"实际直播流: {real_url}") + recorder = DouyinRecorder(audio_bitrate=audio_bitrate) + recorder.start_record(real_url, room_name) + return recorder + +# ----------------- Main Program ----------------- +if __name__ == "__main__": + DOUYIN_URL = "https://live.douyin.com/530395023516" + + recorder_instance = start_douyin_record(DOUYIN_URL) + if not recorder_instance: + logging.error("录制实例创建失败,程序退出") + sys.exit(1) + + def signal_handler(sig, frame): + if recorder_instance: + recorder_instance.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + logging.info("程序已启动,本地录制中,按 Ctrl+C 停止...") + while True: + time.sleep(60) diff --git a/backup/back2 b/backup/back2 new file mode 100644 index 0000000..fce5c1e --- /dev/null +++ b/backup/back2 @@ -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) diff --git a/backup/back3 b/backup/back3 new file mode 100644 index 0000000..0769adf --- /dev/null +++ b/backup/back3 @@ -0,0 +1,202 @@ +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=5000, min_bitrate=3000, audio_bitrate=192, + delay_sec=40, 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*500) # 读取更大块,减少空跑 + 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", + "-s", "720x1280", + "-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(): + try: + data = self.buffer_queue.get(timeout=1) + proc.stdin.write(data) + proc.stdin.flush() + except queue.Empty: + continue + 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=40): + 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/364595817295" + YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" + + hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=40) + 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) diff --git a/backup/back4 b/backup/back4 new file mode 100644 index 0000000..c75402c --- /dev/null +++ b/backup/back4 @@ -0,0 +1,208 @@ +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=5000, min_bitrate=3000, audio_bitrate=192, + delay_sec=40, 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(maxsize=500) # 增大队列,防止阻塞 + 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*2000) # 大块读取,减少空跑 + if not data: + time.sleep(0.05) + continue + try: + self.buffer_queue.put_nowait(data) # 非阻塞写入队列 + except queue.Full: + continue # 队列满直接丢掉,保证不卡 + 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", + "-s", "720x1280", + "-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(): + try: + data = self.buffer_queue.get(timeout=0.5) + try: + proc.stdin.write(data) + proc.stdin.flush() + except (BrokenPipeError, OSError): + break # 管道断开,退出推流循环 + except queue.Empty: + continue + 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(max(self.check_interval, 10)) # 降低 ABR 更新频率,避免频繁干扰 + + 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=40): + 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/125742860435" + YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" + + hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=40) + 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) diff --git a/backup/back5 b/backup/back5 new file mode 100644 index 0000000..ef4db7c --- /dev/null +++ b/backup/back5 @@ -0,0 +1,220 @@ +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=5000, min_bitrate=3000, audio_bitrate=192, + delay_sec=40, 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(maxsize=800) # 队列加大 + 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.PIPE) + print("[拉流] FFmpeg 启动成功") + + def read_stderr(): + while proc.poll() is None: + line = proc.stderr.readline() + if line: + print(f"[拉流FFmpeg]: {line.decode(errors='ignore').strip()}") + threading.Thread(target=read_stderr, daemon=True).start() + + while not self.stop_flag.is_set(): + data = proc.stdout.read(188 * 5000) # 提升吞吐 + if not data: + time.sleep(0.05) + continue + try: + self.buffer_queue.put_nowait(data) + except queue.Full: + continue + 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", + "-x264-params", "keyint=60:scenecut=0", + "-b:v", f"{self.current_bitrate}k", + "-maxrate", f"{self.current_bitrate}k", + "-bufsize", f"{self.current_bitrate*4}k", + "-r", "30", + "-g", "60", + "-s", "720x1280", + "-c:a", "aac", + "-b:a", f"{self.audio_bitrate}k", + "-ar", "44100", + "-ac", "2", + "-strict", "-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(): + try: + data = self.buffer_queue.get(timeout=0.5) + try: + proc.stdin.write(data) + proc.stdin.flush() + except (BrokenPipeError, OSError): + print(f"[{room_name}] 推流管道断开,准备重启") + break + except queue.Empty: + continue + 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 > 200: + new_bitrate = max(self.min_bitrate, self.current_bitrate - 500) + elif queue_len < 20: + 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(max(self.check_interval, 10)) + + 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=40): + 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 = sum(1 for proc in hls_instance.processes.values() if proc and proc.poll() is None) + + 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/592179817312" + YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" + + hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=40) + 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) diff --git a/backup/back6 b/backup/back6 new file mode 100644 index 0000000..6e24def --- /dev/null +++ b/backup/back6 @@ -0,0 +1,209 @@ +import os +import subprocess +import threading +import asyncio +import uuid +import time +import signal +import sys +import shutil +import logging +import spider +import stream + +# ----------------- Logging Setup ----------------- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S" +) + +# ----------------- Low-Latency Douyin to YouTube Streamer ----------------- +class DouyinToYouTubeStreamer: + def __init__(self, bitrate=5000, audio_bitrate=128, resolution='720x1280', fps=30, gop=60): + self.proc = None + self.stop_flag = threading.Event() + self.bitrate = bitrate + self.audio_bitrate = audio_bitrate + self.resolution = resolution + self.fps = fps + self.gop = gop + self.start_time = None + self.room_name = None + self.semaphore = threading.Semaphore(1) + self.pushing = False # 是否正在推流 + + def start_stream(self, real_url, youtube_key, room_name=None): + self.room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}" + logging.info(f"[{self.room_name}] 启动低延迟推流,目标 YouTube Key: {youtube_key}") + threading.Thread(target=self._run_ffmpeg_loop, args=(real_url, youtube_key), daemon=True).start() + threading.Thread(target=self._monitor_thread, daemon=True).start() + + def _build_ffmpeg_cmd(self, real_url, youtube_key): + 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") + headers = "Referer: https://www.douyin.com/\r\nOrigin: https://www.douyin.com\r\n" + + cmd = [ + 'ffmpeg', '-y', '-v', 'error', '-hide_banner', + '-rw_timeout', '10000000', + '-analyzeduration', '1000000', + '-probesize', '5000000', + '-thread_queue_size', '512', + '-user_agent', user_agent, + '-headers', headers, + '-protocol_whitelist', 'file,pipe,http,https,tcp,tls,udp,rtp,crypto', + '-fflags', '+discardcorrupt+genpts', + '-flags', 'low_delay', + '-http_seekable', '0', + '-avioflags', 'direct', + '-reconnect', '1', + '-reconnect_at_eof', '1', + '-reconnect_streamed', '1', + '-reconnect_delay_max', '5', + '-flush_packets', '1', + '-i', real_url, + '-c:v', 'libx264', + '-preset', 'veryfast', + '-tune', 'zerolatency', + '-crf', '23', + '-pix_fmt', 'yuv420p', + '-profile:v', 'main', + '-level', '3.1', + '-x264-params', f'keyint={self.gop}:scenecut=0:min-keyint={self.gop//2}', + '-g', str(self.gop), + '-r', str(self.fps), + '-s', self.resolution, + '-b:v', f'{self.bitrate}k', + '-maxrate', f'{self.bitrate}k', + '-bufsize', f'{self.bitrate * 2}k', + '-c:a', 'aac', + '-b:a', f'{self.audio_bitrate}k', + '-ar', '44100', + '-ac', '2', + '-strict', '-2', + '-avoid_negative_ts', 'make_zero', + '-f', 'flv', + f'rtmp://a.rtmp.youtube.com/live2/{youtube_key}' + ] + return cmd + + def _run_ffmpeg_loop(self, real_url, youtube_key): + while not self.stop_flag.is_set(): + try: + with self.semaphore: + cmd = self._build_ffmpeg_cmd(real_url, youtube_key) + self.proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, + bufsize=0, universal_newlines=False + ) + self.start_time = time.time() + self.pushing = False + logging.info(f"[{self.room_name}] FFmpeg 启动成功 (PID: {self.proc.pid})") + + def read_stderr(): + try: + for line in iter(self.proc.stderr.readline, b''): + if self.stop_flag.is_set(): + break + if line: + decoded = line.decode('utf-8', errors='ignore').strip() + logging.info(f"[{self.room_name} FFmpeg]: {decoded}") + # 判断推流是否成功 + if not self.pushing and "frame=" in decoded and "fps=" in decoded: + self.pushing = True + logging.info(f"[{self.room_name}] 推流到 YouTube 成功") + except Exception as e: + logging.error(f"[{self.room_name}] stderr线程异常: {e}") + + stderr_thread = threading.Thread(target=read_stderr, daemon=True) + stderr_thread.start() + + self.proc.wait() + if not self.stop_flag.is_set(): + logging.warning(f"[{self.room_name}] FFmpeg 退出 (返回码: {self.proc.returncode}),1秒后重启...") + self.pushing = False + except Exception as e: + logging.error(f"[{self.room_name}] FFmpeg 启动异常: {e}") + finally: + if self.proc and self.proc.poll() is None: + self.proc.terminate() + try: + self.proc.wait(timeout=1) + except subprocess.TimeoutExpired: + self.proc.kill() + self.proc = None + if not self.stop_flag.is_set(): + time.sleep(1) + + def _monitor_thread(self): + term_width, _ = shutil.get_terminal_size() + bar_width = min(50, term_width - 30) + while not self.stop_flag.is_set(): + if self.proc: + uptime = time.time() - self.start_time if self.start_time else 0 + uptime_str = f"{int(uptime // 60):02d}:{int(uptime % 60):02d}" + status = "推流中" if self.pushing else "启动中" + bar_len = bar_width if self.pushing else 0 + bar = "#" * bar_len + "-" * (bar_width - bar_len) + print(f"\r[{self.room_name}] [{bar}] {status} | 运行时间: {uptime_str} | 码率: {self.bitrate}k", end="") + else: + print(f"\r[{self.room_name}] FFmpeg 未运行", end="") + time.sleep(2) + + def stop(self): + logging.info(f"[{self.room_name}] 停止推流...") + self.stop_flag.set() + if self.proc and self.proc.poll() is None: + self.proc.terminate() + try: + self.proc.wait(timeout=1) + except subprocess.TimeoutExpired: + self.proc.kill() + + +# ----------------- Douyin Stream Parsing ----------------- +async def get_douyin_real_url(record_url, record_quality="原画", proxy_addr=None, cookies=''): + try: + 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, proxy_addr=proxy_addr, cookies=cookies) + else: + json_data = await spider.get_douyin_app_stream_data(url=record_url, proxy_addr=proxy_addr, cookies=cookies) + port_info = await stream.get_douyin_stream_url(json_data, record_quality, proxy_addr) + return port_info.get("record_url") + except Exception as e: + logging.error(f"获取抖音真实流失败: {e}") + return None + +def start_douyin_to_youtube(record_url, youtube_key, room_name=None, proxy_addr=None, cookies='', bitrate=5000): + real_url = asyncio.run(get_douyin_real_url(record_url, proxy_addr, cookies)) + if not real_url: + logging.error("获取真实直播流失败") + return None + logging.info(f"实际直播流: {real_url}") + streamer = DouyinToYouTubeStreamer(bitrate=bitrate) + streamer.start_stream(real_url, youtube_key, room_name) + return streamer + + +# ----------------- Main Program ----------------- +if __name__ == "__main__": + DOUYIN_URL = "https://live.douyin.com/112633436109" + YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" + + hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY) + if not hls_instance: + logging.error("推流实例创建失败,程序退出") + sys.exit(1) + + def signal_handler(sig, frame): + if hls_instance: + hls_instance.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + logging.info("程序已启动,低延迟推流中,按 Ctrl+C 停止...") + while True: + time.sleep(60) diff --git a/backup/back7 b/backup/back7 new file mode 100644 index 0000000..65ae3df --- /dev/null +++ b/backup/back7 @@ -0,0 +1,257 @@ +import os +import subprocess +import threading +import asyncio +import uuid +import time +import signal +import sys +import logging +import platform +import spider +import stream + +# ----------------- Logging Setup ----------------- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S" +) + +IS_WINDOWS = platform.system() == "Windows" + +# ----------------- Douyin to YouTube Streamer ----------------- +class DouyinToYouTubeStreamer: + def __init__(self, bitrate=5000, audio_bitrate=128, resolution='720x1280', + fps=30, gop=60, max_minutes=8): + self.push_proc = None + self.record_proc = None + self.stop_flag = threading.Event() + self.bitrate = bitrate + self.audio_bitrate = audio_bitrate + self.resolution = resolution + self.fps = fps + self.gop = gop + self.start_time = None + self.room_name = None + self.pushing = False + self.out_file = None + self.max_minutes = max_minutes + + os.makedirs("records", exist_ok=True) + + def start_stream(self, real_url, youtube_key, room_name=None): + self.room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}" + logging.info(f"[{self.room_name}] 启动低延迟推流,目标 YouTube Key: {youtube_key}") + threading.Thread(target=self._run_stream_loop, + args=(real_url, youtube_key), daemon=True).start() + threading.Thread(target=self._monitor_thread, daemon=True).start() + + # ----------------- 构建推流命令 ----------------- + def _build_push_cmd(self, real_url, youtube_key): + user_agent = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36") + headers = "Referer: https://www.douyin.com/\r\nOrigin: https://www.douyin.com\r\n" + cmd = [ + 'ffmpeg', '-y', + '-rw_timeout', '10000000', + '-analyzeduration', '1000000', + '-probesize', '5000000', + '-user_agent', user_agent, + '-headers', headers, + '-protocol_whitelist', 'file,pipe,http,https,tcp,tls,udp,rtp,crypto', + '-fflags', '+discardcorrupt+genpts', + '-flags', 'low_delay', + '-http_seekable', '0', + '-i', real_url, + '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', + '-b:v', f'{self.bitrate}k', + '-maxrate', f'{self.bitrate}k', + '-bufsize', f'{self.bitrate * 2}k', + '-r', str(self.fps), + '-g', str(self.gop), + '-c:a', 'aac', '-b:a', f'{self.audio_bitrate}k', '-ar', '44100', '-ac', '2', + '-f', 'flv', + f'rtmp://a.rtmp.youtube.com/live2/{youtube_key}' + ] + return cmd + + # ----------------- 构建录制命令 ----------------- + def _build_record_cmd(self, real_url): + base_name = f"{self.room_name}_{int(time.time())}.mp4" + self.out_file = os.path.join("records", base_name).replace("\\", "/") + cmd = [ + 'ffmpeg', '-y', + '-i', real_url, + '-c:v', 'libx264', + '-preset', 'veryfast', + '-tune', 'zerolatency', + '-c:a', 'aac', + '-b:a', f'{self.audio_bitrate}k', + '-ar', '44100', + '-ac', '2', + '-movflags', '+faststart+frag_keyframe', + self.out_file + ] + return cmd + + # ----------------- 推流循环 ----------------- + def _run_stream_loop(self, real_url, youtube_key): + while not self.stop_flag.is_set(): + try: + self.start_time = time.time() + self.pushing = False + + # ---- 启动推流 ---- + push_cmd = self._build_push_cmd(real_url, youtube_key) + self.push_proc = subprocess.Popen( + push_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + # ---- 启动本地录制 ---- + record_cmd = self._build_record_cmd(real_url) + self.record_proc = subprocess.Popen( + record_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + logging.info(f"[{self.room_name}] 本地录制开始: {self.out_file}") + logging.info(f"[{self.room_name}] 推流进程 PID: {self.push_proc.pid}") + + # ---- 监控 stderr ---- + def monitor_proc(proc, name): + try: + for line in iter(proc.stderr.readline, b''): + if self.stop_flag.is_set(): + break + if line: + decoded = line.decode('utf-8', errors='ignore').strip() + if name == "push" and not self.pushing and "frame=" in decoded and "fps=" in decoded: + self.pushing = True + logging.info(f"[{self.room_name}] 推流到 YouTube 成功") + logging.debug(f"[{self.room_name} {name}]: {decoded}") + except Exception as e: + logging.error(f"[{self.room_name} {name} stderr异常: {e}") + + threading.Thread(target=monitor_proc, args=(self.push_proc, "push"), daemon=True).start() + threading.Thread(target=monitor_proc, args=(self.record_proc, "record"), daemon=True).start() + + # ---- 循环检查 ---- + while not self.stop_flag.is_set(): + elapsed = time.time() - self.start_time + if elapsed >= self.max_minutes * 60: + logging.warning(f"[{self.room_name}] 超过 {self.max_minutes} 分钟,自动停止推流和录制") + self.stop() + break + if self.push_proc.poll() is not None or self.record_proc.poll() is not None: + logging.warning(f"[{self.room_name}] FFmpeg 进程退出,1秒后重启...") + break + time.sleep(2) + + except Exception as e: + logging.error(f"[{self.room_name}] FFmpeg异常: {e}") + finally: + self._terminate_procs() + self._finalize_recording() + if not self.stop_flag.is_set(): + time.sleep(1) + + def _terminate_procs(self): + for proc in [self.push_proc, self.record_proc]: + if proc and proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + self.push_proc, self.record_proc = None, None + + # ----------------- 监控线程 ----------------- + def _monitor_thread(self): + while not self.stop_flag.is_set(): + uptime = int(time.time() - self.start_time) if self.start_time else 0 + m, s = divmod(uptime, 60) + status = "推流中" if self.pushing else "启动中" + print(f"\r[{self.room_name}] {status} | 运行时间: {m:02d}:{s:02d} | 码率: {self.bitrate}k", end="") + time.sleep(2) + + # ----------------- 停止 ----------------- + def stop(self): + logging.info(f"[{self.room_name}] 停止推流和录制...") + self.stop_flag.set() + self._terminate_procs() + self._finalize_recording() + + def _finalize_recording(self): + if self.out_file and os.path.exists(self.out_file): + try: + # 根据系统发送信号完成封装 + if self.record_proc and self.record_proc.poll() is None: + sig = signal.CTRL_BREAK_EVENT if IS_WINDOWS else signal.SIGINT + self.record_proc.send_signal(sig) + try: + self.record_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self.record_proc.kill() + except Exception: + pass + + # 重命名文件带时长 + elapsed = int(time.time() - self.start_time) if self.start_time else 0 + suffix = f"_{elapsed // 60}min" if elapsed >= 60 else f"_{elapsed}s" + new_name = self.out_file.replace(".mp4", f"{suffix}.mp4") + try: + os.rename(self.out_file, new_name) + except FileExistsError: + new_name = self.out_file.replace(".mp4", f"_{uuid.uuid4().hex[:4]}.mp4") + os.rename(self.out_file, new_name) + size_mb = os.path.getsize(new_name) / (1024 * 1024) + logging.info(f"[{self.room_name}] 本地录制已结束: {new_name} | 大小: {size_mb:.2f} MB") + +# ----------------- Douyin Stream Parsing ----------------- +async def get_douyin_real_url(record_url, record_quality="原画", proxy_addr=None, cookies=''): + try: + 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, + proxy_addr=proxy_addr, cookies=cookies) + else: + json_data = await spider.get_douyin_app_stream_data(url=record_url, + proxy_addr=proxy_addr, cookies=cookies) + port_info = await stream.get_douyin_stream_url(json_data, record_quality, proxy_addr) + return port_info.get("record_url") + except Exception as e: + logging.error(f"获取抖音真实流失败: {e}") + return None + +def start_douyin_to_youtube(record_url, youtube_key, room_name=None, + proxy_addr=None, cookies='', bitrate=5000): + real_url = asyncio.run(get_douyin_real_url(record_url, proxy_addr, cookies)) + if not real_url: + logging.error("获取真实直播流失败") + return None + logging.info(f"实际直播流: {real_url}") + streamer = DouyinToYouTubeStreamer(bitrate=bitrate) + streamer.start_stream(real_url, youtube_key, room_name) + return streamer + +# ----------------- Main Program ----------------- +if __name__ == "__main__": + DOUYIN_URL = "https://live.douyin.com/321245312734" + YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" + + streamer_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY) + if not streamer_instance: + logging.error("推流实例创建失败,程序退出") + sys.exit(1) + + def signal_handler(sig, frame): + if streamer_instance: + streamer_instance.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + logging.info("程序已启动,低延迟推流中,按 Ctrl+C 停止...") + + while True: + time.sleep(60) diff --git a/backup/back8 b/backup/back8 new file mode 100644 index 0000000..7f65b92 --- /dev/null +++ b/backup/back8 @@ -0,0 +1,287 @@ +import os +import subprocess +import threading +import asyncio +import uuid +import time +import signal +import sys +import logging +import platform +import spider +import stream +import aiohttp + +# ----------------- Logging Setup ----------------- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S" +) + +IS_WINDOWS = platform.system() == "Windows" + +# ----------------- Douyin to YouTube Streamer ----------------- +class DouyinToYouTubeStreamer: + def __init__(self, bitrate=5000, audio_bitrate=128, resolution='720x1280', + fps=30, gop=60, max_minutes=8): + self.push_proc = None + self.record_proc = None + self.stop_flag = threading.Event() + self.bitrate = bitrate + self.audio_bitrate = audio_bitrate + self.resolution = resolution + self.fps = fps + self.gop = gop + self.start_time = None + self.room_name = None + self.pushing = False + self.out_file = None + self.max_minutes = max_minutes + + os.makedirs("records", exist_ok=True) + + def start_stream(self, real_url, youtube_key, room_name=None): + self.room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}" + logging.info(f"[{self.room_name}] 启动低延迟推流,目标 YouTube Key: {youtube_key}") + threading.Thread(target=self._run_stream_loop, + args=(real_url, youtube_key), daemon=True).start() + threading.Thread(target=self._monitor_thread, daemon=True).start() + + # ----------------- 构建推流命令 ----------------- + def _build_push_cmd(self, real_url, youtube_key): + user_agent = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36") + headers = "Referer: https://www.douyin.com/\r\nOrigin: https://www.douyin.com\r\n" + cmd = [ + 'ffmpeg', '-y', + '-rw_timeout', '10000000', + '-analyzeduration', '1000000', + '-probesize', '5000000', + '-user_agent', user_agent, + '-headers', headers, + '-protocol_whitelist', 'file,pipe,http,https,tcp,tls,udp,rtp,crypto', + '-fflags', '+discardcorrupt+genpts', + '-flags', 'low_delay', + '-http_seekable', '0', + '-i', real_url, + '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', + '-b:v', f'{self.bitrate}k', + '-maxrate', f'{self.bitrate}k', + '-bufsize', f'{self.bitrate * 2}k', + '-r', str(self.fps), + '-g', str(self.gop), + '-c:a', 'aac', '-b:a', f'{self.audio_bitrate}k', '-ar', '44100', '-ac', '2', + '-f', 'flv', + f'rtmp://a.rtmp.youtube.com/live2/{youtube_key}' + ] + return cmd + + # ----------------- 构建录制命令 ----------------- + def _build_record_cmd(self, real_url): + base_name = f"{self.room_name}_{int(time.time())}.mp4" + self.out_file = os.path.join("records", base_name).replace("\\", "/") + cmd = [ + 'ffmpeg', '-y', + '-i', real_url, + '-c:v', 'libx264', + '-preset', 'veryfast', + '-tune', 'zerolatency', + '-c:a', 'aac', + '-b:a', f'{self.audio_bitrate}k', + '-ar', '44100', + '-ac', '2', + '-movflags', '+faststart+frag_keyframe', + self.out_file + ] + return cmd + + # ----------------- 推流循环 ----------------- + def _run_stream_loop(self, real_url, youtube_key): + while not self.stop_flag.is_set(): + try: + self.start_time = time.time() + self.pushing = False + + # ---- 启动推流 ---- + push_cmd = self._build_push_cmd(real_url, youtube_key) + self.push_proc = subprocess.Popen( + push_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + # ---- 启动本地录制 ---- + record_cmd = self._build_record_cmd(real_url) + self.record_proc = subprocess.Popen( + record_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + logging.info(f"[{self.room_name}] 本地录制开始: {self.out_file}") + logging.info(f"[{self.room_name}] 推流进程 PID: {self.push_proc.pid}") + + # ---- 监控 stderr ---- + def monitor_proc(proc, name): + try: + for line in iter(proc.stderr.readline, b''): + if self.stop_flag.is_set(): + break + if line: + decoded = line.decode('utf-8', errors='ignore').strip() + if name == "push" and not self.pushing and "frame=" in decoded and "fps=" in decoded: + self.pushing = True + logging.info(f"[{self.room_name}] 推流到 YouTube 成功") + logging.debug(f"[{self.room_name} {name}]: {decoded}") + except Exception as e: + logging.error(f"[{self.room_name} {name} stderr异常: {e}") + + threading.Thread(target=monitor_proc, args=(self.push_proc, "push"), daemon=True).start() + threading.Thread(target=monitor_proc, args=(self.record_proc, "record"), daemon=True).start() + + # ---- 循环检查 ---- + while not self.stop_flag.is_set(): + elapsed = time.time() - self.start_time + if elapsed >= self.max_minutes * 60: + logging.warning(f"[{self.room_name}] 超过 {self.max_minutes} 分钟,自动停止推流和录制") + self.stop() + break + if self.push_proc.poll() is not None or self.record_proc.poll() is not None: + logging.warning(f"[{self.room_name}] FFmpeg 进程退出,1秒后重启...") + break + time.sleep(2) + + except Exception as e: + logging.error(f"[{self.room_name}] FFmpeg异常: {e}") + finally: + self._terminate_procs() + self._finalize_recording() + if not self.stop_flag.is_set(): + time.sleep(1) + + def _terminate_procs(self): + for proc in [self.push_proc, self.record_proc]: + if proc and proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + self.push_proc, self.record_proc = None, None + + # ----------------- 监控线程 ----------------- + def _monitor_thread(self): + while not self.stop_flag.is_set(): + uptime = int(time.time() - self.start_time) if self.start_time else 0 + m, s = divmod(uptime, 60) + status = "推流中" if self.pushing else "启动中" + print(f"\r[{self.room_name}] {status} | 运行时间: {m:02d}:{s:02d} | 码率: {self.bitrate}k", end="") + time.sleep(2) + + # ----------------- 停止 ----------------- + def stop(self): + logging.info(f"[{self.room_name}] 停止推流和录制...") + self.stop_flag.set() + self._terminate_procs() + self._finalize_recording() + + def _finalize_recording(self): + if self.out_file and os.path.exists(self.out_file): + try: + if self.record_proc and self.record_proc.poll() is None: + sig = signal.CTRL_BREAK_EVENT if IS_WINDOWS else signal.SIGINT + self.record_proc.send_signal(sig) + try: + self.record_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self.record_proc.kill() + except Exception: + pass + + elapsed = int(time.time() - self.start_time) if self.start_time else 0 + suffix = f"_{elapsed // 60}min" if elapsed >= 60 else f"_{elapsed}s" + new_name = self.out_file.replace(".mp4", f"{suffix}.mp4") + try: + os.rename(self.out_file, new_name) + except FileExistsError: + new_name = self.out_file.replace(".mp4", f"_{uuid.uuid4().hex[:4]}.mp4") + os.rename(self.out_file, new_name) + size_mb = os.path.getsize(new_name) / (1024 * 1024) + logging.info(f"[{self.room_name}] 本地录制已结束: {new_name} | 大小: {size_mb:.2f} MB") + +# ----------------- Douyin Stream Parsing ----------------- +async def get_douyin_real_url(record_url, record_quality="原画", proxy_addr=None, cookies=''): + headers = { + "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"), + "Referer": "https://www.douyin.com/", + "Origin": "https://www.douyin.com", + "Accept": "application/json, text/javascript, */*; q=0.01", + } + + session_args = {"headers": headers} + if proxy_addr: + session_args["proxy"] = proxy_addr + + try: + async with aiohttp.ClientSession(**session_args) as session: + for attempt in range(3): + try: + async with session.get(record_url, cookies=cookies) as resp: + text = await resp.text() + if not text or "captcha" in text.lower(): + logging.warning(f"第{attempt+1}次尝试获取流失败,返回内容为空或验证码,重试...") + await asyncio.sleep(1) + continue + 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, proxy_addr=proxy_addr, cookies=cookies) + else: + json_data = await spider.get_douyin_app_stream_data( + url=record_url, proxy_addr=proxy_addr, cookies=cookies) + + port_info = await stream.get_douyin_stream_url( + json_data, record_quality, proxy_addr) + if port_info and port_info.get("record_url"): + return port_info.get("record_url") + else: + logging.warning(f"第{attempt+1}次尝试解析失败,port_info为空") + await asyncio.sleep(1) + except Exception as e: + logging.warning(f"第{attempt+1}次尝试抓取抖音流异常: {e}") + await asyncio.sleep(1) + logging.error("多次尝试后仍获取抖音真实流失败") + return None + except Exception as e: + logging.error(f"初始化 aiohttp session 失败: {e}") + return None + +def start_douyin_to_youtube(record_url, youtube_key, room_name=None, + proxy_addr=None, cookies='', bitrate=5000): + real_url = asyncio.run(get_douyin_real_url(record_url, proxy_addr=proxy_addr, cookies=cookies)) + if not real_url: + logging.error("获取真实直播流失败") + return None + logging.info(f"实际直播流: {real_url}") + streamer = DouyinToYouTubeStreamer(bitrate=bitrate) + streamer.start_stream(real_url, youtube_key, room_name) + return streamer + +# ----------------- Main Program ----------------- +if __name__ == "__main__": + DOUYIN_URL = "https://live.douyin.com/321245312734" + YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" + + streamer_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY) + if not streamer_instance: + logging.error("推流实例创建失败,程序退出") + sys.exit(1) + + def signal_handler(sig, frame): + if streamer_instance: + streamer_instance.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + logging.info("程序已启动,低延迟推流中,按 Ctrl+C 停止...") + while True: + time.sleep(60) diff --git a/backup/back9 b/backup/back9 new file mode 100644 index 0000000..78d3954 --- /dev/null +++ b/backup/back9 @@ -0,0 +1,275 @@ +import os +import subprocess +import threading +import asyncio +import uuid +import time +import signal +import sys +import logging +import platform +import aiohttp +import spider # 你的 spider 模块 +import stream # 你的 stream 模块 + +# ----------------- Logging Setup ----------------- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S" +) + +IS_WINDOWS = platform.system() == "Windows" + +# ----------------- Douyin to YouTube Streamer ----------------- +class DouyinToYouTubeStreamer: + def __init__(self, bitrate=5000, audio_bitrate=128, resolution='720x1280', + fps=30, gop=60, max_minutes=None, enable_recording=False): + self.push_proc = None + self.record_proc = None + self.stop_flag = threading.Event() + self.bitrate = bitrate + self.audio_bitrate = audio_bitrate + self.resolution = resolution + self.fps = fps + self.gop = gop + self.start_time = None + self.room_name = None + self.pushing = False + self.out_file = None + self.max_minutes = max_minutes # None 表示无限制 + self.enable_recording = enable_recording + + os.makedirs("records", exist_ok=True) + + # ----------------- 启动推流 ----------------- + def start_stream(self, real_url, youtube_key, room_name=None): + self.room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}" + logging.info(f"[{self.room_name}] 启动低延迟推流,目标 YouTube Key: {youtube_key}") + threading.Thread(target=self._run_stream_loop, + args=(real_url, youtube_key), daemon=True).start() + threading.Thread(target=self._monitor_thread, daemon=True).start() + + # ----------------- 构建推流命令 ----------------- + def _build_push_cmd(self, real_url, youtube_key): + user_agent = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36") + headers = "Referer: https://www.douyin.com/\r\nOrigin: https://www.douyin.com\r\n" + cmd = [ + 'ffmpeg', '-y', + '-rw_timeout', '10000000', + '-analyzeduration', '1000000', + '-probesize', '5000000', + '-user_agent', user_agent, + '-headers', headers, + '-protocol_whitelist', 'file,pipe,http,https,tcp,tls,udp,rtp,crypto', + '-fflags', '+discardcorrupt+genpts', + '-flags', 'low_delay', + '-http_seekable', '0', + '-i', real_url, + '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', + '-b:v', f'{self.bitrate}k', + '-maxrate', f'{self.bitrate}k', + '-bufsize', f'{self.bitrate * 2}k', + '-r', str(self.fps), + '-g', str(self.gop), + '-s', self.resolution, + '-c:a', 'aac', '-b:a', f'{self.audio_bitrate}k', '-ar', '44100', '-ac', '2', + '-f', 'flv', + f'rtmp://a.rtmp.youtube.com/live2/{youtube_key}' + ] + return cmd + + # ----------------- 构建录制命令 ----------------- + def _build_record_cmd(self, real_url): + base_name = f"{self.room_name}_{int(time.time())}.mp4" + self.out_file = os.path.join("records", base_name).replace("\\", "/") + cmd = [ + 'ffmpeg', '-y', + '-i', real_url, + '-c:v', 'libx264', + '-preset', 'veryfast', + '-tune', 'zerolatency', + '-c:a', 'aac', + '-b:a', f'{self.audio_bitrate}k', + '-ar', '44100', + '-ac', '2', + '-movflags', '+faststart+frag_keyframe', + self.out_file + ] + return cmd + + # ----------------- 推流循环 ----------------- + def _run_stream_loop(self, real_url, youtube_key): + while not self.stop_flag.is_set(): + try: + self.start_time = time.time() + self.pushing = False + + # ---- 启动推流 ---- + push_cmd = self._build_push_cmd(real_url, youtube_key) + self.push_proc = subprocess.Popen( + push_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + bufsize=1, universal_newlines=True + ) + logging.info(f"[{self.room_name}] 推流进程 PID: {self.push_proc.pid}") + + # ---- 启动本地录制 ---- + if self.enable_recording: + record_cmd = self._build_record_cmd(real_url) + self.record_proc = subprocess.Popen( + record_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + bufsize=1, universal_newlines=True + ) + logging.info(f"[{self.room_name}] 本地录制开始: {self.out_file}") + + # ---- monitor stderr 阻塞读取(Windows兼容) ---- + def monitor_proc(proc, name): + try: + while not self.stop_flag.is_set() and proc and proc.poll() is None: + line = proc.stderr.readline() + if not line: + continue + if name == "push" and not self.pushing and "frame=" in line and "fps=" in line: + self.pushing = True + logging.info(f"[{self.room_name}] 推流到 YouTube 成功") + except Exception as e: + logging.error(f"[{self.room_name} {name} stderr异常: {e}") + + threading.Thread(target=monitor_proc, args=(self.push_proc, "push"), daemon=True).start() + if self.enable_recording: + threading.Thread(target=monitor_proc, args=(self.record_proc, "record"), daemon=True).start() + + # ---- 循环检查进程状态 ---- + while not self.stop_flag.is_set(): + elapsed = time.time() - self.start_time + if self.max_minutes and elapsed >= self.max_minutes * 60: + logging.warning(f"[{self.room_name}] 超过 {self.max_minutes} 分钟,自动停止推流和录制") + self.stop() + break + + if self.push_proc.poll() is not None: + logging.warning(f"[{self.room_name}] 推流进程退出,5秒后重启...") + time.sleep(5) + break + if self.enable_recording and self.record_proc and self.record_proc.poll() is not None: + logging.warning(f"[{self.room_name}] 录制进程退出,5秒后重启...") + time.sleep(5) + break + + time.sleep(2) + + except Exception as e: + logging.error(f"[{self.room_name}] FFmpeg异常: {e}") + finally: + self._terminate_procs() + if not self.stop_flag.is_set(): + time.sleep(1) + + def _terminate_procs(self): + for proc in [self.push_proc, self.record_proc]: + if proc and proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + self.push_proc, self.record_proc = None, None + + # ----------------- 监控线程 ----------------- + def _monitor_thread(self): + while not self.stop_flag.is_set(): + uptime = int(time.time() - self.start_time) if self.start_time else 0 + m, s = divmod(uptime, 60) + status = "推流中" if self.pushing else "启动中" + print(f"\r[{self.room_name}] {status} | 运行时间: {m:02d}:{s:02d} | 码率: {self.bitrate}k", end="") + time.sleep(5) + + # ----------------- 停止 ----------------- + def stop(self): + logging.info(f"[{self.room_name}] 停止推流和录制...") + self.stop_flag.set() + self._terminate_procs() + + +# ----------------- Douyin Stream Parsing ----------------- +async def get_douyin_real_url(record_url, record_quality="原画", proxy_addr=None, cookies=''): + headers = { + "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"), + "Referer": "https://www.douyin.com/", + "Origin": "https://www.douyin.com", + "Accept": "application/json, text/javascript, */*; q=0.01", + } + + session_args = {"headers": headers} + if proxy_addr: + session_args["proxy"] = proxy_addr + + try: + async with aiohttp.ClientSession(**session_args) as session: + for attempt in range(3): + try: + async with session.get(record_url, cookies=cookies) as resp: + text = await resp.text() + if not text or "captcha" in text.lower(): + logging.warning(f"第{attempt+1}次尝试获取流失败,返回内容为空或验证码,重试...") + await asyncio.sleep(1) + continue + 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, proxy_addr=proxy_addr, cookies=cookies) + else: + json_data = await spider.get_douyin_app_stream_data( + url=record_url, proxy_addr=proxy_addr, cookies=cookies) + + port_info = await stream.get_douyin_stream_url( + json_data, record_quality, proxy_addr) + if port_info and port_info.get("record_url"): + return port_info.get("record_url") + else: + logging.warning(f"第{attempt+1}次尝试解析失败,port_info为空") + await asyncio.sleep(1) + except Exception as e: + logging.warning(f"第{attempt+1}次尝试抓取抖音流异常: {e}") + await asyncio.sleep(1) + logging.error("多次尝试后仍获取抖音真实流失败") + return None + except Exception as e: + logging.error(f"初始化 aiohttp session 失败: {e}") + return None + +def start_douyin_to_youtube(record_url, youtube_key, room_name=None, + proxy_addr=None, cookies='', bitrate=5000, enable_recording=False): + real_url = asyncio.run(get_douyin_real_url(record_url, proxy_addr=proxy_addr, cookies=cookies)) + if not real_url: + logging.error("获取真实直播流失败") + return None + logging.info(f"实际直播流: {real_url}") + streamer = DouyinToYouTubeStreamer(bitrate=bitrate, enable_recording=enable_recording, max_minutes=None) + streamer.start_stream(real_url, youtube_key, room_name) + return streamer + + +# ----------------- Main Program ----------------- +if __name__ == "__main__": + DOUYIN_URL = "https://live.douyin.com/851742623054" + YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" + + # enable_recording=True 可开启本地录制 + streamer_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, enable_recording=False) + if not streamer_instance: + logging.error("推流实例创建失败,程序退出") + sys.exit(1) + + def signal_handler(sig, frame): + if streamer_instance: + streamer_instance.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + logging.info("程序已启动,低延迟推流中,按 Ctrl+C 停止...") + while True: + time.sleep(60) diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..31625fa --- /dev/null +++ b/config.ini @@ -0,0 +1 @@ +qxvb-r47b-r5ju-6ud3-6k7z \ No newline at end of file diff --git a/config/URL_config.ini b/config/URL_config.ini new file mode 100644 index 0000000..0b59dd9 --- /dev/null +++ b/config/URL_config.ini @@ -0,0 +1,8 @@ +https://live.douyin.com/210443559964,主播: 小小诺 +https://live.douyin.com/525514386431,主播: 阿闯烤肉夹馍 +https://live.douyin.com/8687122573,主播: 小彤炒粉 +https://live.douyin.com/642534242822,主播: (小王_)煎饼果子 +https://live.douyin.com/589037028237,主播: 娜姐辉哥在奋斗 +https://live.douyin.com/743565594721,主播: 汪汪汪小妞 +https://live.douyin.com/249578288248,主播: 思思努力版 +https://live.douyin.com/153648759493,主播: 晨晨_烤冷面 diff --git a/config/config.ini b/config/config.ini new file mode 100644 index 0000000..aede5da --- /dev/null +++ b/config/config.ini @@ -0,0 +1,129 @@ +[录制设置] +language(zh_cn/en) = en +是否跳过代理检测(是/否) = 否 +直播保存路径(不填则默认) = +保存文件夹是否以作者区分 = 是 +保存文件夹是否以时间区分 = 否 +保存文件夹是否以标题区分 = 否 +保存文件名是否包含标题 = 否 +是否去除名称中的表情符号 = 是 +视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频 = mp4 +原画|超清|高清|标清|流畅 = 原画 +是否使用代理ip(是/否) = 是 +代理地址 = +同一时间访问网络的线程数 = 20 +循环时间(秒) = 300 +排队读取网址时间(秒) = 0 +是否显示循环秒数 = 否 +是否显示直播源地址 = 是 +分段录制是否开启 = 是 +是否强制启用https录制 = 否 +录制空间剩余阈值(gb) = 1.0 +视频分段时间(秒) = 480 +录制完成后自动转为mp4格式 = 是 +mp4格式重新编码为h264 = 否 +追加格式后删除原文件 = 是 +生成时间字幕文件 = 否 +是否录制完成后执行自定义脚本 = 否 +自定义脚本执行命令 = +使用代理录制的平台(逗号分隔) = tiktok, sooplive, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu +额外使用代理录制的平台(逗号分隔) = + +[推送配置] +直播状态推送渠道 = +钉钉推送接口链接 = +微信推送接口链接 = +bark推送接口链接 = +bark推送中断级别 = active +bark推送铃声 = +钉钉通知@对象(填手机号) = +钉钉通知@全体(是/否) = 否 +tgapi令牌 = +tg聊天id(个人或者群组id) = +smtp邮件服务器 = +是否使用smtp服务ssl加密(是/否) = +smtp邮件服务器端口 = +邮箱登录账号 = +发件人密码(授权码) = +发件人邮箱 = +发件人显示昵称 = +收件人邮箱 = +ntfy推送地址 = https://ntfy.sh/xxxx +ntfy推送标签 = tada +ntfy推送邮箱 = +pushplus推送token = +自定义推送标题 = +自定义开播推送内容 = +自定义关播推送内容 = +只推送通知不录制(是/否) = 否 +直播推送检测频率(秒) = 1800 +开播推送开启(是/否) = 是 +关播推送开启(是/否) = 否 + +[Cookie] +抖音cookie = ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986 +快手cookie = +tiktok_cookie = +虎牙cookie = +斗鱼cookie = +yy_cookie = +b站cookie = +小红书cookie = +bigo_cookie = +blued_cookie = +sooplive_cookie = +netease_cookie = +千度热播_cookie = +pandatv_cookie = +猫耳fm_cookie = +winktv_cookie = +flextv_cookie = +look_cookie = +twitcasting_cookie = +baidu_cookie = +weibo_cookie = +kugou_cookie = +twitch_cookie = +liveme_cookie = +huajiao_cookie = +liuxing_cookie = +showroom_cookie = +acfun_cookie = +changliao_cookie = +yinbo_cookie = +yingke_cookie = +zhihu_cookie = +chzzk_cookie = +haixiu_cookie = +vvxqiu_cookie = +17live_cookie = +langlive_cookie = +pplive_cookie = +6room_cookie = +lehaitv_cookie = +huamao_cookie = +shopee_cookie = +youtube_cookie = +taobao_cookie = +jd_cookie = +faceit_cookie = +migu_cookie = +lianjie_cookie = +laixiu_cookie = +picarto_cookie = + +[Authorization] +popkontv_token = + +[账号密码] +sooplive账号 = +sooplive密码 = +flextv账号 = +flextv密码 = +popkontv账号 = +partner_code = P-00001 +popkontv密码 = +twitcasting账号类型 = normal +twitcasting账号 = +twitcasting密码 = + diff --git a/config/url.txt b/config/url.txt new file mode 100644 index 0000000..c545076 --- /dev/null +++ b/config/url.txt @@ -0,0 +1,9 @@ +https://live.douyin.com/270711345461,主播: 茄茄 +https://live.douyin.com/957925266814,主播: 卓力君 +https://live.douyin.com/292625589966,主播: 思了个思(游戏版) +https://live.douyin.com/52070571116,主播: 兔然不困了 +https://live.douyin.com/913137650417,主播: 发千金 +https://live.douyin.com/905966903795,主播: 总被男人骗 +https://live.douyin.com/97282309368,主播: 会跳舞的饭 +https://live.douyin.com/640697071840,主播: 煎饼先生 +https://live.douyin.com/167254117139,主播: 乔妮妮 \ No newline at end of file diff --git a/http_clients/__init__.py b/http_clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/http_clients/async_http.py b/http_clients/async_http.py new file mode 100644 index 0000000..0bea871 --- /dev/null +++ b/http_clients/async_http.py @@ -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 diff --git a/http_clients/sync_http.py b/http_clients/sync_http.py new file mode 100644 index 0000000..76d7c07 --- /dev/null +++ b/http_clients/sync_http.py @@ -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 diff --git a/initializer.py b/initializer.py new file mode 100644 index 0000000..faa9f7b --- /dev/null +++ b/initializer.py @@ -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() diff --git a/javascript/crypto-js.min.js b/javascript/crypto-js.min.js new file mode 100644 index 0000000..fb96be7 --- /dev/null +++ b/javascript/crypto-js.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports?module.exports=exports=e():"function"==typeof define&&define.amd?define([],e):t.CryptoJS=e()}(this,function(){var W,O,I,U,K,X,L,l,j,T,t,N,q,e,Z,V,G,J,Q,Y,$,t1,e1,r1,i1,o1,n1,s,s1,c1,a1,h1,l1,o,f1,r,d1,u1,n,c,a,h,f,d,i=function(h){var i;if("undefined"!=typeof window&&window.crypto&&(i=window.crypto),"undefined"!=typeof self&&self.crypto&&(i=self.crypto),!(i=!(i=!(i="undefined"!=typeof globalThis&&globalThis.crypto?globalThis.crypto:i)&&"undefined"!=typeof window&&window.msCrypto?window.msCrypto:i)&&"undefined"!=typeof global&&global.crypto?global.crypto:i)&&"function"==typeof require)try{i=require("crypto")}catch(t){}var r=Object.create||function(t){return e.prototype=t,t=new e,e.prototype=null,t};function e(){}var t={},o=t.lib={},n=o.Base={extend:function(t){var e=r(this);return t&&e.mixIn(t),e.hasOwnProperty("init")&&this.init!==e.init||(e.init=function(){e.$super.init.apply(this,arguments)}),(e.init.prototype=e).$super=this,e},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var e in t)t.hasOwnProperty(e)&&(this[e]=t[e]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},l=o.WordArray=n.extend({init:function(t,e){t=this.words=t||[],this.sigBytes=null!=e?e:4*t.length},toString:function(t){return(t||c).stringify(this)},concat:function(t){var e=this.words,r=t.words,i=this.sigBytes,o=t.sigBytes;if(this.clamp(),i%4)for(var n=0;n>>2]>>>24-n%4*8&255;e[i+n>>>2]|=s<<24-(i+n)%4*8}else for(var c=0;c>>2]=r[c>>>2];return this.sigBytes+=o,this},clamp:function(){var t=this.words,e=this.sigBytes;t[e>>>2]&=4294967295<<32-e%4*8,t.length=h.ceil(e/4)},clone:function(){var t=n.clone.call(this);return t.words=this.words.slice(0),t},random:function(t){for(var e=[],r=0;r>>2]>>>24-o%4*8&255;i.push((n>>>4).toString(16)),i.push((15&n).toString(16))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i>>3]|=parseInt(t.substr(i,2),16)<<24-i%8*4;return new l.init(r,e/2)}},a=s.Latin1={stringify:function(t){for(var e=t.words,r=t.sigBytes,i=[],o=0;o>>2]>>>24-o%4*8&255;i.push(String.fromCharCode(n))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i>>2]|=(255&t.charCodeAt(i))<<24-i%4*8;return new l.init(r,e)}},f=s.Utf8={stringify:function(t){try{return decodeURIComponent(escape(a.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return a.parse(unescape(encodeURIComponent(t)))}},d=o.BufferedBlockAlgorithm=n.extend({reset:function(){this._data=new l.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=f.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(t){var e,r=this._data,i=r.words,o=r.sigBytes,n=this.blockSize,s=o/(4*n),c=(s=t?h.ceil(s):h.max((0|s)-this._minBufferSize,0))*n,t=h.min(4*c,o);if(c){for(var a=0;a>>2]|=t[i]<<24-i%4*8;I.call(this,r,e)}else I.apply(this,arguments)}).prototype=p),i),p1=u.lib.WordArray;function _1(t){return t<<8&4278255360|t>>>8&16711935}(u=u.enc).Utf16=u.Utf16BE={stringify:function(t){for(var e=t.words,r=t.sigBytes,i=[],o=0;o>>2]>>>16-o%4*8&65535;i.push(String.fromCharCode(n))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i>>1]|=t.charCodeAt(i)<<16-i%2*16;return p1.create(r,2*e)}},u.Utf16LE={stringify:function(t){for(var e=t.words,r=t.sigBytes,i=[],o=0;o>>2]>>>16-o%4*8&65535);i.push(String.fromCharCode(n))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i>>1]|=_1(t.charCodeAt(i)<<16-i%2*16);return p1.create(r,2*e)}},U=(p=i).lib.WordArray,p.enc.Base64={stringify:function(t){for(var e=t.words,r=t.sigBytes,i=this._map,o=(t.clamp(),[]),n=0;n>>2]>>>24-n%4*8&255)<<16|(e[n+1>>>2]>>>24-(n+1)%4*8&255)<<8|e[n+2>>>2]>>>24-(n+2)%4*8&255,c=0;c<4&&n+.75*c>>6*(3-c)&63));var a=i.charAt(64);if(a)for(;o.length%4;)o.push(a);return o.join("")},parse:function(t){var e=t.length,r=this._map;if(!(i=this._reverseMap))for(var i=this._reverseMap=[],o=0;o>>6-u%4*2,s=s|n,f[d>>>2]|=s<<24-d%4*8,d++);return U.create(f,d)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="},K=(u=i).lib.WordArray,u.enc.Base64url={stringify:function(t,e){for(var r=t.words,i=t.sigBytes,o=(e=void 0===e?!0:e)?this._safe_map:this._map,n=(t.clamp(),[]),s=0;s>>2]>>>24-s%4*8&255)<<16|(r[s+1>>>2]>>>24-(s+1)%4*8&255)<<8|r[s+2>>>2]>>>24-(s+2)%4*8&255,a=0;a<4&&s+.75*a>>6*(3-a)&63));var h=o.charAt(64);if(h)for(;n.length%4;)n.push(h);return n.join("")},parse:function(t,e){var r=t.length,i=(e=void 0===e?!0:e)?this._safe_map:this._map;if(!(o=this._reverseMap))for(var o=this._reverseMap=[],n=0;n>>6-u%4*2,c=c|s,f[d>>>2]|=c<<24-d%4*8,d++);return K.create(f,d)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",_safe_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"};for(var y1=Math,p=i,g1=(u=p.lib).WordArray,v1=u.Hasher,u=p.algo,A=[],B1=0;B1<64;B1++)A[B1]=4294967296*y1.abs(y1.sin(B1+1))|0;function z(t,e,r,i,o,n,s){t=t+(e&r|~e&i)+o+s;return(t<>>32-n)+e}function H(t,e,r,i,o,n,s){t=t+(e&i|r&~i)+o+s;return(t<>>32-n)+e}function C(t,e,r,i,o,n,s){t=t+(e^r^i)+o+s;return(t<>>32-n)+e}function R(t,e,r,i,o,n,s){t=t+(r^(e|~i))+o+s;return(t<>>32-n)+e}u=u.MD5=v1.extend({_doReset:function(){this._hash=new g1.init([1732584193,4023233417,2562383102,271733878])},_doProcessBlock:function(t,e){for(var r=0;r<16;r++){var i=e+r,o=t[i];t[i]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8)}var n=this._hash.words,s=t[e+0],c=t[e+1],a=t[e+2],h=t[e+3],l=t[e+4],f=t[e+5],d=t[e+6],u=t[e+7],p=t[e+8],_=t[e+9],y=t[e+10],g=t[e+11],v=t[e+12],B=t[e+13],w=t[e+14],k=t[e+15],x=z(n[0],S=n[1],m=n[2],b=n[3],s,7,A[0]),b=z(b,x,S,m,c,12,A[1]),m=z(m,b,x,S,a,17,A[2]),S=z(S,m,b,x,h,22,A[3]);x=z(x,S,m,b,l,7,A[4]),b=z(b,x,S,m,f,12,A[5]),m=z(m,b,x,S,d,17,A[6]),S=z(S,m,b,x,u,22,A[7]),x=z(x,S,m,b,p,7,A[8]),b=z(b,x,S,m,_,12,A[9]),m=z(m,b,x,S,y,17,A[10]),S=z(S,m,b,x,g,22,A[11]),x=z(x,S,m,b,v,7,A[12]),b=z(b,x,S,m,B,12,A[13]),m=z(m,b,x,S,w,17,A[14]),x=H(x,S=z(S,m,b,x,k,22,A[15]),m,b,c,5,A[16]),b=H(b,x,S,m,d,9,A[17]),m=H(m,b,x,S,g,14,A[18]),S=H(S,m,b,x,s,20,A[19]),x=H(x,S,m,b,f,5,A[20]),b=H(b,x,S,m,y,9,A[21]),m=H(m,b,x,S,k,14,A[22]),S=H(S,m,b,x,l,20,A[23]),x=H(x,S,m,b,_,5,A[24]),b=H(b,x,S,m,w,9,A[25]),m=H(m,b,x,S,h,14,A[26]),S=H(S,m,b,x,p,20,A[27]),x=H(x,S,m,b,B,5,A[28]),b=H(b,x,S,m,a,9,A[29]),m=H(m,b,x,S,u,14,A[30]),x=C(x,S=H(S,m,b,x,v,20,A[31]),m,b,f,4,A[32]),b=C(b,x,S,m,p,11,A[33]),m=C(m,b,x,S,g,16,A[34]),S=C(S,m,b,x,w,23,A[35]),x=C(x,S,m,b,c,4,A[36]),b=C(b,x,S,m,l,11,A[37]),m=C(m,b,x,S,u,16,A[38]),S=C(S,m,b,x,y,23,A[39]),x=C(x,S,m,b,B,4,A[40]),b=C(b,x,S,m,s,11,A[41]),m=C(m,b,x,S,h,16,A[42]),S=C(S,m,b,x,d,23,A[43]),x=C(x,S,m,b,_,4,A[44]),b=C(b,x,S,m,v,11,A[45]),m=C(m,b,x,S,k,16,A[46]),x=R(x,S=C(S,m,b,x,a,23,A[47]),m,b,s,6,A[48]),b=R(b,x,S,m,u,10,A[49]),m=R(m,b,x,S,w,15,A[50]),S=R(S,m,b,x,f,21,A[51]),x=R(x,S,m,b,v,6,A[52]),b=R(b,x,S,m,h,10,A[53]),m=R(m,b,x,S,y,15,A[54]),S=R(S,m,b,x,c,21,A[55]),x=R(x,S,m,b,p,6,A[56]),b=R(b,x,S,m,k,10,A[57]),m=R(m,b,x,S,d,15,A[58]),S=R(S,m,b,x,B,21,A[59]),x=R(x,S,m,b,l,6,A[60]),b=R(b,x,S,m,g,10,A[61]),m=R(m,b,x,S,a,15,A[62]),S=R(S,m,b,x,_,21,A[63]),n[0]=n[0]+x|0,n[1]=n[1]+S|0,n[2]=n[2]+m|0,n[3]=n[3]+b|0},_doFinalize:function(){for(var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes,o=(e[i>>>5]|=128<<24-i%32,y1.floor(r/4294967296)),o=(e[15+(64+i>>>9<<4)]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8),e[14+(64+i>>>9<<4)]=16711935&(r<<8|r>>>24)|4278255360&(r<<24|r>>>8),t.sigBytes=4*(e.length+1),this._process(),this._hash),n=o.words,s=0;s<4;s++){var c=n[s];n[s]=16711935&(c<<8|c>>>24)|4278255360&(c<<24|c>>>8)}return o},clone:function(){var t=v1.clone.call(this);return t._hash=this._hash.clone(),t}}),p.MD5=v1._createHelper(u),p.HmacMD5=v1._createHmacHelper(u),u=(p=i).lib,X=u.WordArray,L=u.Hasher,u=p.algo,l=[],u=u.SHA1=L.extend({_doReset:function(){this._hash=new X.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(t,e){for(var r=this._hash.words,i=r[0],o=r[1],n=r[2],s=r[3],c=r[4],a=0;a<80;a++){a<16?l[a]=0|t[e+a]:(h=l[a-3]^l[a-8]^l[a-14]^l[a-16],l[a]=h<<1|h>>>31);var h=(i<<5|i>>>27)+c+l[a];h+=a<20?1518500249+(o&n|~o&s):a<40?1859775393+(o^n^s):a<60?(o&n|o&s|n&s)-1894007588:(o^n^s)-899497514,c=s,s=n,n=o<<30|o>>>2,o=i,i=h}r[0]=r[0]+i|0,r[1]=r[1]+o|0,r[2]=r[2]+n|0,r[3]=r[3]+s|0,r[4]=r[4]+c|0},_doFinalize:function(){var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes;return e[i>>>5]|=128<<24-i%32,e[14+(64+i>>>9<<4)]=Math.floor(r/4294967296),e[15+(64+i>>>9<<4)]=r,t.sigBytes=4*e.length,this._process(),this._hash},clone:function(){var t=L.clone.call(this);return t._hash=this._hash.clone(),t}}),p.SHA1=L._createHelper(u),p.HmacSHA1=L._createHmacHelper(u);var w1=Math,p=i,k1=(u=p.lib).WordArray,x1=u.Hasher,u=p.algo,b1=[],m1=[];function S1(t){return 4294967296*(t-(0|t))|0}for(var A1=2,z1=0;z1<64;)!function(t){for(var e=w1.sqrt(t),r=2;r<=e;r++)if(!(t%r))return;return 1}(A1)||(z1<8&&(b1[z1]=S1(w1.pow(A1,.5))),m1[z1]=S1(w1.pow(A1,1/3)),z1++),A1++;var _=[],u=u.SHA256=x1.extend({_doReset:function(){this._hash=new k1.init(b1.slice(0))},_doProcessBlock:function(t,e){for(var r=this._hash.words,i=r[0],o=r[1],n=r[2],s=r[3],c=r[4],a=r[5],h=r[6],l=r[7],f=0;f<64;f++){f<16?_[f]=0|t[e+f]:(d=_[f-15],u=_[f-2],_[f]=((d<<25|d>>>7)^(d<<14|d>>>18)^d>>>3)+_[f-7]+((u<<15|u>>>17)^(u<<13|u>>>19)^u>>>10)+_[f-16]);var d=i&o^i&n^o&n,u=l+((c<<26|c>>>6)^(c<<21|c>>>11)^(c<<7|c>>>25))+(c&a^~c&h)+m1[f]+_[f],l=h,h=a,a=c,c=s+u|0,s=n,n=o,o=i,i=u+(((i<<30|i>>>2)^(i<<19|i>>>13)^(i<<10|i>>>22))+d)|0}r[0]=r[0]+i|0,r[1]=r[1]+o|0,r[2]=r[2]+n|0,r[3]=r[3]+s|0,r[4]=r[4]+c|0,r[5]=r[5]+a|0,r[6]=r[6]+h|0,r[7]=r[7]+l|0},_doFinalize:function(){var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes;return e[i>>>5]|=128<<24-i%32,e[14+(64+i>>>9<<4)]=w1.floor(r/4294967296),e[15+(64+i>>>9<<4)]=r,t.sigBytes=4*e.length,this._process(),this._hash},clone:function(){var t=x1.clone.call(this);return t._hash=this._hash.clone(),t}}),p=(p.SHA256=x1._createHelper(u),p.HmacSHA256=x1._createHmacHelper(u),j=(p=i).lib.WordArray,u=p.algo,T=u.SHA256,u=u.SHA224=T.extend({_doReset:function(){this._hash=new j.init([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428])},_doFinalize:function(){var t=T._doFinalize.call(this);return t.sigBytes-=4,t}}),p.SHA224=T._createHelper(u),p.HmacSHA224=T._createHmacHelper(u),i),H1=p.lib.Hasher,y=(u=p.x64).Word,C1=u.WordArray,u=p.algo;function g(){return y.create.apply(y,arguments)}for(var R1=[g(1116352408,3609767458),g(1899447441,602891725),g(3049323471,3964484399),g(3921009573,2173295548),g(961987163,4081628472),g(1508970993,3053834265),g(2453635748,2937671579),g(2870763221,3664609560),g(3624381080,2734883394),g(310598401,1164996542),g(607225278,1323610764),g(1426881987,3590304994),g(1925078388,4068182383),g(2162078206,991336113),g(2614888103,633803317),g(3248222580,3479774868),g(3835390401,2666613458),g(4022224774,944711139),g(264347078,2341262773),g(604807628,2007800933),g(770255983,1495990901),g(1249150122,1856431235),g(1555081692,3175218132),g(1996064986,2198950837),g(2554220882,3999719339),g(2821834349,766784016),g(2952996808,2566594879),g(3210313671,3203337956),g(3336571891,1034457026),g(3584528711,2466948901),g(113926993,3758326383),g(338241895,168717936),g(666307205,1188179964),g(773529912,1546045734),g(1294757372,1522805485),g(1396182291,2643833823),g(1695183700,2343527390),g(1986661051,1014477480),g(2177026350,1206759142),g(2456956037,344077627),g(2730485921,1290863460),g(2820302411,3158454273),g(3259730800,3505952657),g(3345764771,106217008),g(3516065817,3606008344),g(3600352804,1432725776),g(4094571909,1467031594),g(275423344,851169720),g(430227734,3100823752),g(506948616,1363258195),g(659060556,3750685593),g(883997877,3785050280),g(958139571,3318307427),g(1322822218,3812723403),g(1537002063,2003034995),g(1747873779,3602036899),g(1955562222,1575990012),g(2024104815,1125592928),g(2227730452,2716904306),g(2361852424,442776044),g(2428436474,593698344),g(2756734187,3733110249),g(3204031479,2999351573),g(3329325298,3815920427),g(3391569614,3928383900),g(3515267271,566280711),g(3940187606,3454069534),g(4118630271,4000239992),g(116418474,1914138554),g(174292421,2731055270),g(289380356,3203993006),g(460393269,320620315),g(685471733,587496836),g(852142971,1086792851),g(1017036298,365543100),g(1126000580,2618297676),g(1288033470,3409855158),g(1501505948,4234509866),g(1607167915,987167468),g(1816402316,1246189591)],D1=[],E1=0;E1<80;E1++)D1[E1]=g();u=u.SHA512=H1.extend({_doReset:function(){this._hash=new C1.init([new y.init(1779033703,4089235720),new y.init(3144134277,2227873595),new y.init(1013904242,4271175723),new y.init(2773480762,1595750129),new y.init(1359893119,2917565137),new y.init(2600822924,725511199),new y.init(528734635,4215389547),new y.init(1541459225,327033209)])},_doProcessBlock:function(W,O){for(var t=this._hash.words,e=t[0],r=t[1],i=t[2],o=t[3],n=t[4],s=t[5],c=t[6],t=t[7],I=e.high,a=e.low,U=r.high,h=r.low,K=i.high,l=i.low,X=o.high,f=o.low,L=n.high,d=n.low,j=s.high,u=s.low,T=c.high,p=c.low,N=t.high,_=t.low,y=I,g=a,v=U,B=h,w=K,k=l,q=X,x=f,b=L,m=d,Z=j,S=u,V=T,G=p,J=N,Q=_,A=0;A<80;A++)var z,H,C=D1[A],R=(A<16?(H=C.high=0|W[O+2*A],z=C.low=0|W[O+2*A+1]):(F=(P=D1[A-15]).high,P=P.low,M=(E=D1[A-2]).high,E=E.low,D=(R=D1[A-7]).high,R=R.low,$=(Y=D1[A-16]).high,H=(H=((F>>>1|P<<31)^(F>>>8|P<<24)^F>>>7)+D+((z=(D=(P>>>1|F<<31)^(P>>>8|F<<24)^(P>>>7|F<<25))+R)>>>0>>0?1:0))+((M>>>19|E<<13)^(M<<3|E>>>29)^M>>>6)+((z+=P=(E>>>19|M<<13)^(E<<3|M>>>29)^(E>>>6|M<<26))>>>0

>>0?1:0),z+=F=Y.low,C.high=H=H+$+(z>>>0>>0?1:0),C.low=z),b&Z^~b&V),D=m&S^~m&G,E=y&v^y&w^v&w,M=(g>>>28|y<<4)^(g<<30|y>>>2)^(g<<25|y>>>7),P=R1[A],Y=P.high,$=P.low,F=Q+((m>>>14|b<<18)^(m>>>18|b<<14)^(m<<23|b>>>9)),C=J+((b>>>14|m<<18)^(b>>>18|m<<14)^(b<<23|m>>>9))+(F>>>0>>0?1:0),t1=M+(g&B^g&k^B&k),J=V,Q=G,V=Z,G=S,Z=b,S=m,b=q+(C=C+R+((F=F+D)>>>0>>0?1:0)+Y+((F=F+$)>>>0<$>>>0?1:0)+H+((F=F+z)>>>0>>0?1:0))+((m=x+F|0)>>>0>>0?1:0)|0,q=w,x=k,w=v,k=B,v=y,B=g,y=C+(((y>>>28|g<<4)^(y<<30|g>>>2)^(y<<25|g>>>7))+E+(t1>>>0>>0?1:0))+((g=F+t1|0)>>>0>>0?1:0)|0;a=e.low=a+g,e.high=I+y+(a>>>0>>0?1:0),h=r.low=h+B,r.high=U+v+(h>>>0>>0?1:0),l=i.low=l+k,i.high=K+w+(l>>>0>>0?1:0),f=o.low=f+x,o.high=X+q+(f>>>0>>0?1:0),d=n.low=d+m,n.high=L+b+(d>>>0>>0?1:0),u=s.low=u+S,s.high=j+Z+(u>>>0>>0?1:0),p=c.low=p+G,c.high=T+V+(p>>>0>>0?1:0),_=t.low=_+Q,t.high=N+J+(_>>>0>>0?1:0)},_doFinalize:function(){var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes;return e[i>>>5]|=128<<24-i%32,e[30+(128+i>>>10<<5)]=Math.floor(r/4294967296),e[31+(128+i>>>10<<5)]=r,t.sigBytes=4*e.length,this._process(),this._hash.toX32()},clone:function(){var t=H1.clone.call(this);return t._hash=this._hash.clone(),t},blockSize:32}),p.SHA512=H1._createHelper(u),p.HmacSHA512=H1._createHmacHelper(u),u=(p=i).x64,t=u.Word,N=u.WordArray,u=p.algo,q=u.SHA512,u=u.SHA384=q.extend({_doReset:function(){this._hash=new N.init([new t.init(3418070365,3238371032),new t.init(1654270250,914150663),new t.init(2438529370,812702999),new t.init(355462360,4144912697),new t.init(1731405415,4290775857),new t.init(2394180231,1750603025),new t.init(3675008525,1694076839),new t.init(1203062813,3204075428)])},_doFinalize:function(){var t=q._doFinalize.call(this);return t.sigBytes-=16,t}}),p.SHA384=q._createHelper(u),p.HmacSHA384=q._createHmacHelper(u);for(var M1=Math,p=i,P1=(u=p.lib).WordArray,F1=u.Hasher,W1=p.x64.Word,u=p.algo,O1=[],I1=[],U1=[],v=1,B=0,K1=0;K1<24;K1++){O1[v+5*B]=(K1+1)*(K1+2)/2%64;var X1=(2*v+3*B)%5;v=B%5,B=X1}for(v=0;v<5;v++)for(B=0;B<5;B++)I1[v+5*B]=B+(2*v+3*B)%5*5;for(var L1=1,j1=0;j1<24;j1++){for(var T1,N1=0,q1=0,Z1=0;Z1<7;Z1++)1&L1&&((T1=(1<>>32-e}function Y1(t){return"string"==typeof t?f1:o}function $1(t,e,r){var i,o=this._iv;o?(i=o,this._iv=void 0):i=this._prevBlock;for(var n=0;n>24&255)?(r=t>>8&255,i=255&t,255===(e=t>>16&255)?(e=0,255===r?(r=0,255===i?i=0:++i):++r):++e,t=0,t=(t+=e<<16)+(r<<8)+i):t+=1<<24,t}u=u.SHA3=F1.extend({cfg:F1.cfg.extend({outputLength:512}),_doReset:function(){for(var t=this._state=[],e=0;e<25;e++)t[e]=new W1.init;this.blockSize=(1600-2*this.cfg.outputLength)/32},_doProcessBlock:function(t,e){for(var r=this._state,i=this.blockSize/2,o=0;o>>24)|4278255360&(n<<24|n>>>8);(x=r[o]).high^=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),x.low^=n}for(var c=0;c<24;c++){for(var a=0;a<5;a++){for(var h=0,l=0,f=0;f<5;f++)h^=(x=r[a+5*f]).high,l^=x.low;var d=D[a];d.high=h,d.low=l}for(a=0;a<5;a++)for(var u=D[(a+4)%5],p=D[(a+1)%5],_=p.high,p=p.low,h=u.high^(_<<1|p>>>31),l=u.low^(p<<1|_>>>31),f=0;f<5;f++)(x=r[a+5*f]).high^=h,x.low^=l;for(var y=1;y<25;y++){var g=(x=r[y]).high,v=x.low,B=O1[y],g=(l=B<32?(h=g<>>32-B,v<>>32-B):(h=v<>>64-B,g<>>64-B),D[I1[y]]);g.high=h,g.low=l}var w=D[0],k=r[0];w.high=k.high,w.low=k.low;for(a=0;a<5;a++)for(f=0;f<5;f++){var x=r[y=a+5*f],b=D[y],m=D[(a+1)%5+5*f],S=D[(a+2)%5+5*f];x.high=b.high^~m.high&S.high,x.low=b.low^~m.low&S.low}x=r[0],w=U1[c];x.high^=w.high,x.low^=w.low}},_doFinalize:function(){for(var t=this._data,e=t.words,r=(this._nDataBytes,8*t.sigBytes),i=32*this.blockSize,o=(e[r>>>5]|=1<<24-r%32,e[(M1.ceil((1+r)/i)*i>>>5)-1]|=128,t.sigBytes=4*e.length,this._process(),this._state),r=this.cfg.outputLength/8,n=r/8,s=[],c=0;c>>24)|4278255360&(h<<24|h>>>8);s.push(16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8)),s.push(h)}return new P1.init(s,r)},clone:function(){for(var t=F1.clone.call(this),e=t._state=this._state.slice(0),r=0;r<25;r++)e[r]=e[r].clone();return t}}),p.SHA3=F1._createHelper(u),p.HmacSHA3=F1._createHmacHelper(u),Math,u=(p=i).lib,e=u.WordArray,Z=u.Hasher,u=p.algo,V=e.create([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8,3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12,1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2,4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13]),G=e.create([5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12,6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2,15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13,8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14,12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11]),J=e.create([11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8,7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12,11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5,11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12,9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6]),Q=e.create([8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6,9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11,9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5,15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8,8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11]),Y=e.create([0,1518500249,1859775393,2400959708,2840853838]),$=e.create([1352829926,1548603684,1836072691,2053994217,0]),u=u.RIPEMD160=Z.extend({_doReset:function(){this._hash=e.create([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(t,e){for(var r=0;r<16;r++){var i=e+r,o=t[i];t[i]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8)}for(var n,s,c,a,h,l,f=this._hash.words,d=Y.words,u=$.words,p=V.words,_=G.words,y=J.words,g=Q.words,v=n=f[0],B=s=f[1],w=c=f[2],k=a=f[3],x=h=f[4],r=0;r<80;r+=1)l=(l=Q1(l=(l=n+t[e+p[r]]|0)+(r<16?(s^c^a)+d[0]:r<32?G1(s,c,a)+d[1]:r<48?((s|~c)^a)+d[2]:r<64?J1(s,c,a)+d[3]:(s^(c|~a))+d[4])|0,y[r]))+h|0,n=h,h=a,a=Q1(c,10),c=s,s=l,l=(l=Q1(l=(l=v+t[e+_[r]]|0)+(r<16?(B^(w|~k))+u[0]:r<32?J1(B,w,k)+u[1]:r<48?((B|~w)^k)+u[2]:r<64?G1(B,w,k)+u[3]:(B^w^k)+u[4])|0,g[r]))+x|0,v=x,x=k,k=Q1(w,10),w=B,B=l;l=f[1]+c+k|0,f[1]=f[2]+a+x|0,f[2]=f[3]+h+v|0,f[3]=f[4]+n+B|0,f[4]=f[0]+s+w|0,f[0]=l},_doFinalize:function(){for(var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes,i=(e[i>>>5]|=128<<24-i%32,e[14+(64+i>>>9<<4)]=16711935&(r<<8|r>>>24)|4278255360&(r<<24|r>>>8),t.sigBytes=4*(e.length+1),this._process(),this._hash),o=i.words,n=0;n<5;n++){var s=o[n];o[n]=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8)}return i},clone:function(){var t=Z.clone.call(this);return t._hash=this._hash.clone(),t}}),p.RIPEMD160=Z._createHelper(u),p.HmacRIPEMD160=Z._createHmacHelper(u),u=(p=i).lib.Base,t1=p.enc.Utf8,p.algo.HMAC=u.extend({init:function(t,e){t=this._hasher=new t.init,"string"==typeof e&&(e=t1.parse(e));for(var r=t.blockSize,i=4*r,t=((e=e.sigBytes>i?t.finalize(e):e).clamp(),this._oKey=e.clone()),e=this._iKey=e.clone(),o=t.words,n=e.words,s=0;s>>2];t.sigBytes-=e}},P.BlockCipher=h1.extend({cfg:h1.cfg.extend({mode:r,padding:u}),reset:function(){h1.reset.call(this);var t,e=this.cfg,r=e.iv,e=e.mode;this._xformMode==this._ENC_XFORM_MODE?t=e.createEncryptor:(t=e.createDecryptor,this._minBufferSize=1),this._mode&&this._mode.__creator==t?this._mode.init(this,r&&r.words):(this._mode=t.call(e,this,r&&r.words),this._mode.__creator=t)},_doProcessBlock:function(t,e){this._mode.processBlock(t,e)},_doFinalize:function(){var t,e=this.cfg.padding;return this._xformMode==this._ENC_XFORM_MODE?(e.pad(this._data,this.blockSize),t=this._process(!0)):(t=this._process(!0),e.unpad(t)),t},blockSize:4}),l1=P.CipherParams=p.extend({init:function(t){this.mixIn(t)},toString:function(t){return(t||this.formatter).stringify(this)}}),r=(w.format={}).OpenSSL={stringify:function(t){var e=t.ciphertext,t=t.salt,t=t?s.create([1398893684,1701076831]).concat(t).concat(e):e;return t.toString(c1)},parse:function(t){var e,t=c1.parse(t),r=t.words;return 1398893684==r[0]&&1701076831==r[1]&&(e=s.create(r.slice(2,4)),r.splice(0,4),t.sigBytes-=16),l1.create({ciphertext:t,salt:e})}},o=P.SerializableCipher=p.extend({cfg:p.extend({format:r}),encrypt:function(t,e,r,i){i=this.cfg.extend(i);var o=t.createEncryptor(r,i),e=o.finalize(e),o=o.cfg;return l1.create({ciphertext:e,key:r,iv:o.iv,algorithm:t,mode:o.mode,padding:o.padding,blockSize:t.blockSize,formatter:i.format})},decrypt:function(t,e,r,i){return i=this.cfg.extend(i),e=this._parse(e,i.format),t.createDecryptor(r,i).finalize(e.ciphertext)},_parse:function(t,e){return"string"==typeof t?e.parse(t,this):t}}),u=(w.kdf={}).OpenSSL={execute:function(t,e,r,i,o){i=i||s.random(8),o=(o?a1.create({keySize:e+r,hasher:o}):a1.create({keySize:e+r})).compute(t,i);t=s.create(o.words.slice(e),4*r);return o.sigBytes=4*e,l1.create({key:o,iv:t,salt:i})}},f1=P.PasswordBasedCipher=o.extend({cfg:o.cfg.extend({kdf:u}),encrypt:function(t,e,r,i){r=(i=this.cfg.extend(i)).kdf.execute(r,t.keySize,t.ivSize,i.salt,i.hasher),i.iv=r.iv,t=o.encrypt.call(this,t,e,r.key,i);return t.mixIn(r),t},decrypt:function(t,e,r,i){i=this.cfg.extend(i),e=this._parse(e,i.format);r=i.kdf.execute(r,t.keySize,t.ivSize,e.salt,i.hasher);return i.iv=r.iv,o.decrypt.call(this,t,e,r.key,i)}})),i.mode.CFB=((p=i.lib.BlockCipherMode.extend()).Encryptor=p.extend({processBlock:function(t,e){var r=this._cipher,i=r.blockSize;t2.call(this,t,e,i,r),this._prevBlock=t.slice(e,e+i)}}),p.Decryptor=p.extend({processBlock:function(t,e){var r=this._cipher,i=r.blockSize,o=t.slice(e,e+i);t2.call(this,t,e,i,r),this._prevBlock=o}}),p),i.mode.CTR=(r=i.lib.BlockCipherMode.extend(),w=r.Encryptor=r.extend({processBlock:function(t,e){var r=this._cipher,i=r.blockSize,o=this._iv,n=this._counter,s=(o&&(n=this._counter=o.slice(0),this._iv=void 0),n.slice(0));r.encryptBlock(s,0),n[i-1]=n[i-1]+1|0;for(var c=0;c>>2]|=e<<24-r%4*8,t.sigBytes+=e},unpad:function(t){var e=255&t.words[t.sigBytes-1>>>2];t.sigBytes-=e}},i.pad.Iso10126={pad:function(t,e){e*=4,e-=t.sigBytes%e;t.concat(i.lib.WordArray.random(e-1)).concat(i.lib.WordArray.create([e<<24],1))},unpad:function(t){var e=255&t.words[t.sigBytes-1>>>2];t.sigBytes-=e}},i.pad.Iso97971={pad:function(t,e){t.concat(i.lib.WordArray.create([2147483648],1)),i.pad.ZeroPadding.pad(t,e)},unpad:function(t){i.pad.ZeroPadding.unpad(t),t.sigBytes--}},i.pad.ZeroPadding={pad:function(t,e){e*=4;t.clamp(),t.sigBytes+=e-(t.sigBytes%e||e)},unpad:function(t){for(var e=t.words,r=t.sigBytes-1,r=t.sigBytes-1;0<=r;r--)if(e[r>>>2]>>>24-r%4*8&255){t.sigBytes=r+1;break}}},i.pad.NoPadding={pad:function(){},unpad:function(){}},d1=(P=i).lib.CipherParams,u1=P.enc.Hex,P.format.Hex={stringify:function(t){return t.ciphertext.toString(u1)},parse:function(t){t=u1.parse(t);return d1.create({ciphertext:t})}};for(var w=i,p=w.lib.BlockCipher,u=w.algo,k=[],r2=[],i2=[],o2=[],n2=[],s2=[],c2=[],a2=[],h2=[],l2=[],x=[],b=0;b<256;b++)x[b]=b<128?b<<1:b<<1^283;for(var m=0,S=0,b=0;b<256;b++){var E=S^S<<1^S<<2^S<<3^S<<4,f2=(k[m]=E=E>>>8^255&E^99,x[r2[E]=m]),d2=x[f2],u2=x[d2],M=257*x[E]^16843008*E;i2[m]=M<<24|M>>>8,o2[m]=M<<16|M>>>16,n2[m]=M<<8|M>>>24,s2[m]=M,c2[E]=(M=16843009*u2^65537*d2^257*f2^16843008*m)<<24|M>>>8,a2[E]=M<<16|M>>>16,h2[E]=M<<8|M>>>24,l2[E]=M,m?(m=f2^x[x[x[u2^f2]]],S^=x[x[S]]):m=S=1}var p2=[0,1,2,4,8,16,32,64,128,27,54],u=u.AES=p.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var t=this._keyPriorReset=this._key,e=t.words,r=t.sigBytes/4,i=4*(1+(this._nRounds=6+r)),o=this._keySchedule=[],n=0;n>>24]<<24|k[a>>>16&255]<<16|k[a>>>8&255]<<8|k[255&a]):(a=k[(a=a<<8|a>>>24)>>>24]<<24|k[a>>>16&255]<<16|k[a>>>8&255]<<8|k[255&a],a^=p2[n/r|0]<<24),o[n]=o[n-r]^a);for(var s=this._invKeySchedule=[],c=0;c>>24]]^a2[k[a>>>16&255]]^h2[k[a>>>8&255]]^l2[k[255&a]]}}},encryptBlock:function(t,e){this._doCryptBlock(t,e,this._keySchedule,i2,o2,n2,s2,k)},decryptBlock:function(t,e){var r=t[e+1],r=(t[e+1]=t[e+3],t[e+3]=r,this._doCryptBlock(t,e,this._invKeySchedule,c2,a2,h2,l2,r2),t[e+1]);t[e+1]=t[e+3],t[e+3]=r},_doCryptBlock:function(t,e,r,i,o,n,s,c){for(var a=this._nRounds,h=t[e]^r[0],l=t[e+1]^r[1],f=t[e+2]^r[2],d=t[e+3]^r[3],u=4,p=1;p>>24]^o[l>>>16&255]^n[f>>>8&255]^s[255&d]^r[u++],y=i[l>>>24]^o[f>>>16&255]^n[d>>>8&255]^s[255&h]^r[u++],g=i[f>>>24]^o[d>>>16&255]^n[h>>>8&255]^s[255&l]^r[u++],v=i[d>>>24]^o[h>>>16&255]^n[l>>>8&255]^s[255&f]^r[u++],h=_,l=y,f=g,d=v;_=(c[h>>>24]<<24|c[l>>>16&255]<<16|c[f>>>8&255]<<8|c[255&d])^r[u++],y=(c[l>>>24]<<24|c[f>>>16&255]<<16|c[d>>>8&255]<<8|c[255&h])^r[u++],g=(c[f>>>24]<<24|c[d>>>16&255]<<16|c[h>>>8&255]<<8|c[255&l])^r[u++],v=(c[d>>>24]<<24|c[h>>>16&255]<<16|c[l>>>8&255]<<8|c[255&f])^r[u++];t[e]=_,t[e+1]=y,t[e+2]=g,t[e+3]=v},keySize:8}),P=(w.AES=p._createHelper(u),i),_2=(w=P.lib).WordArray,w=w.BlockCipher,p=P.algo,y2=[57,49,41,33,25,17,9,1,58,50,42,34,26,18,10,2,59,51,43,35,27,19,11,3,60,52,44,36,63,55,47,39,31,23,15,7,62,54,46,38,30,22,14,6,61,53,45,37,29,21,13,5,28,20,12,4],g2=[14,17,11,24,1,5,3,28,15,6,21,10,23,19,12,4,26,8,16,7,27,20,13,2,41,52,31,37,47,55,30,40,51,45,33,48,44,49,39,56,34,53,46,42,50,36,29,32],v2=[1,2,4,6,8,10,12,14,15,17,19,21,23,25,27,28],B2=[{0:8421888,268435456:32768,536870912:8421378,805306368:2,1073741824:512,1342177280:8421890,1610612736:8389122,1879048192:8388608,2147483648:514,2415919104:8389120,2684354560:33280,2952790016:8421376,3221225472:32770,3489660928:8388610,3758096384:0,4026531840:33282,134217728:0,402653184:8421890,671088640:33282,939524096:32768,1207959552:8421888,1476395008:512,1744830464:8421378,2013265920:2,2281701376:8389120,2550136832:33280,2818572288:8421376,3087007744:8389122,3355443200:8388610,3623878656:32770,3892314112:514,4160749568:8388608,1:32768,268435457:2,536870913:8421888,805306369:8388608,1073741825:8421378,1342177281:33280,1610612737:512,1879048193:8389122,2147483649:8421890,2415919105:8421376,2684354561:8388610,2952790017:33282,3221225473:514,3489660929:8389120,3758096385:32770,4026531841:0,134217729:8421890,402653185:8421376,671088641:8388608,939524097:512,1207959553:32768,1476395009:8388610,1744830465:2,2013265921:33282,2281701377:32770,2550136833:8389122,2818572289:514,3087007745:8421888,3355443201:8389120,3623878657:0,3892314113:33280,4160749569:8421378},{0:1074282512,16777216:16384,33554432:524288,50331648:1074266128,67108864:1073741840,83886080:1074282496,100663296:1073758208,117440512:16,134217728:540672,150994944:1073758224,167772160:1073741824,184549376:540688,201326592:524304,218103808:0,234881024:16400,251658240:1074266112,8388608:1073758208,25165824:540688,41943040:16,58720256:1073758224,75497472:1074282512,92274688:1073741824,109051904:524288,125829120:1074266128,142606336:524304,159383552:0,176160768:16384,192937984:1074266112,209715200:1073741840,226492416:540672,243269632:1074282496,260046848:16400,268435456:0,285212672:1074266128,301989888:1073758224,318767104:1074282496,335544320:1074266112,352321536:16,369098752:540688,385875968:16384,402653184:16400,419430400:524288,436207616:524304,452984832:1073741840,469762048:540672,486539264:1073758208,503316480:1073741824,520093696:1074282512,276824064:540688,293601280:524288,310378496:1074266112,327155712:16384,343932928:1073758208,360710144:1074282512,377487360:16,394264576:1073741824,411041792:1074282496,427819008:1073741840,444596224:1073758224,461373440:524304,478150656:0,494927872:16400,511705088:1074266128,528482304:540672},{0:260,1048576:0,2097152:67109120,3145728:65796,4194304:65540,5242880:67108868,6291456:67174660,7340032:67174400,8388608:67108864,9437184:67174656,10485760:65792,11534336:67174404,12582912:67109124,13631488:65536,14680064:4,15728640:256,524288:67174656,1572864:67174404,2621440:0,3670016:67109120,4718592:67108868,5767168:65536,6815744:65540,7864320:260,8912896:4,9961472:256,11010048:67174400,12058624:65796,13107200:65792,14155776:67109124,15204352:67174660,16252928:67108864,16777216:67174656,17825792:65540,18874368:65536,19922944:67109120,20971520:256,22020096:67174660,23068672:67108868,24117248:0,25165824:67109124,26214400:67108864,27262976:4,28311552:65792,29360128:67174400,30408704:260,31457280:65796,32505856:67174404,17301504:67108864,18350080:260,19398656:67174656,20447232:0,21495808:65540,22544384:67109120,23592960:256,24641536:67174404,25690112:65536,26738688:67174660,27787264:65796,28835840:67108868,29884416:67109124,30932992:67174400,31981568:4,33030144:65792},{0:2151682048,65536:2147487808,131072:4198464,196608:2151677952,262144:0,327680:4198400,393216:2147483712,458752:4194368,524288:2147483648,589824:4194304,655360:64,720896:2147487744,786432:2151678016,851968:4160,917504:4096,983040:2151682112,32768:2147487808,98304:64,163840:2151678016,229376:2147487744,294912:4198400,360448:2151682112,425984:0,491520:2151677952,557056:4096,622592:2151682048,688128:4194304,753664:4160,819200:2147483648,884736:4194368,950272:4198464,1015808:2147483712,1048576:4194368,1114112:4198400,1179648:2147483712,1245184:0,1310720:4160,1376256:2151678016,1441792:2151682048,1507328:2147487808,1572864:2151682112,1638400:2147483648,1703936:2151677952,1769472:4198464,1835008:2147487744,1900544:4194304,1966080:64,2031616:4096,1081344:2151677952,1146880:2151682112,1212416:0,1277952:4198400,1343488:4194368,1409024:2147483648,1474560:2147487808,1540096:64,1605632:2147483712,1671168:4096,1736704:2147487744,1802240:2151678016,1867776:4160,1933312:2151682048,1998848:4194304,2064384:4198464},{0:128,4096:17039360,8192:262144,12288:536870912,16384:537133184,20480:16777344,24576:553648256,28672:262272,32768:16777216,36864:537133056,40960:536871040,45056:553910400,49152:553910272,53248:0,57344:17039488,61440:553648128,2048:17039488,6144:553648256,10240:128,14336:17039360,18432:262144,22528:537133184,26624:553910272,30720:536870912,34816:537133056,38912:0,43008:553910400,47104:16777344,51200:536871040,55296:553648128,59392:16777216,63488:262272,65536:262144,69632:128,73728:536870912,77824:553648256,81920:16777344,86016:553910272,90112:537133184,94208:16777216,98304:553910400,102400:553648128,106496:17039360,110592:537133056,114688:262272,118784:536871040,122880:0,126976:17039488,67584:553648256,71680:16777216,75776:17039360,79872:537133184,83968:536870912,88064:17039488,92160:128,96256:553910272,100352:262272,104448:553910400,108544:0,112640:553648128,116736:16777344,120832:262144,124928:537133056,129024:536871040},{0:268435464,256:8192,512:270532608,768:270540808,1024:268443648,1280:2097152,1536:2097160,1792:268435456,2048:0,2304:268443656,2560:2105344,2816:8,3072:270532616,3328:2105352,3584:8200,3840:270540800,128:270532608,384:270540808,640:8,896:2097152,1152:2105352,1408:268435464,1664:268443648,1920:8200,2176:2097160,2432:8192,2688:268443656,2944:270532616,3200:0,3456:270540800,3712:2105344,3968:268435456,4096:268443648,4352:270532616,4608:270540808,4864:8200,5120:2097152,5376:268435456,5632:268435464,5888:2105344,6144:2105352,6400:0,6656:8,6912:270532608,7168:8192,7424:268443656,7680:270540800,7936:2097160,4224:8,4480:2105344,4736:2097152,4992:268435464,5248:268443648,5504:8200,5760:270540808,6016:270532608,6272:270540800,6528:270532616,6784:8192,7040:2105352,7296:2097160,7552:0,7808:268435456,8064:268443656},{0:1048576,16:33555457,32:1024,48:1049601,64:34604033,80:0,96:1,112:34603009,128:33555456,144:1048577,160:33554433,176:34604032,192:34603008,208:1025,224:1049600,240:33554432,8:34603009,24:0,40:33555457,56:34604032,72:1048576,88:33554433,104:33554432,120:1025,136:1049601,152:33555456,168:34603008,184:1048577,200:1024,216:34604033,232:1,248:1049600,256:33554432,272:1048576,288:33555457,304:34603009,320:1048577,336:33555456,352:34604032,368:1049601,384:1025,400:34604033,416:1049600,432:1,448:0,464:34603008,480:33554433,496:1024,264:1049600,280:33555457,296:34603009,312:1,328:33554432,344:1048576,360:1025,376:34604032,392:33554433,408:34603008,424:0,440:34604033,456:1049601,472:1024,488:33555456,504:1048577},{0:134219808,1:131072,2:134217728,3:32,4:131104,5:134350880,6:134350848,7:2048,8:134348800,9:134219776,10:133120,11:134348832,12:2080,13:0,14:134217760,15:133152,2147483648:2048,2147483649:134350880,2147483650:134219808,2147483651:134217728,2147483652:134348800,2147483653:133120,2147483654:133152,2147483655:32,2147483656:134217760,2147483657:2080,2147483658:131104,2147483659:134350848,2147483660:0,2147483661:134348832,2147483662:134219776,2147483663:131072,16:133152,17:134350848,18:32,19:2048,20:134219776,21:134217760,22:134348832,23:131072,24:0,25:131104,26:134348800,27:134219808,28:134350880,29:133120,30:2080,31:134217728,2147483664:131072,2147483665:2048,2147483666:134348832,2147483667:133152,2147483668:32,2147483669:134348800,2147483670:134217728,2147483671:134219808,2147483672:134350880,2147483673:134217760,2147483674:134219776,2147483675:0,2147483676:133120,2147483677:2080,2147483678:131104,2147483679:134350848}],w2=[4160749569,528482304,33030144,2064384,129024,8064,504,2147483679],k2=p.DES=w.extend({_doReset:function(){for(var t=this._key.words,e=[],r=0;r<56;r++){var i=y2[r]-1;e[r]=t[i>>>5]>>>31-i%32&1}for(var o=this._subKeys=[],n=0;n<16;n++){for(var s=o[n]=[],c=v2[n],r=0;r<24;r++)s[r/6|0]|=e[(g2[r]-1+c)%28]<<31-r%6,s[4+(r/6|0)]|=e[28+(g2[r+24]-1+c)%28]<<31-r%6;s[0]=s[0]<<1|s[0]>>>31;for(r=1;r<7;r++)s[r]=s[r]>>>4*(r-1)+3;s[7]=s[7]<<5|s[7]>>>27}for(var a=this._invSubKeys=[],r=0;r<16;r++)a[r]=o[15-r]},encryptBlock:function(t,e){this._doCryptBlock(t,e,this._subKeys)},decryptBlock:function(t,e){this._doCryptBlock(t,e,this._invSubKeys)},_doCryptBlock:function(t,e,r){this._lBlock=t[e],this._rBlock=t[e+1],x2.call(this,4,252645135),x2.call(this,16,65535),b2.call(this,2,858993459),b2.call(this,8,16711935),x2.call(this,1,1431655765);for(var i=0;i<16;i++){for(var o=r[i],n=this._lBlock,s=this._rBlock,c=0,a=0;a<8;a++)c|=B2[a][((s^o[a])&w2[a])>>>0];this._lBlock=s,this._rBlock=n^c}var h=this._lBlock;this._lBlock=this._rBlock,this._rBlock=h,x2.call(this,1,1431655765),b2.call(this,8,16711935),b2.call(this,2,858993459),x2.call(this,16,65535),x2.call(this,4,252645135),t[e]=this._lBlock,t[e+1]=this._rBlock},keySize:2,ivSize:2,blockSize:2});function x2(t,e){e=(this._lBlock>>>t^this._rBlock)&e;this._rBlock^=e,this._lBlock^=e<>>t^this._lBlock)&e;this._lBlock^=e,this._rBlock^=e<192.");var e=t.slice(0,2),r=t.length<4?t.slice(0,2):t.slice(2,4),t=t.length<6?t.slice(0,2):t.slice(4,6);this._des1=k2.createEncryptor(_2.create(e)),this._des2=k2.createEncryptor(_2.create(r)),this._des3=k2.createEncryptor(_2.create(t))},encryptBlock:function(t,e){this._des1.encryptBlock(t,e),this._des2.decryptBlock(t,e),this._des3.encryptBlock(t,e)},decryptBlock:function(t,e){this._des3.decryptBlock(t,e),this._des2.encryptBlock(t,e),this._des1.decryptBlock(t,e)},keySize:6,ivSize:2,blockSize:2}),P.TripleDES=w._createHelper(p);var u=i,P=u.lib.StreamCipher,w=u.algo,m2=w.RC4=P.extend({_doReset:function(){for(var t=this._key,e=t.words,r=t.sigBytes,i=this._S=[],o=0;o<256;o++)i[o]=o;for(var o=0,n=0;o<256;o++){var s=o%r,s=e[s>>>2]>>>24-s%4*8&255,n=(n+i[o]+s)%256,s=i[o];i[o]=i[n],i[n]=s}this._i=this._j=0},_doProcessBlock:function(t,e){t[e]^=S2.call(this)},keySize:8,ivSize:0});function S2(){for(var t=this._S,e=this._i,r=this._j,i=0,o=0;o<4;o++){var r=(r+t[e=(e+1)%256])%256,n=t[e];t[e]=t[r],t[r]=n,i|=t[(t[e]+t[r])%256]<<24-8*o}return this._i=e,this._j=r,i}function A2(){for(var t=this._X,e=this._C,r=0;r<8;r++)c[r]=e[r];e[0]=e[0]+1295307597+this._b|0,e[1]=e[1]+3545052371+(e[0]>>>0>>0?1:0)|0,e[2]=e[2]+886263092+(e[1]>>>0>>0?1:0)|0,e[3]=e[3]+1295307597+(e[2]>>>0>>0?1:0)|0,e[4]=e[4]+3545052371+(e[3]>>>0>>0?1:0)|0,e[5]=e[5]+886263092+(e[4]>>>0>>0?1:0)|0,e[6]=e[6]+1295307597+(e[5]>>>0>>0?1:0)|0,e[7]=e[7]+3545052371+(e[6]>>>0>>0?1:0)|0,this._b=e[7]>>>0>>0?1:0;for(r=0;r<8;r++){var i=t[r]+e[r],o=65535&i,n=i>>>16;a[r]=((o*o>>>17)+o*n>>>15)+n*n^((4294901760&i)*i|0)+((65535&i)*i|0)}t[0]=a[0]+(a[7]<<16|a[7]>>>16)+(a[6]<<16|a[6]>>>16)|0,t[1]=a[1]+(a[0]<<8|a[0]>>>24)+a[7]|0,t[2]=a[2]+(a[1]<<16|a[1]>>>16)+(a[0]<<16|a[0]>>>16)|0,t[3]=a[3]+(a[2]<<8|a[2]>>>24)+a[1]|0,t[4]=a[4]+(a[3]<<16|a[3]>>>16)+(a[2]<<16|a[2]>>>16)|0,t[5]=a[5]+(a[4]<<8|a[4]>>>24)+a[3]|0,t[6]=a[6]+(a[5]<<16|a[5]>>>16)+(a[4]<<16|a[4]>>>16)|0,t[7]=a[7]+(a[6]<<8|a[6]>>>24)+a[5]|0}function z2(){for(var t=this._X,e=this._C,r=0;r<8;r++)f[r]=e[r];e[0]=e[0]+1295307597+this._b|0,e[1]=e[1]+3545052371+(e[0]>>>0>>0?1:0)|0,e[2]=e[2]+886263092+(e[1]>>>0>>0?1:0)|0,e[3]=e[3]+1295307597+(e[2]>>>0>>0?1:0)|0,e[4]=e[4]+3545052371+(e[3]>>>0>>0?1:0)|0,e[5]=e[5]+886263092+(e[4]>>>0>>0?1:0)|0,e[6]=e[6]+1295307597+(e[5]>>>0>>0?1:0)|0,e[7]=e[7]+3545052371+(e[6]>>>0>>0?1:0)|0,this._b=e[7]>>>0>>0?1:0;for(r=0;r<8;r++){var i=t[r]+e[r],o=65535&i,n=i>>>16;d[r]=((o*o>>>17)+o*n>>>15)+n*n^((4294901760&i)*i|0)+((65535&i)*i|0)}t[0]=d[0]+(d[7]<<16|d[7]>>>16)+(d[6]<<16|d[6]>>>16)|0,t[1]=d[1]+(d[0]<<8|d[0]>>>24)+d[7]|0,t[2]=d[2]+(d[1]<<16|d[1]>>>16)+(d[0]<<16|d[0]>>>16)|0,t[3]=d[3]+(d[2]<<8|d[2]>>>24)+d[1]|0,t[4]=d[4]+(d[3]<<16|d[3]>>>16)+(d[2]<<16|d[2]>>>16)|0,t[5]=d[5]+(d[4]<<8|d[4]>>>24)+d[3]|0,t[6]=d[6]+(d[5]<<16|d[5]>>>16)+(d[4]<<16|d[4]>>>16)|0,t[7]=d[7]+(d[6]<<8|d[6]>>>24)+d[5]|0}u.RC4=P._createHelper(m2),w=w.RC4Drop=m2.extend({cfg:m2.cfg.extend({drop:192}),_doReset:function(){m2._doReset.call(this);for(var t=this.cfg.drop;0>>24)|4278255360&(t[r]<<24|t[r]>>>8);for(var i=this._X=[t[0],t[3]<<16|t[2]>>>16,t[1],t[0]<<16|t[3]>>>16,t[2],t[1]<<16|t[0]>>>16,t[3],t[2]<<16|t[1]>>>16],o=this._C=[t[2]<<16|t[2]>>>16,4294901760&t[0]|65535&t[1],t[3]<<16|t[3]>>>16,4294901760&t[1]|65535&t[2],t[0]<<16|t[0]>>>16,4294901760&t[2]|65535&t[3],t[1]<<16|t[1]>>>16,4294901760&t[3]|65535&t[0]],r=this._b=0;r<4;r++)A2.call(this);for(r=0;r<8;r++)o[r]^=i[r+4&7];if(e){var e=e.words,n=e[0],e=e[1],n=16711935&(n<<8|n>>>24)|4278255360&(n<<24|n>>>8),e=16711935&(e<<8|e>>>24)|4278255360&(e<<24|e>>>8),s=n>>>16|4294901760&e,c=e<<16|65535&n;o[0]^=n,o[1]^=s,o[2]^=e,o[3]^=c,o[4]^=n,o[5]^=s,o[6]^=e,o[7]^=c;for(r=0;r<4;r++)A2.call(this)}},_doProcessBlock:function(t,e){var r=this._X;A2.call(this),n[0]=r[0]^r[5]>>>16^r[3]<<16,n[1]=r[2]^r[7]>>>16^r[5]<<16,n[2]=r[4]^r[1]>>>16^r[7]<<16,n[3]=r[6]^r[3]>>>16^r[1]<<16;for(var i=0;i<4;i++)n[i]=16711935&(n[i]<<8|n[i]>>>24)|4278255360&(n[i]<<24|n[i]>>>8),t[e+i]^=n[i]},blockSize:4,ivSize:2}),p.Rabbit=u._createHelper(P),p=(w=i).lib.StreamCipher,u=w.algo,h=[],f=[],d=[],u=u.RabbitLegacy=p.extend({_doReset:function(){for(var t=this._key.words,e=this.cfg.iv,r=this._X=[t[0],t[3]<<16|t[2]>>>16,t[1],t[0]<<16|t[3]>>>16,t[2],t[1]<<16|t[0]>>>16,t[3],t[2]<<16|t[1]>>>16],i=this._C=[t[2]<<16|t[2]>>>16,4294901760&t[0]|65535&t[1],t[3]<<16|t[3]>>>16,4294901760&t[1]|65535&t[2],t[0]<<16|t[0]>>>16,4294901760&t[2]|65535&t[3],t[1]<<16|t[1]>>>16,4294901760&t[3]|65535&t[0]],o=this._b=0;o<4;o++)z2.call(this);for(o=0;o<8;o++)i[o]^=r[o+4&7];if(e){var t=e.words,e=t[0],t=t[1],e=16711935&(e<<8|e>>>24)|4278255360&(e<<24|e>>>8),t=16711935&(t<<8|t>>>24)|4278255360&(t<<24|t>>>8),n=e>>>16|4294901760&t,s=t<<16|65535&e;i[0]^=e,i[1]^=n,i[2]^=t,i[3]^=s,i[4]^=e,i[5]^=n,i[6]^=t,i[7]^=s;for(o=0;o<4;o++)z2.call(this)}},_doProcessBlock:function(t,e){var r=this._X;z2.call(this),h[0]=r[0]^r[5]>>>16^r[3]<<16,h[1]=r[2]^r[7]>>>16^r[5]<<16,h[2]=r[4]^r[1]>>>16^r[7]<<16,h[3]=r[6]^r[3]>>>16^r[1]<<16;for(var i=0;i<4;i++)h[i]=16711935&(h[i]<<8|h[i]>>>24)|4278255360&(h[i]<<24|h[i]>>>8),t[e+i]^=h[i]},blockSize:4,ivSize:2}),w.RabbitLegacy=p._createHelper(u);{w=(P=i).lib.BlockCipher,p=P.algo;const F=16,D2=[608135816,2242054355,320440878,57701188,2752067618,698298832,137296536,3964562569,1160258022,953160567,3193202383,887688300,3232508343,3380367581,1065670069,3041331479,2450970073,2306472731],E2=[[3509652390,2564797868,805139163,3491422135,3101798381,1780907670,3128725573,4046225305,614570311,3012652279,134345442,2240740374,1667834072,1901547113,2757295779,4103290238,227898511,1921955416,1904987480,2182433518,2069144605,3260701109,2620446009,720527379,3318853667,677414384,3393288472,3101374703,2390351024,1614419982,1822297739,2954791486,3608508353,3174124327,2024746970,1432378464,3864339955,2857741204,1464375394,1676153920,1439316330,715854006,3033291828,289532110,2706671279,2087905683,3018724369,1668267050,732546397,1947742710,3462151702,2609353502,2950085171,1814351708,2050118529,680887927,999245976,1800124847,3300911131,1713906067,1641548236,4213287313,1216130144,1575780402,4018429277,3917837745,3693486850,3949271944,596196993,3549867205,258830323,2213823033,772490370,2760122372,1774776394,2652871518,566650946,4142492826,1728879713,2882767088,1783734482,3629395816,2517608232,2874225571,1861159788,326777828,3124490320,2130389656,2716951837,967770486,1724537150,2185432712,2364442137,1164943284,2105845187,998989502,3765401048,2244026483,1075463327,1455516326,1322494562,910128902,469688178,1117454909,936433444,3490320968,3675253459,1240580251,122909385,2157517691,634681816,4142456567,3825094682,3061402683,2540495037,79693498,3249098678,1084186820,1583128258,426386531,1761308591,1047286709,322548459,995290223,1845252383,2603652396,3431023940,2942221577,3202600964,3727903485,1712269319,422464435,3234572375,1170764815,3523960633,3117677531,1434042557,442511882,3600875718,1076654713,1738483198,4213154764,2393238008,3677496056,1014306527,4251020053,793779912,2902807211,842905082,4246964064,1395751752,1040244610,2656851899,3396308128,445077038,3742853595,3577915638,679411651,2892444358,2354009459,1767581616,3150600392,3791627101,3102740896,284835224,4246832056,1258075500,768725851,2589189241,3069724005,3532540348,1274779536,3789419226,2764799539,1660621633,3471099624,4011903706,913787905,3497959166,737222580,2514213453,2928710040,3937242737,1804850592,3499020752,2949064160,2386320175,2390070455,2415321851,4061277028,2290661394,2416832540,1336762016,1754252060,3520065937,3014181293,791618072,3188594551,3933548030,2332172193,3852520463,3043980520,413987798,3465142937,3030929376,4245938359,2093235073,3534596313,375366246,2157278981,2479649556,555357303,3870105701,2008414854,3344188149,4221384143,3956125452,2067696032,3594591187,2921233993,2428461,544322398,577241275,1471733935,610547355,4027169054,1432588573,1507829418,2025931657,3646575487,545086370,48609733,2200306550,1653985193,298326376,1316178497,3007786442,2064951626,458293330,2589141269,3591329599,3164325604,727753846,2179363840,146436021,1461446943,4069977195,705550613,3059967265,3887724982,4281599278,3313849956,1404054877,2845806497,146425753,1854211946],[1266315497,3048417604,3681880366,3289982499,290971e4,1235738493,2632868024,2414719590,3970600049,1771706367,1449415276,3266420449,422970021,1963543593,2690192192,3826793022,1062508698,1531092325,1804592342,2583117782,2714934279,4024971509,1294809318,4028980673,1289560198,2221992742,1669523910,35572830,157838143,1052438473,1016535060,1802137761,1753167236,1386275462,3080475397,2857371447,1040679964,2145300060,2390574316,1461121720,2956646967,4031777805,4028374788,33600511,2920084762,1018524850,629373528,3691585981,3515945977,2091462646,2486323059,586499841,988145025,935516892,3367335476,2599673255,2839830854,265290510,3972581182,2759138881,3795373465,1005194799,847297441,406762289,1314163512,1332590856,1866599683,4127851711,750260880,613907577,1450815602,3165620655,3734664991,3650291728,3012275730,3704569646,1427272223,778793252,1343938022,2676280711,2052605720,1946737175,3164576444,3914038668,3967478842,3682934266,1661551462,3294938066,4011595847,840292616,3712170807,616741398,312560963,711312465,1351876610,322626781,1910503582,271666773,2175563734,1594956187,70604529,3617834859,1007753275,1495573769,4069517037,2549218298,2663038764,504708206,2263041392,3941167025,2249088522,1514023603,1998579484,1312622330,694541497,2582060303,2151582166,1382467621,776784248,2618340202,3323268794,2497899128,2784771155,503983604,4076293799,907881277,423175695,432175456,1378068232,4145222326,3954048622,3938656102,3820766613,2793130115,2977904593,26017576,3274890735,3194772133,1700274565,1756076034,4006520079,3677328699,720338349,1533947780,354530856,688349552,3973924725,1637815568,332179504,3949051286,53804574,2852348879,3044236432,1282449977,3583942155,3416972820,4006381244,1617046695,2628476075,3002303598,1686838959,431878346,2686675385,1700445008,1080580658,1009431731,832498133,3223435511,2605976345,2271191193,2516031870,1648197032,4164389018,2548247927,300782431,375919233,238389289,3353747414,2531188641,2019080857,1475708069,455242339,2609103871,448939670,3451063019,1395535956,2413381860,1841049896,1491858159,885456874,4264095073,4001119347,1565136089,3898914787,1108368660,540939232,1173283510,2745871338,3681308437,4207628240,3343053890,4016749493,1699691293,1103962373,3625875870,2256883143,3830138730,1031889488,3479347698,1535977030,4236805024,3251091107,2132092099,1774941330,1199868427,1452454533,157007616,2904115357,342012276,595725824,1480756522,206960106,497939518,591360097,863170706,2375253569,3596610801,1814182875,2094937945,3421402208,1082520231,3463918190,2785509508,435703966,3908032597,1641649973,2842273706,3305899714,1510255612,2148256476,2655287854,3276092548,4258621189,236887753,3681803219,274041037,1734335097,3815195456,3317970021,1899903192,1026095262,4050517792,356393447,2410691914,3873677099,3682840055],[3913112168,2491498743,4132185628,2489919796,1091903735,1979897079,3170134830,3567386728,3557303409,857797738,1136121015,1342202287,507115054,2535736646,337727348,3213592640,1301675037,2528481711,1895095763,1721773893,3216771564,62756741,2142006736,835421444,2531993523,1442658625,3659876326,2882144922,676362277,1392781812,170690266,3921047035,1759253602,3611846912,1745797284,664899054,1329594018,3901205900,3045908486,2062866102,2865634940,3543621612,3464012697,1080764994,553557557,3656615353,3996768171,991055499,499776247,1265440854,648242737,3940784050,980351604,3713745714,1749149687,3396870395,4211799374,3640570775,1161844396,3125318951,1431517754,545492359,4268468663,3499529547,1437099964,2702547544,3433638243,2581715763,2787789398,1060185593,1593081372,2418618748,4260947970,69676912,2159744348,86519011,2512459080,3838209314,1220612927,3339683548,133810670,1090789135,1078426020,1569222167,845107691,3583754449,4072456591,1091646820,628848692,1613405280,3757631651,526609435,236106946,48312990,2942717905,3402727701,1797494240,859738849,992217954,4005476642,2243076622,3870952857,3732016268,765654824,3490871365,2511836413,1685915746,3888969200,1414112111,2273134842,3281911079,4080962846,172450625,2569994100,980381355,4109958455,2819808352,2716589560,2568741196,3681446669,3329971472,1835478071,660984891,3704678404,4045999559,3422617507,3040415634,1762651403,1719377915,3470491036,2693910283,3642056355,3138596744,1364962596,2073328063,1983633131,926494387,3423689081,2150032023,4096667949,1749200295,3328846651,309677260,2016342300,1779581495,3079819751,111262694,1274766160,443224088,298511866,1025883608,3806446537,1145181785,168956806,3641502830,3584813610,1689216846,3666258015,3200248200,1692713982,2646376535,4042768518,1618508792,1610833997,3523052358,4130873264,2001055236,3610705100,2202168115,4028541809,2961195399,1006657119,2006996926,3186142756,1430667929,3210227297,1314452623,4074634658,4101304120,2273951170,1399257539,3367210612,3027628629,1190975929,2062231137,2333990788,2221543033,2438960610,1181637006,548689776,2362791313,3372408396,3104550113,3145860560,296247880,1970579870,3078560182,3769228297,1714227617,3291629107,3898220290,166772364,1251581989,493813264,448347421,195405023,2709975567,677966185,3703036547,1463355134,2715995803,1338867538,1343315457,2802222074,2684532164,233230375,2599980071,2000651841,3277868038,1638401717,4028070440,3237316320,6314154,819756386,300326615,590932579,1405279636,3267499572,3150704214,2428286686,3959192993,3461946742,1862657033,1266418056,963775037,2089974820,2263052895,1917689273,448879540,3550394620,3981727096,150775221,3627908307,1303187396,508620638,2975983352,2726630617,1817252668,1876281319,1457606340,908771278,3720792119,3617206836,2455994898,1729034894,1080033504],[976866871,3556439503,2881648439,1522871579,1555064734,1336096578,3548522304,2579274686,3574697629,3205460757,3593280638,3338716283,3079412587,564236357,2993598910,1781952180,1464380207,3163844217,3332601554,1699332808,1393555694,1183702653,3581086237,1288719814,691649499,2847557200,2895455976,3193889540,2717570544,1781354906,1676643554,2592534050,3230253752,1126444790,2770207658,2633158820,2210423226,2615765581,2414155088,3127139286,673620729,2805611233,1269405062,4015350505,3341807571,4149409754,1057255273,2012875353,2162469141,2276492801,2601117357,993977747,3918593370,2654263191,753973209,36408145,2530585658,25011837,3520020182,2088578344,530523599,2918365339,1524020338,1518925132,3760827505,3759777254,1202760957,3985898139,3906192525,674977740,4174734889,2031300136,2019492241,3983892565,4153806404,3822280332,352677332,2297720250,60907813,90501309,3286998549,1016092578,2535922412,2839152426,457141659,509813237,4120667899,652014361,1966332200,2975202805,55981186,2327461051,676427537,3255491064,2882294119,3433927263,1307055953,942726286,933058658,2468411793,3933900994,4215176142,1361170020,2001714738,2830558078,3274259782,1222529897,1679025792,2729314320,3714953764,1770335741,151462246,3013232138,1682292957,1483529935,471910574,1539241949,458788160,3436315007,1807016891,3718408830,978976581,1043663428,3165965781,1927990952,4200891579,2372276910,3208408903,3533431907,1412390302,2931980059,4132332400,1947078029,3881505623,4168226417,2941484381,1077988104,1320477388,886195818,18198404,3786409e3,2509781533,112762804,3463356488,1866414978,891333506,18488651,661792760,1628790961,3885187036,3141171499,876946877,2693282273,1372485963,791857591,2686433993,3759982718,3167212022,3472953795,2716379847,445679433,3561995674,3504004811,3574258232,54117162,3331405415,2381918588,3769707343,4154350007,1140177722,4074052095,668550556,3214352940,367459370,261225585,2610173221,4209349473,3468074219,3265815641,314222801,3066103646,3808782860,282218597,3406013506,3773591054,379116347,1285071038,846784868,2669647154,3771962079,3550491691,2305946142,453669953,1268987020,3317592352,3279303384,3744833421,2610507566,3859509063,266596637,3847019092,517658769,3462560207,3443424879,370717030,4247526661,2224018117,4143653529,4112773975,2788324899,2477274417,1456262402,2901442914,1517677493,1846949527,2295493580,3734397586,2176403920,1280348187,1908823572,3871786941,846861322,1172426758,3287448474,3383383037,1655181056,3139813346,901632758,1897031941,2986607138,3066810236,3447102507,1393639104,373351379,950779232,625454576,3124240540,4148612726,2007998917,544563296,2244738638,2330496472,2058025392,1291430526,424198748,50039436,29584100,3605783033,2429876329,2791104160,1057563949,3255363231,3075367218,3463963227,1469046755,985887462]];var H2={pbox:[],sbox:[]};function C2(t,e){var r=t.sbox[0][e>>24&255]+t.sbox[1][e>>16&255];return r=(r^=t.sbox[2][e>>8&255])+t.sbox[3][255&e]}function R2(e,t,r){let i=t,o=r,n;for(let t=0;t=a&&(e=0);let r=0,i=0,o=0;for(let t=0;t= 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 + }; diff --git a/javascript/migu.js b/javascript/migu.js new file mode 100644 index 0000000..3ac86d3 --- /dev/null +++ b/javascript/migu.js @@ -0,0 +1,143 @@ +/** + * Function to get the ddCalcu parameter value + * @param {string} inputUrl - The original URL before encryption + * @returns {Promise} - 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); +}); \ No newline at end of file diff --git a/javascript/taobao-sign.js b/javascript/taobao-sign.js new file mode 100644 index 0000000..b130db5 --- /dev/null +++ b/javascript/taobao-sign.js @@ -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) \ No newline at end of file diff --git a/javascript/x-bogus.js b/javascript/x-bogus.js new file mode 100644 index 0000000..5cf2bde --- /dev/null +++ b/javascript/x-bogus.js @@ -0,0 +1,564 @@ +var window = null; + +function _0x5cd844(e) { + var b = { + exports: {} + }; + return e(b, b.exports), b.exports +} +jsvmp = function(e, b, a) { + function f(e, b, a) { + return (f = function() { + if ("undefined" == typeof Reflect || !Reflect.construct || Reflect.construct.sham) return !1; + if ("function" == typeof Proxy) return !0; + try { + return Date.prototype.toString.call(Reflect.construct(Date, [], function() {})), !0 + } catch (e) { + return !1 + } + }() ? Reflect.construct : function(e, b, a) { + var f = [null]; + f.push.apply(f, b); + var c = new(Function.bind.apply(e, f)); + return a && function(e, b) { + (Object.setPrototypeOf || function(e, b) { + return e.__proto__ = b, e + })(e, b) + }(c, a.prototype), c + }).apply(null, arguments) + } + + function c(e) { + return function(e) { + if (Array.isArray(e)) { + for (var b = 0, a = new Array(e.length); b < e.length; b++) a[b] = e[b]; + return a + } + }(e) || function(e) { + if (Symbol.iterator in Object(e) || "[object Arguments]" === Object.prototype.toString.call(e)) return Array.from(e) + }(e) || function() { + throw new TypeError("Invalid attempt to spread non-iterable instance") + }() + } + for (var r = [], t = 0, d = [], i = 0, n = function(e, b) { + var a = e[b++], + f = e[b], + c = parseInt("" + a + f, 16); + if (c >> 7 == 0) return [1, c]; + if (c >> 6 == 2) { + var r = parseInt("" + e[++b] + e[++b], 16); + return c &= 63, [2, r = (c <<= 8) + r] + } + if (c >> 6 == 3) { + var t = parseInt("" + e[++b] + e[++b], 16), + d = parseInt("" + e[++b] + e[++b], 16); + return c &= 63, [3, d = (c <<= 16) + (t <<= 8) + d] + } + }, s = function(e, b) { + var a = parseInt("" + e[b] + e[b + 1], 16); + return a > 127 ? -256 + a : a + }, o = function(e, b) { + var a = parseInt("" + e[b] + e[b + 1] + e[b + 2] + e[b + 3], 16); + return a > 32767 ? -65536 + a : a + }, l = function(e, b) { + var a = parseInt("" + e[b] + e[b + 1] + e[b + 2] + e[b + 3] + e[b + 4] + e[b + 5] + e[b + 6] + e[b + 7], 16); + return a > 2147483647 ? 0 + a : a + }, _ = function(e, b) { + return parseInt("" + e[b] + e[b + 1], 16) + }, x = function(e, b) { + return parseInt("" + e[b] + e[b + 1] + e[b + 2] + e[b + 3], 16) + }, u = u || this || window, h = (e.length, 0), p = "", y = h; y < h + 16; y++) { + var v = "" + e[y++] + e[y]; + v = parseInt(v, 16), p += String.fromCharCode(v) + } + if ("HNOJ@?RC" != p) throw new Error("error magic number " + p); + parseInt("" + e[h += 16] + e[h + 1], 16), h += 8, t = 0; + for (var g = 0; g < 4; g++) { + var w = h + 2 * g, + A = parseInt("" + e[w++] + e[w], 16); + t += (3 & A) << 2 * g + } + h += 16; + var C = parseInt("" + e[h += 8] + e[h + 1] + e[h + 2] + e[h + 3] + e[h + 4] + e[h + 5] + e[h + 6] + e[h + 7], 16), + m = C, + S = h += 8, + z = x(e, h += C); + z[1], h += 4, r = { + p: [], + q: [] + }; + for (var B = 0; B < z; B++) { + for (var R = n(e, h), q = h += 2 * R[0], I = r.p.length, k = 0; k < R[1]; k++) { + var j = n(e, q); + r.p.push(j[1]), q += 2 * j[0] + } + h = q, r.q.push([I, r.p.length]) + } + var O = { + 5: 1, + 6: 1, + 70: 1, + 22: 1, + 23: 1, + 37: 1, + 73: 1 + }, + U = { + 72: 1 + }, + D = { + 74: 1 + }, + N = { + 11: 1, + 12: 1, + 24: 1, + 26: 1, + 27: 1, + 31: 1 + }, + J = { + 10: 1 + }, + L = { + 2: 1, + 29: 1, + 30: 1, + 20: 1 + }, + T = [], + E = []; + + function M(e, b, a) { + for (var f = b; f < b + a;) { + var c = _(e, f); + T[f] = c, f += 2, U[c] ? (E[f] = s(e, f), f += 2) : O[c] ? (E[f] = o(e, f), f += 4) : D[c] ? (E[f] = l(e, f), f += 8) : N[c] ? (E[f] = _(e, f), f += 2) : J[c] ? (E[f] = x(e, f), f += 4) : L[c] && (E[f] = x(e, f), f += 4) + } + } + return F(e, S, m / 2, [], b, a); + + function P(e, b, a, n, h, p, y, v) { + null == p && (p = this); + var g, w, A, C, m = [], + S = 0; + y && (w = y); + var z, B, R = b, + q = R + 2 * a; + if (!v) + for (; R < q;) { + var I = parseInt("" + e[R] + e[R + 1], 16); + R += 2; + var j = 3 & (z = 13 * I % 241); + if (z >>= 2, j < 1) + if (j = 3 & z, z >>= 2, j < 1) { + if ((j = z) < 1) return [1, m[S--]]; + j < 5 ? (w = m[S--], m[S] = m[S] * w) : j < 7 ? (w = m[S--], m[S] = m[S] != w) : j < 14 ? (A = m[S--], C = m[S--], (j = m[S--]).x === P ? j.y >= 1 ? m[++S] = F(e, j.c, j.l, A, j.z, C, null, 1) : (m[++S] = F(e, j.c, j.l, A, j.z, C, null, 0), j.y++) : m[++S] = j.apply(C, A)) : j < 16 && (B = o(e, R), (g = function b() { + var a = arguments; + return b.y > 0 || b.y++, F(e, b.c, b.l, a, b.z, this, null, 0) + }).c = R + 4, g.l = B - 2, g.x = P, g.y = 0, g.z = h, m[S] = g, R += 2 * B - 2) + } else if (j < 2)(j = z) > 8 ? (w = m[S--], m[S] = typeof w) : j > 4 ? m[S -= 1] = m[S][m[S + 1]] : j > 2 && (A = m[S--], (j = m[S]).x === P ? j.y >= 1 ? m[S] = F(e, j.c, j.l, [A], j.z, C, null, 1) : (m[S] = F(e, j.c, j.l, [A], j.z, C, null, 0), j.y++) : m[S] = j(A)); + else if (j < 3) { + if ((j = z) < 9) { + for (w = m[S--], B = x(e, R), j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]); + R += 4, m[S--][j] = w + } else if (j < 13) throw m[S--] + } else(j = z) < 1 ? m[++S] = null : j < 3 ? (w = m[S--], m[S] = m[S] >= w) : j < 12 && (m[++S] = void 0); + else if (j < 2) + if (j = 3 & z, z >>= 2, j < 1) + if ((j = z) < 5) { + B = o(e, R); + try { + if (d[i][2] = 1, 1 == (w = P(e, R + 4, B - 3, [], h, p, null, 0))[0]) return w + } catch (b) { + if (d[i] && d[i][1] && 1 == (w = P(e, d[i][1][0], d[i][1][1], [], h, p, b, 0))[0]) return w + } finally { + if (d[i] && d[i][0] && 1 == (w = P(e, d[i][0][0], d[i][0][1], [], h, p, null, 0))[0]) return w; + d[i] = 0, i-- + } + R += 2 * B - 2 + } else j < 7 ? (B = _(e, R), R += 2, m[S -= B] = 0 === B ? new m[S] : f(m[S], c(m.slice(S + 1, S + B + 1)))) : j < 9 && (w = m[S--], m[S] = m[S] & w); + else if (j < 2) + if ((j = z) > 12) m[++S] = s(e, R), R += 2; + else if (j > 10) w = m[S--], m[S] = m[S] << w; + else if (j > 8) { + for (B = x(e, R), j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]); + R += 4, m[S] = m[S][j] + } else j > 6 && (A = m[S--], w = delete m[S--][A]); + else if (j < 3)(j = z) < 2 ? m[++S] = w : j < 11 ? (w = m[S -= 2][m[S + 1]] = m[S + 2], S--) : j < 13 && (w = m[S], m[++S] = w); + else if ((j = z) > 12) m[++S] = p; + else if (j > 5) w = m[S--], m[S] = m[S] !== w; + else if (j > 3) w = m[S--], m[S] = m[S] / w; + else if (j > 1) { + if ((B = o(e, R)) < 0) { + v = 1, M(e, b, 2 * a), R += 2 * B - 2; + break + } + R += 2 * B - 2 + } else j > -1 && (m[S] = !m[S]); + else if (j < 3) + if (j = 3 & z, z >>= 2, j < 1)(j = z) > 13 ? (m[++S] = o(e, R), R += 4) : j > 11 ? (w = m[S--], m[S] = m[S] >> w) : j > 9 ? (B = _(e, R), R += 2, w = m[S--], h[B] = w) : j > 7 ? (B = x(e, R), R += 4, A = S + 1, m[S -= B - 1] = B ? m.slice(S, A) : []) : j > 0 && (w = m[S--], m[S] = m[S] > w); + else if (j < 2)(j = z) > 12 ? (w = m[S - 1], A = m[S], m[++S] = w, m[++S] = A) : j > 3 ? (w = m[S--], m[S] = m[S] == w) : j > 1 ? (w = m[S--], m[S] = m[S] + w) : j > -1 && (m[++S] = u); + else if (j < 3) { + if ((j = z) > 13) m[++S] = !1; + else if (j > 6) w = m[S--], m[S] = m[S] instanceof w; + else if (j > 4) w = m[S--], m[S] = m[S] % w; + else if (j > 2) + if (m[S--]) R += 4; + else { + if ((B = o(e, R)) < 0) { + v = 1, M(e, b, 2 * a), R += 2 * B - 2; + break + } + R += 2 * B - 2 + } + else if (j > 0) { + for (B = x(e, R), w = "", k = r.q[B][0]; k < r.q[B][1]; k++) w += String.fromCharCode(t ^ r.p[k]); + m[++S] = w, R += 4 + } + } else(j = z) > 7 ? (w = m[S--], m[S] = m[S] | w) : j > 5 ? (B = _(e, R), R += 2, m[++S] = h["$" + B]) : j > 3 && (B = o(e, R), d[i][0] && !d[i][2] ? d[i][1] = [R + 4, B - 3] : d[i++] = [0, [R + 4, B - 3], 0], R += 2 * B - 2); + else if (j = 3 & z, z >>= 2, j > 2)(j = z) > 13 ? (m[++S] = l(e, R), R += 8) : j > 11 ? (w = m[S--], m[S] = m[S] >>> w) : j > 9 ? m[++S] = !0 : j > 7 ? (B = _(e, R), R += 2, m[S] = m[S][B]) : j > 0 && (w = m[S--], m[S] = m[S] < w); + else if (j > 1)(j = z) > 10 ? (B = o(e, R), d[++i] = [ + [R + 4, B - 3], 0, 0 + ], R += 2 * B - 2) : j > 8 ? (w = m[S--], m[S] = m[S] ^ w) : j > 6 && (w = m[S--]); + else if (j > 0) { + if ((j = z) > 7) w = m[S--], m[S] = m[S] in w; + else if (j > 5) m[S] = ++m[S]; + else if (j > 3) B = _(e, R), R += 2, w = h[B], m[++S] = w; + else if (j > 1) { + var O = 0, + U = m[S].length, + D = m[S]; + m[++S] = function() { + var e = O < U; + if (e) { + var b = D[O++]; + m[++S] = b + } + m[++S] = e + } + } + } else if ((j = z) > 13) w = m[S], m[S] = m[S - 1], m[S - 1] = w; + else if (j > 4) w = m[S--], m[S] = m[S] === w; + else if (j > 2) w = m[S--], m[S] = m[S] - w; + else if (j > 0) { + for (B = x(e, R), j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]); + j = +j, R += 4, m[++S] = j + } + } + if (v) + for (; R < q;) + if (I = T[R], R += 2, j = 3 & (z = 13 * I % 241), z >>= 2, j > 2) + if (j = 3 & z, z >>= 2, j > 2)(j = z) < 2 ? (w = m[S--], m[S] = m[S] < w) : j < 9 ? (B = E[R], R += 2, m[S] = m[S][B]) : j < 11 ? m[++S] = !0 : j < 13 ? (w = m[S--], m[S] = m[S] >>> w) : j < 15 && (m[++S] = E[R], R += 8); + else if (j > 1)(j = z) < 6 || (j < 8 ? w = m[S--] : j < 10 ? (w = m[S--], m[S] = m[S] ^ w) : j < 12 && (B = E[R], d[++i] = [ + [R + 4, B - 3], 0, 0 + ], R += 2 * B - 2)); + else if (j > 0)(j = z) > 7 ? (w = m[S--], m[S] = m[S] in w) : j > 5 ? m[S] = ++m[S] : j > 3 ? (B = E[R], R += 2, w = h[B], m[++S] = w) : j > 1 && (O = 0, U = m[S].length, D = m[S], m[++S] = function() { + var e = O < U; + if (e) { + var b = D[O++]; + m[++S] = b + } + m[++S] = e + }); + else if ((j = z) < 2) { + for (B = E[R], j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]); + j = +j, R += 4, m[++S] = j + } else j < 4 ? (w = m[S--], m[S] = m[S] - w) : j < 6 ? (w = m[S--], m[S] = m[S] === w) : j < 15 && (w = m[S], m[S] = m[S - 1], m[S - 1] = w); + else if (j > 1) + if (j = 3 & z, z >>= 2, j < 1)(j = z) > 13 ? (m[++S] = E[R], R += 4) : j > 11 ? (w = m[S--], m[S] = m[S] >> w) : j > 9 ? (B = E[R], R += 2, w = m[S--], h[B] = w) : j > 7 ? (B = E[R], R += 4, A = S + 1, m[S -= B - 1] = B ? m.slice(S, A) : []) : j > 0 && (w = m[S--], m[S] = m[S] > w); + else if (j < 2)(j = z) < 1 ? m[++S] = u : j < 3 ? (w = m[S--], m[S] = m[S] + w) : j < 5 ? (w = m[S--], m[S] = m[S] == w) : j < 14 && (w = m[S - 1], A = m[S], m[++S] = w, m[++S] = A); + else if (j < 3) { + if ((j = z) > 13) m[++S] = !1; + else if (j > 6) w = m[S--], m[S] = m[S] instanceof w; + else if (j > 4) w = m[S--], m[S] = m[S] % w; + else if (j > 2) m[S--] ? R += 4 : R += 2 * (B = E[R]) - 2; + else if (j > 0) { + for (B = E[R], w = "", k = r.q[B][0]; k < r.q[B][1]; k++) w += String.fromCharCode(t ^ r.p[k]); + m[++S] = w, R += 4 + } + } else(j = z) > 7 ? (w = m[S--], m[S] = m[S] | w) : j > 5 ? (B = E[R], R += 2, m[++S] = h["$" + B]) : j > 3 && (B = E[R], d[i][0] && !d[i][2] ? d[i][1] = [R + 4, B - 3] : d[i++] = [0, [R + 4, B - 3], 0], R += 2 * B - 2); + else if (j > 0) + if (j = 3 & z, z >>= 2, j < 1) { + if ((j = z) > 9); + else if (j > 7) w = m[S--], m[S] = m[S] & w; + else if (j > 5) B = E[R], R += 2, m[S -= B] = 0 === B ? new m[S] : f(m[S], c(m.slice(S + 1, S + B + 1))); + else if (j > 3) { + B = E[R]; + try { + if (d[i][2] = 1, 1 == (w = P(e, R + 4, B - 3, [], h, p, null, 0))[0]) return w + } catch (b) { + if (d[i] && d[i][1] && 1 == (w = P(e, d[i][1][0], d[i][1][1], [], h, p, b, 0))[0]) return w + } finally { + if (d[i] && d[i][0] && 1 == (w = P(e, d[i][0][0], d[i][0][1], [], h, p, null, 0))[0]) return w; + d[i] = 0, i-- + } + R += 2 * B - 2 + } + } else if (j < 2) + if ((j = z) < 8) A = m[S--], w = delete m[S--][A]; + else if (j < 10) { + for (B = E[R], j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]); + R += 4, m[S] = m[S][j] + } else j < 12 ? (w = m[S--], m[S] = m[S] << w) : j < 14 && (m[++S] = E[R], R += 2); + else j < 3 ? (j = z) < 2 ? m[++S] = w : j < 11 ? (w = m[S -= 2][m[S + 1]] = m[S + 2], S--) : j < 13 && (w = m[S], m[++S] = w) : (j = z) > 12 ? m[++S] = p : j > 5 ? (w = m[S--], m[S] = m[S] !== w) : j > 3 ? (w = m[S--], m[S] = m[S] / w) : j > 1 ? R += 2 * (B = E[R]) - 2 : j > -1 && (m[S] = !m[S]); + else if (j = 3 & z, z >>= 2, j < 1) { + if ((j = z) < 1) return [1, m[S--]]; + j < 5 ? (w = m[S--], m[S] = m[S] * w) : j < 7 ? (w = m[S--], m[S] = m[S] != w) : j < 14 ? (A = m[S--], C = m[S--], (j = m[S--]).x === P ? j.y >= 1 ? m[++S] = F(e, j.c, j.l, A, j.z, C, null, 1) : (m[++S] = F(e, j.c, j.l, A, j.z, C, null, 0), j.y++) : m[++S] = j.apply(C, A)) : j < 16 && (B = E[R], (g = function b() { + var a = arguments; + return b.y > 0 || b.y++, F(e, b.c, b.l, a, b.z, this, null, 0) + }).c = R + 4, g.l = B - 2, g.x = P, g.y = 0, g.z = h, m[S] = g, R += 2 * B - 2) + } else if (j < 2)(j = z) > 8 ? (w = m[S--], m[S] = typeof w) : j > 4 ? m[S -= 1] = m[S][m[S + 1]] : j > 2 && (A = m[S--], (j = m[S]).x === P ? j.y >= 1 ? m[S] = F(e, j.c, j.l, [A], j.z, C, null, 1) : (m[S] = F(e, j.c, j.l, [A], j.z, C, null, 0), j.y++) : m[S] = j(A)); + else if (j < 3) { + if ((j = z) < 9) { + for (w = m[S--], B = E[R], j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]); + R += 4, m[S--][j] = w + } else if (j < 13) throw m[S--] + } else(j = z) < 1 ? m[++S] = null : j < 3 ? (w = m[S--], m[S] = m[S] >= w) : j < 12 && (m[++S] = void 0); + return [0, null] + } + + function F(e, b, a, f, c, r, t, d) { + null == r && (r = this), c && !c.d && (c.d = 0, c.$0 = c, c[1] = {}); + var i, n, s = {}, + o = s.d = c ? c.d + 1 : 0; + for (s["$" + o] = s, n = 0; n < o; n++) s[i = "$" + n] = c[i]; + for (n = 0, o = s.length = f.length; n < o; n++) s[n] = f[n]; + return d && !T[b] && M(e, b, 2 * a), T[b] ? P(e, b, a, 0, s, r, null, 1)[1] : P(e, b, a, 0, s, r, null, 0)[1] + } +}; +var _0x397dc7 = "undefined" != typeof globalThis ? globalThis : void 0 !== window ? window : "undefined" != typeof global ? global : "undefined" != typeof self ? self : {}, + _0x124d1a = _0x5cd844(function(_0x770f81) { + ! function() { + var _0x250d36 = "input is invalid type", + _0x4cfaee = !1, + _0x1702f9 = {}, + _0x5ccbb3 = !_0x4cfaee && "object" == typeof self, + _0x54d876 = !_0x1702f9.JS_MD5_NO_NODE_JS && "object" == typeof process && process.versions && process.versions.node, + _0x185caf; + _0x54d876 ? _0x1702f9 = _0x397dc7 : _0x5ccbb3 && (_0x1702f9 = self); + var _0x17dcbf = !_0x1702f9.JS_MD5_NO_COMMON_JS && _0x770f81.exports, + _0x554fed = !1, + _0x2de28f = !_0x1702f9.JS_MD5_NO_ARRAY_BUFFER && "undefined" != typeof ArrayBuffer, + _0x3a9a1b = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"], + _0x465562 = [128, 32768, 8388608, -2147483648], + _0x20b37e = [0, 8, 16, 24], + _0x323604 = ["hex", "array", "digest", "buffer", "arrayBuffer", "base64"], + _0x2c185e = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"], + _0x4b59e0 = []; + if (_0x2de28f) { + var _0x395837 = new ArrayBuffer(68); + _0x185caf = new Uint8Array(_0x395837), _0x4b59e0 = new Uint32Array(_0x395837) + }!_0x1702f9.JS_MD5_NO_NODE_JS && Array.isArray || (Array.isArray = function(e) { + return "[object Array]" === Object.prototype.toString.call(e) + }), _0x2de28f && (_0x1702f9.JS_MD5_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView) && (ArrayBuffer.isView = function(e) { + return "object" == typeof e && e.buffer && e.buffer.constructor === ArrayBuffer + }); + var _0x4e9930 = function(e) { + return function(b) { + return new _0x5887c8(!0).update(b)[e]() + } + }, + _0x38ba77 = function() { + var e = _0x4e9930("hex"); + _0x54d876 && (e = _0x474989(e)), e.create = function() { + return new _0x5887c8 + }, e.update = function(b) { + return e.create().update(b) + }; + for (var b = 0; b < _0x323604.length; ++b) { + var a = _0x323604[b]; + e[a] = _0x4e9930(a) + } + return e + }, + _0x474989 = function(_0x57eeaa) { + var _0x114910, _0x226465 = eval("require('crypto');"), + _0x1f6ae0 = eval("require('buffer')['Buffer'];"); + return function(e) { + if ("string" == typeof e) return _0x226465.createHash("md5").update(e, "utf8").digest("hex"); + if (null == e) throw _0x250d36; + return e.constructor === ArrayBuffer && (e = new Uint8Array(e)), Array.isArray(e) || ArrayBuffer.isView(e) || e.constructor === _0x1f6ae0 ? _0x226465.createHash("md5").update(new _0x1f6ae0.from(e)).digest("hex") : _0x57eeaa(e) + } + }; + + function _0x5887c8(e) { + if (e) _0x4b59e0[0] = _0x4b59e0[16] = _0x4b59e0[1] = _0x4b59e0[2] = _0x4b59e0[3] = _0x4b59e0[4] = _0x4b59e0[5] = _0x4b59e0[6] = _0x4b59e0[7] = _0x4b59e0[8] = _0x4b59e0[9] = _0x4b59e0[10] = _0x4b59e0[11] = _0x4b59e0[12] = _0x4b59e0[13] = _0x4b59e0[14] = _0x4b59e0[15] = 0, this.blocks = _0x4b59e0, this.buffer8 = _0x185caf; + else if (_0x2de28f) { + var b = new ArrayBuffer(68); + this.buffer8 = new Uint8Array(b), this.blocks = new Uint32Array(b) + } else this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + this.h0 = this.h1 = this.h2 = this.h3 = this.start = this.bytes = this.hBytes = 0, this.finalized = this.hashed = !1, this.first = !0 + } + _0x5887c8.prototype.update = function(e) { + if (!this.finalized) { + var b, a = typeof e; + if ("string" !== a) { + if ("object" !== a || null === e) throw _0x250d36; + if (_0x2de28f && e.constructor === ArrayBuffer) e = new Uint8Array(e); + else if (!(Array.isArray(e) || _0x2de28f && ArrayBuffer.isView(e))) throw _0x250d36; + b = !0 + } + for (var f, c, r = 0, t = e.length, d = this.blocks, i = this.buffer8; r < t;) { + if (this.hashed && (this.hashed = !1, d[0] = d[16], d[16] = d[1] = d[2] = d[3] = d[4] = d[5] = d[6] = d[7] = d[8] = d[9] = d[10] = d[11] = d[12] = d[13] = d[14] = d[15] = 0), b) + if (_0x2de28f) + for (c = this.start; r < t && c < 64; ++r) i[c++] = e[r]; + else + for (c = this.start; r < t && c < 64; ++r) d[c >> 2] |= e[r] << _0x20b37e[3 & c++]; + else if (_0x2de28f) + for (c = this.start; r < t && c < 64; ++r)(f = e.charCodeAt(r)) < 128 ? i[c++] = f : f < 2048 ? (i[c++] = 192 | f >> 6, i[c++] = 128 | 63 & f) : f < 55296 || f >= 57344 ? (i[c++] = 224 | f >> 12, i[c++] = 128 | f >> 6 & 63, i[c++] = 128 | 63 & f) : (f = 65536 + ((1023 & f) << 10 | 1023 & e.charCodeAt(++r)), i[c++] = 240 | f >> 18, i[c++] = 128 | f >> 12 & 63, i[c++] = 128 | f >> 6 & 63, i[c++] = 128 | 63 & f); + else + for (c = this.start; r < t && c < 64; ++r)(f = e.charCodeAt(r)) < 128 ? d[c >> 2] |= f << _0x20b37e[3 & c++] : f < 2048 ? (d[c >> 2] |= (192 | f >> 6) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]) : f < 55296 || f >= 57344 ? (d[c >> 2] |= (224 | f >> 12) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 6 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]) : (f = 65536 + ((1023 & f) << 10 | 1023 & e.charCodeAt(++r)), d[c >> 2] |= (240 | f >> 18) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 12 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 6 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]); + this.lastByteIndex = c, this.bytes += c - this.start, c >= 64 ? (this.start = c - 64, this.hash(), this.hashed = !0) : this.start = c + } + return this.bytes > 4294967295 && (this.hBytes += this.bytes / 4294967296 << 0, this.bytes = this.bytes % 4294967296), this + } + }, _0x5887c8.prototype.finalize = function() { + if (!this.finalized) { + this.finalized = !0; + var e = this.blocks, + b = this.lastByteIndex; + e[b >> 2] |= _0x465562[3 & b], b >= 56 && (this.hashed || this.hash(), e[0] = e[16], e[16] = e[1] = e[2] = e[3] = e[4] = e[5] = e[6] = e[7] = e[8] = e[9] = e[10] = e[11] = e[12] = e[13] = e[14] = e[15] = 0), e[14] = this.bytes << 3, e[15] = this.hBytes << 3 | this.bytes >>> 29, this.hash() + } + }, _0x5887c8.prototype.hash = function() { + var e, b, a, f, c, r, t = this.blocks; + this.first ? b = ((b = ((e = ((e = t[0] - 680876937) << 7 | e >>> 25) - 271733879 << 0) ^ (a = ((a = (-271733879 ^ (f = ((f = (-1732584194 ^ 2004318071 & e) + t[1] - 117830708) << 12 | f >>> 20) + e << 0) & (-271733879 ^ e)) + t[2] - 1126478375) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[3] - 1316259209) << 22 | b >>> 10) + a << 0 : (e = this.h0, b = this.h1, a = this.h2, b = ((b += ((e = ((e += ((f = this.h3) ^ b & (a ^ f)) + t[0] - 680876936) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[1] - 389564586) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[2] + 606105819) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[3] - 1044525330) << 22 | b >>> 10) + a << 0), b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[4] - 176418897) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[5] + 1200080426) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[6] - 1473231341) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[7] - 45705983) << 22 | b >>> 10) + a << 0, b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[8] + 1770035416) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[9] - 1958414417) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[10] - 42063) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[11] - 1990404162) << 22 | b >>> 10) + a << 0, b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[12] + 1804603682) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[13] - 40341101) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[14] - 1502002290) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[15] + 1236535329) << 22 | b >>> 10) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[1] - 165796510) << 5 | e >>> 27) + b << 0) ^ b)) + t[6] - 1069501632) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[11] + 643717713) << 14 | a >>> 18) + f << 0) ^ f)) + t[0] - 373897302) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[5] - 701558691) << 5 | e >>> 27) + b << 0) ^ b)) + t[10] + 38016083) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[15] - 660478335) << 14 | a >>> 18) + f << 0) ^ f)) + t[4] - 405537848) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[9] + 568446438) << 5 | e >>> 27) + b << 0) ^ b)) + t[14] - 1019803690) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[3] - 187363961) << 14 | a >>> 18) + f << 0) ^ f)) + t[8] + 1163531501) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[13] - 1444681467) << 5 | e >>> 27) + b << 0) ^ b)) + t[2] - 51403784) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[7] + 1735328473) << 14 | a >>> 18) + f << 0) ^ f)) + t[12] - 1926607734) << 20 | b >>> 12) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[5] - 378558) << 4 | e >>> 28) + b << 0)) + t[8] - 2022574463) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[11] + 1839030562) << 16 | a >>> 16) + f << 0)) + t[14] - 35309556) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[1] - 1530992060) << 4 | e >>> 28) + b << 0)) + t[4] + 1272893353) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[7] - 155497632) << 16 | a >>> 16) + f << 0)) + t[10] - 1094730640) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[13] + 681279174) << 4 | e >>> 28) + b << 0)) + t[0] - 358537222) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[3] - 722521979) << 16 | a >>> 16) + f << 0)) + t[6] + 76029189) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[9] - 640364487) << 4 | e >>> 28) + b << 0)) + t[12] - 421815835) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[15] + 530742520) << 16 | a >>> 16) + f << 0)) + t[2] - 995338651) << 23 | b >>> 9) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[0] - 198630844) << 6 | e >>> 26) + b << 0) | ~a)) + t[7] + 1126891415) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[14] - 1416354905) << 15 | a >>> 17) + f << 0) | ~e)) + t[5] - 57434055) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[12] + 1700485571) << 6 | e >>> 26) + b << 0) | ~a)) + t[3] - 1894986606) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[10] - 1051523) << 15 | a >>> 17) + f << 0) | ~e)) + t[1] - 2054922799) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[8] + 1873313359) << 6 | e >>> 26) + b << 0) | ~a)) + t[15] - 30611744) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[6] - 1560198380) << 15 | a >>> 17) + f << 0) | ~e)) + t[13] + 1309151649) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[4] - 145523070) << 6 | e >>> 26) + b << 0) | ~a)) + t[11] - 1120210379) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[2] + 718787259) << 15 | a >>> 17) + f << 0) | ~e)) + t[9] - 343485551) << 21 | b >>> 11) + a << 0, this.first ? (this.h0 = e + 1732584193 << 0, this.h1 = b - 271733879 << 0, this.h2 = a - 1732584194 << 0, this.h3 = f + 271733878 << 0, this.first = !1) : (this.h0 = this.h0 + e << 0, this.h1 = this.h1 + b << 0, this.h2 = this.h2 + a << 0, this.h3 = this.h3 + f << 0) + }, _0x5887c8.prototype.hex = function() { + this.finalize(); + var e = this.h0, + b = this.h1, + a = this.h2, + f = this.h3; + return _0x3a9a1b[e >> 4 & 15] + _0x3a9a1b[15 & e] + _0x3a9a1b[e >> 12 & 15] + _0x3a9a1b[e >> 8 & 15] + _0x3a9a1b[e >> 20 & 15] + _0x3a9a1b[e >> 16 & 15] + _0x3a9a1b[e >> 28 & 15] + _0x3a9a1b[e >> 24 & 15] + _0x3a9a1b[b >> 4 & 15] + _0x3a9a1b[15 & b] + _0x3a9a1b[b >> 12 & 15] + _0x3a9a1b[b >> 8 & 15] + _0x3a9a1b[b >> 20 & 15] + _0x3a9a1b[b >> 16 & 15] + _0x3a9a1b[b >> 28 & 15] + _0x3a9a1b[b >> 24 & 15] + _0x3a9a1b[a >> 4 & 15] + _0x3a9a1b[15 & a] + _0x3a9a1b[a >> 12 & 15] + _0x3a9a1b[a >> 8 & 15] + _0x3a9a1b[a >> 20 & 15] + _0x3a9a1b[a >> 16 & 15] + _0x3a9a1b[a >> 28 & 15] + _0x3a9a1b[a >> 24 & 15] + _0x3a9a1b[f >> 4 & 15] + _0x3a9a1b[15 & f] + _0x3a9a1b[f >> 12 & 15] + _0x3a9a1b[f >> 8 & 15] + _0x3a9a1b[f >> 20 & 15] + _0x3a9a1b[f >> 16 & 15] + _0x3a9a1b[f >> 28 & 15] + _0x3a9a1b[f >> 24 & 15] + }, _0x5887c8.prototype.toString = _0x5887c8.prototype.hex, _0x5887c8.prototype.digest = function() { + this.finalize(); + var e = this.h0, + b = this.h1, + a = this.h2, + f = this.h3; + return [255 & e, e >> 8 & 255, e >> 16 & 255, e >> 24 & 255, 255 & b, b >> 8 & 255, b >> 16 & 255, b >> 24 & 255, 255 & a, a >> 8 & 255, a >> 16 & 255, a >> 24 & 255, 255 & f, f >> 8 & 255, f >> 16 & 255, f >> 24 & 255] + }, _0x5887c8.prototype.array = _0x5887c8.prototype.digest, _0x5887c8.prototype.arrayBuffer = function() { + this.finalize(); + var e = new ArrayBuffer(16), + b = new Uint32Array(e); + return b[0] = this.h0, b[1] = this.h1, b[2] = this.h2, b[3] = this.h3, e + }, _0x5887c8.prototype.buffer = _0x5887c8.prototype.arrayBuffer, _0x5887c8.prototype.base64 = function() { + for (var e, b, a, f = "", c = this.array(), r = 0; r < 15;) e = c[r++], b = c[r++], a = c[r++], f += _0x2c185e[e >>> 2] + _0x2c185e[63 & (e << 4 | b >>> 4)] + _0x2c185e[63 & (b << 2 | a >>> 6)] + _0x2c185e[63 & a]; + return f + (_0x2c185e[(e = c[r]) >>> 2] + _0x2c185e[e << 4 & 63] + "==") + }; + var _0x4dd781 = _0x38ba77(); + _0x17dcbf ? _0x770f81.exports = _0x4dd781 : (_0x1702f9.md5 = _0x4dd781, _0x554fed && (void 0)(function() { + return _0x4dd781 + })) + }() + }); + +function _0x178cef(e) { + return jsvmp("484e4f4a403f52430038001eab0015840e8ee21a00000000000000621b000200001d000146000306000e271f001b000200021d00010500121b001b000b021b000b04041d0001071b000b0500000003000126207575757575757575757575757575757575757575757575757575757575757575", [, , void 0 !== _0x124d1a ? _0x124d1a : void 0, _0x178cef, e]) +} +for (var _0xb55f3e = { + boe: !1, + aid: 0, + dfp: !1, + sdi: !1, + enablePathList: [], + _enablePathListRegex: [], + urlRewriteRules: [], + _urlRewriteRules: [], + initialized: !1, + enableTrack: !1, + track: { + unitTime: 0, + unitAmount: 0, + fre: 0 + }, + triggerUnload: !1, + region: "", + regionConf: {}, + umode: 0, + v: !1, + perf: !1, + xxbg: !0 + }, _0x3eaf64 = { + debug: function(e, b) { + let a = !1; + a = !1 + } + }, _0x233455 = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"], _0x2e9f6d = [], _0x511f86 = [], _0x3d35de = 0; _0x3d35de < 256; _0x3d35de++) _0x2e9f6d[_0x3d35de] = _0x233455[_0x3d35de >> 4 & 15] + _0x233455[15 & _0x3d35de], _0x3d35de < 16 && (_0x3d35de < 10 ? _0x511f86[48 + _0x3d35de] = _0x3d35de : _0x511f86[87 + _0x3d35de] = _0x3d35de); +var _0x2ce54d = function(e) { + for (var b = e.length, a = "", f = 0; f < b;) a += _0x2e9f6d[e[f++]]; + return a + }, + _0x5960a2 = function(e) { + for (var b = e.length >> 1, a = b << 1, f = new Uint8Array(b), c = 0, r = 0; r < a;) f[c++] = _0x511f86[e.charCodeAt(r++)] << 4 | _0x511f86[e.charCodeAt(r++)]; + return f + }, + _0x4e46b6 = { + encode: _0x2ce54d, + decode: _0x5960a2 + }; + +function sign(e, b) { + return jsvmp("484e4f4a403f5243001f240fbf2031ccf317480300000000000007181b0002012e1d00921b000b171b000b02402217000a1c1b000b1726402217000c1c1b000b170200004017002646000306000e271f001b000200021d00920500121b001b000b031b000b17041d0092071b000b041e012f17000d1b000b05260a0000101c1b000b06260a0000101c1b001b000b071e01301d00931b001b000b081e00081d00941b0048021d00951b001b000b1b1d00961b0048401d009e1b001b000b031b000b16041d009f1b001b000b09221e0131241b000b031b000b09221e0131241b000b1e0a000110040a0001101d00d51b001b000b09221e0131241b000b031b000b09221e0131241b000b180a000110040a0001101d00d71b001b000b0a1e00101d00d91b001b000b0b261b000b1a1b000b190a0002101d00db1b001b000b0c261b000b221b000b210a0002101d00dc1b001b000b0d261b000b230200200a0002101d00dd1b001b000b09221e0131241b000b031b000b24040a0001101d00df1b001b000b0e1a00221e00de240a0000104903e82b1d00e31b001b000b0f260a0000101d00e41b001b000b1d1d00e71b001b000b1a4901002b1d00e81b001b000b1a4901002c1d00ea1b001b000b191d00f21b001b000b1f480e191d00f81b001b000b1f480f191d00f91b001b000b20480e191d00fb1b001b000b20480f191d00fe1b001b000b25480e191d01001b001b000b25480f191d01011b001b000b264818344900ff2f1d01031b001b000b264810344900ff2f1d01321b001b000b264808344900ff2f1d01331b001b000b264800344900ff2f1d01341b001b000b274818344900ff2f1d01351b001b000b274810344900ff2f1d01361b001b000b274808344900ff2f1d01371b001b000b274800344900ff2f1d01381b001b000b281b000b29311b000b2a311b000b2b311b000b2c311b000b2d311b000b2e311b000b2f311b000b30311b000b31311b000b32311b000b33311b000b34311b000b35311b000b36311b000b37311b000b38311b000b39311d01391b004900ff1d013a1b001b000b10261b000b281b000b2a1b000b2c1b000b2e1b000b301b000b321b000b341b000b361b000b381b000b3a1b000b291b000b2b1b000b2d1b000b2f1b000b311b000b331b000b351b000b371b000b390a0013101d013b1b001b000b0c261b000b111b000b3b041b000b3c0a0002101d013c1b001b000b12261b000b1c1b000b3b1b000b3d0a0003101d013d1b001b000b13261b000b3e0200240a0002101d013e1b000b3f0000013f000126207575757575757575757575757575757575757575757575757575757575757575012b0e7776757a7d7643617c637661676a027a77065c717976706708777671667474766107767d65707c77760374766707707c7d607c7f7607757a61767166740a7c66677661447a77677b0a7a7d7d7661447a77677b0b7c666776615b767a747b670b7a7d7d76615b767a747b6709666076615274767d670b677c5f7c64766150726076077a7d77766b5c7508767f767067617c7d09667d7776757a7d76770963617c677c676a637608677c4067617a7d740470727f7f0763617c7076606010487c71797670673363617c707660604e067c717976706705677a677f76047d7c7776012e0125012402602341525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a383c2e0260224157787763747b2749586042512b233c5e75656420254b5a22412126384446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e0260214157787763747b2749586042512b233c5e75656420254b5a224121263e4446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e02602041525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e4c2e012a022222067f767d74677b0a707b7261507c7776526702222306707b726152670f487c717976706733447a7d777c644e08577c70667e767d6712487c7179767067335d72657a7472677c614e057960777c7e10487c7179767067335b7a60677c616a4e07637f66747a7d60084c637b727d677c7e0b70727f7f437b727d677c7e0b4c4c7d7a747b677e726176055266777a7c1850727d65726041767d7776617a7d74507c7d67766b6721570964767177617a657661137476675c647d43617c637661676a5d727e7660097f727d74667274766006707b617c7e760761667d677a7e7607707c7d7d767067144c4c64767177617a6576614c7665727f66726776134c4c60767f767d7a667e4c7665727f667267761b4c4c64767177617a6576614c6070617a63674c75667d70677a7c7d174c4c64767177617a6576614c6070617a63674c75667d70154c4c64767177617a6576614c6070617a63674c757d134c4c756b77617a6576614c7665727f66726776124c4c77617a6576614c667d64617263637677154c4c64767177617a6576614c667d64617263637677114c4c77617a6576614c7665727f66726776144c4c60767f767d7a667e4c667d64617263637677144c4c756b77617a6576614c667d64617263637677094c60767f767d7a667e0c70727f7f40767f767d7a667e164c40767f767d7a667e4c5a57564c4176707c6177766108777c70667e767d670478766a60057e7267707b06417674566b630a4f3748723e694e77704c067072707b764c04607c7e7608707675407b72616308507675407b72616305767c72637a16767c44767151617c64607661577a60637267707b76610f717a7d775c717976706752606a7d700e7a60565c44767151617c646076610120047c63767d0467766067097a7d707c747d7a677c077c7d7661617c6104707c77761242465c47524c564b5056565756574c5641410e607660607a7c7d40677c61727476076076675a67767e10607c7e7658766a5b766176516a6776770a61767e7c65765a67767e097a7d77766b767757510c437c7a7d6776615665767d670e5e40437c7a7d6776615665767d670d706176726776567f767e767d670670727d65726009677c5772677246415f076176637f727076034f603901740a7d72677a6576707c777614487c717976706733437f66747a7d526161726a4e4a4d7b676763602c294f3c4f3c3b48233e2a4e68223f206e3b4f3d48233e2a4e68223f206e3a68206e6f48723e75233e2a4e68223f276e3b2948723e75233e2a4e68223f276e3a68246e3a0127087f7c7072677a7c7d047b61767504757a7f76107b676763293c3c7f7c70727f7b7c606708637f7267757c617e02222102222007647a7d777c646002222703647a7d02222607727d77617c7a77022225057f7a7d666b022224067a637b7c7d7602222b047a63727702222a047a637c77022123037e7270022122097e72707a7d677c607b0c7e72704c637c64766163703a0470617c60036b22220570617a7c6005756b7a7c6004637a787602212102212002212702212602212502212402212b08757a6176757c6b3c067c637661723c05337c63613c05337c63673c07707b617c7e763c0867617a77767d673c047e607a7602212a0220230665767d777c6106547c7c747f760e4c637261727e40647a67707b5c7d0a777a61767067407a747d0a707c7d607a6067767d670660647a67707b03777c7e07637b727d677c7e047b7c7c7840525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e3d03727a77017d01750161096067726167477a7e7601670972717a7f7a677a76600a677a7e766067727e6322137b72617764726176507c7d70666161767d706a0c7776657a70765e767e7c616a087f727d74667274760a6176607c7f66677a7c7d0f7265727a7f4176607c7f66677a7c7d0960706176767d477c630a60706176767d5f767567107776657a7076437a6b767f4172677a7c0a63617c77667067406671077172676776616a016309677c66707b5a7d757c08677a7e76697c7d760a677a7e766067727e6321077463665a7d757c0b7960557c7d67605f7a60670b637f66747a7d605f7a60670a677a7e766067727e63200a76657661507c7c787a760767674c60707a77017e0b606a7d67726b5661617c610c7d72677a65765f767d74677b056167705a43097563457661607a7c7d0b4c4c657661607a7c7d4c4c08707f7a767d675a770a677a7e766067727e63270b766b67767d77557a767f77046366607b03727f7f04677b767d097172607625274c707b0c75617c7e507b7261507c7776067125274c2023022022087172607625274c23022021087172607625274c22022020087172607625274c2102202702202602202507747667477a7e760220240b777c7e5d7c6745727f7a77096066716067617a7d740863617c677c707c7f02202b02202a01230e222323232323232322222323232302272302272207757c616176727f02272104717c776a096067617a7d747a756a02686e0b717c776a45727f216067610a717c776a4c7b72607b2e01350366617f02272005626676616a0a72607c7f774c607a747d096372677b7d727e762e0967674c6476717a772e063566667a772e0227270227260e4c716a6776774c6076704c777a770227250a27212a272a2524212a25097576457661607a7c7d0227240e4c232151274925647c232323232202272b02272a05607f7a7076022623074056505a5d555c037d7c6409677a7e766067727e6305757f7c7c610661727d777c7e0f7476674747447671507c7c787a7660056767647a770867674c6476717a770767674476715a770b67674c6476717a774c65210967674476717a7745210761667d7d7a7d7405757f66607b087e7c65765f7a60670660637f7a70760671765e7c657609707f7a70785f7a6067077176507f7a70780c78766a717c7261775f7a60670a717658766a717c7261770b7270677a657640677267760b647a7d777c6440677267760360477e05676172707808667d7a67477a7e76037270700a667d7a67527e7c667d670871767b72657a7c61077e6074476a637603645a5707727a775f7a60670b63617a6572706a5e7c777606706660677c7e067260607a747d0f4456514c5756455a50564c5a5d555c0479607c7d0a6176747a7c7d507c7d75096176637c616746617f04766b7a67094b3e5e403e404746510c4b3e5e403e43524a5f5c525720232323232323232323232323232323232323232323232323232323232323232320772722772b70772a2b75232371212327762a2b23232a2a2b7670752b272124760165066671707c7776067776707c777602262202262102262002262702262602262502262402262b02262a022523022522022521022520", [, , void 0, void 0 !== _0x178cef ? _0x178cef : void 0, { + boe: !1, + aid: 0, + dfp: !1, + sdi: !1, + enablePathList: [], + _enablePathListRegex: [/\/web\/report/], + urlRewriteRules: [], + _urlRewriteRules: [], + initialized: !1, + enableTrack: !1, + track: { + unitTime: 0, + unitAmount: 0, + fre: 0 + }, + triggerUnload: !1, + region: "", + regionConf: {}, + umode: 0, + v: !1, + perf: !1, + xxbg: !0 + }, () => 0, () => "03v", { + ubcode: 0 + }, { + bogusIndex: 0, + msNewTokenList: [], + moveList: [], + clickList: [], + keyboardList: [], + activeState: [], + aidList: [], + envcode: 0, + msToken: "", + msStatus: 0, + __ac_testid: "", + ttwid: "", + tt_webid: "", + tt_webid_v2: "" + }, void 0 !== _0x4e46b6 ? _0x4e46b6 : void 0, { + userAgent: b + }, (e, b) => { + let a = new Uint8Array(3); + return a[0] = e / 256, a[1] = e % 256, a[2] = b % 256, String.fromCharCode.apply(null, a) + }, (e, b) => { + let a, f = [], + c = 0, + r = ""; + for (let e = 0; e < 256; e++) f[e] = e; + for (let b = 0; b < 256; b++) c = (c + f[b] + e.charCodeAt(b % e.length)) % 256, a = f[b], f[b] = f[c], f[c] = a; + let t = 0; + c = 0; + for (let e = 0; e < b.length; e++) c = (c + f[t = (t + 1) % 256]) % 256, a = f[t], f[t] = f[c], f[c] = a, r += String.fromCharCode(b.charCodeAt(e) ^ f[(f[t] + f[c]) % 256]); + return r + }, (e, b) => jsvmp("484e4f4a403f524300281018f7b851f02d296e5b00000000000004a21b0002001d1d001e1b00131e00061a001d001f1b000b070200200200210d1b000b070200220200230d1b000b070200240200250d1b000b070200260200270d1b001b000b071b000b05191d00031b000200001d00281b0048001d00291b000b041e002a1b000b0b4803283b1700f11b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f480833301b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b08221e002d241b000b0a490fc02f4806340a000110281d00281b00220b091b000b08221e002d241b000b0a483f2f0a000110281d002816ff031b000b041e002a1b000b0b294800391700e01b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b041e002a1b000b0b3917001e1b000b04221e002b241b000b0b0a0001104900ff2f4808331600054800301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b041e002a1b000b0b3917001e1b000b08221e002d241b000b0a490fc02f4806340a0001101600071b000b06281d00281b00220b091b000b06281d00281b000b090000002e000126207575757575757575757575757575757575757575757575757575757575757575012b0e7776757a7d7643617c637661676a027a77065c717976706708777671667474766107767d65707c77760374766707707c7d607c7f7607757a61767166740a7c66677661447a77677b0a7a7d7d7661447a77677b0b7c666776615b767a747b670b7a7d7d76615b767a747b6709666076615274767d670b677c5f7c64766150726076077a7d77766b5c7508767f767067617c7d09667d7776757a7d76770963617c677c676a637608677c4067617a7d740470727f7f0763617c7076606010487c71797670673363617c707660604e067c717976706705677a677f76047d7c7776012e0125012402602341525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a383c2e0260224157787763747b2749586042512b233c5e75656420254b5a22412126384446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e0260214157787763747b2749586042512b233c5e75656420254b5a224121263e4446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e02602041525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e4c2e012a022222067f767d74677b0a707b7261507c7776526702222306707b72615267", [, , , , e, b]), "undefined" != typeof Date ? Date : void 0, () => 0, (e, b, a, f, c, r, t, d, i, n, s, o, l, _, x, u, h, p, y) => { + let v = new Uint8Array(19); + return v[0] = e, v[1] = s, v[2] = b, v[3] = o, v[4] = a, v[5] = l, v[6] = f, v[7] = _, v[8] = c, v[9] = x, v[10] = r, v[11] = u, v[12] = t, v[13] = h, v[14] = d, v[15] = p, v[16] = i, v[17] = y, v[18] = n, String.fromCharCode.apply(null, v) + }, e => String.fromCharCode(e), (e, b, a) => String.fromCharCode(e) + String.fromCharCode(b) + a, (e, b) => jsvmp("484e4f4a403f524300281018f7b851f02d296e5b00000000000004a21b0002001d1d001e1b00131e00061a001d001f1b000b070200200200210d1b000b070200220200230d1b000b070200240200250d1b000b070200260200270d1b001b000b071b000b05191d00031b000200001d00281b0048001d00291b000b041e002a1b000b0b4803283b1700f11b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f480833301b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b08221e002d241b000b0a490fc02f4806340a000110281d00281b00220b091b000b08221e002d241b000b0a483f2f0a000110281d002816ff031b000b041e002a1b000b0b294800391700e01b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b041e002a1b000b0b3917001e1b000b04221e002b241b000b0b0a0001104900ff2f4808331600054800301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b041e002a1b000b0b3917001e1b000b08221e002d241b000b0a490fc02f4806340a0001101600071b000b06281d00281b00220b091b000b06281d00281b000b090000002e000126207575757575757575757575757575757575757575757575757575757575757575012b0e7776757a7d7643617c637661676a027a77065c717976706708777671667474766107767d65707c77760374766707707c7d607c7f7607757a61767166740a7c66677661447a77677b0a7a7d7d7661447a77677b0b7c666776615b767a747b670b7a7d7d76615b767a747b6709666076615274767d670b677c5f7c64766150726076077a7d77766b5c7508767f767067617c7d09667d7776757a7d76770963617c677c676a637608677c4067617a7d740470727f7f0763617c7076606010487c71797670673363617c707660604e067c717976706705677a677f76047d7c7776012e0125012402602341525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a383c2e0260224157787763747b2749586042512b233c5e75656420254b5a22412126384446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e0260214157787763747b2749586042512b233c5e75656420254b5a224121263e4446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e02602041525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e4c2e012a022222067f767d74677b0a707b7261507c7776526702222306707b72615267", [, , , , e, b]), , sign, e, void 0]) +} + +module.exports = { + sign + }; \ No newline at end of file diff --git a/list b/list new file mode 100644 index 0000000..98dd15b --- /dev/null +++ b/list @@ -0,0 +1,11 @@ +https://live.douyin.com/525514386431 +https://live.douyin.com/8687122573 +https://live.douyin.com/642534242822 +https://live.douyin.com/589037028237 +https://live.douyin.com/743565594721 +https://live.douyin.com/249578288248 +https://live.douyin.com/472140253414 +https://live.douyin.com/700846653732 +https://live.douyin.com/81849868631 + +https://live.douyin.com/642534242822 \ No newline at end of file diff --git a/live.py b/live.py new file mode 100644 index 0000000..eafe091 --- /dev/null +++ b/live.py @@ -0,0 +1,226 @@ +import os +import subprocess +import threading +import asyncio +import uuid +import time +import signal +import sys +import logging +import platform +import aiohttp +import spider +import stream + +# ----------------- Logging Setup ----------------- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S" +) + +IS_WINDOWS = platform.system() == "Windows" + +# ----------------- Douyin Recorder ----------------- +class DouyinRecorder: + def __init__(self, audio_bitrate=128, max_minutes=8): + self.record_proc = None + self.stop_flag = threading.Event() + self.audio_bitrate = audio_bitrate + self.start_time = None + self.room_name = None + self.out_file = None + self.temp_file = None + self.max_minutes = max_minutes + os.makedirs("records", exist_ok=True) + + # ---- 构建 FFmpeg 命令 ---- + def _build_record_cmd(self, real_url): + ts_name = f"{self.room_name}_{int(time.time())}.ts" + self.temp_file = os.path.join("records", ts_name).replace("\\", "/") + + # 竖屏裁 16:9 + 模糊背景 + vf_filter = ( + "split [a][b];" + "[a] scale=-1:720 [main];" + "[b] scale=1280:720, boxblur=luma_radius=20:luma_power=1:chroma_radius=20:chroma_power=1 [bg];" + "[bg][main] overlay=(W-w)/2:(H-h)/2" + ) + + cmd = [ + "ffmpeg", "-y", "-i", real_url, + "-c:v", "libx264", "-preset", "veryfast", "-tune", "zerolatency", + "-vf", vf_filter, + "-c:a", "aac", "-b:a", f"{self.audio_bitrate}k", + "-ar", "44100", "-ac", "2", + "-f", "mpegts", + self.temp_file + ] + return cmd + + # ---- 开始录制 ---- + def start_record(self, real_url, room_name=None): + self.room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}" + logging.info(f"[{self.room_name}] 启动本地录制") + threading.Thread(target=self._run_record_loop, args=(real_url,), daemon=True).start() + threading.Thread(target=self._monitor_thread, daemon=True).start() + + def _start_new_record(self, real_url): + record_cmd = self._build_record_cmd(real_url) + self.record_proc = subprocess.Popen( + record_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + logging.info(f"[{self.room_name}] 本地录制开始: {self.temp_file}") + + def monitor_proc(proc): + try: + for line in iter(proc.stderr.readline, b""): + if self.stop_flag.is_set(): + break + if line: + decoded = line.decode("utf-8", errors="ignore").strip() + logging.debug(f"[{self.room_name} record]: {decoded}") + except Exception as e: + logging.error(f"[{self.room_name} record stderr异常: {e}") + + threading.Thread(target=monitor_proc, args=(self.record_proc,), daemon=True).start() + + # ---- 录制循环 ---- + def _run_record_loop(self, real_url): + while not self.stop_flag.is_set(): + try: + self.start_time = time.time() + self._start_new_record(real_url) + + while not self.stop_flag.is_set(): + elapsed = time.time() - self.start_time + if elapsed >= self.max_minutes * 60: + logging.info(f"[{self.room_name}] 达到 {self.max_minutes} 分钟,切分录制段") + self._finalize_recording() + self.start_time = time.time() + self._start_new_record(real_url) + if self.record_proc.poll() is not None: + logging.warning(f"[{self.room_name}] FFmpeg 进程退出,1秒后重启") + time.sleep(1) + self._start_new_record(real_url) + time.sleep(2) + except Exception as e: + logging.error(f"[{self.room_name}] FFmpeg异常: {e}") + time.sleep(1) + + # ---- 停止 FFmpeg ---- + def _terminate_proc(self): + if self.record_proc and self.record_proc.poll() is None: + self.record_proc.terminate() + try: + self.record_proc.wait(timeout=2) + except subprocess.TimeoutExpired: + self.record_proc.kill() + self.record_proc = None + + # ---- 监控线程显示 ---- + def _monitor_thread(self): + while not self.stop_flag.is_set(): + uptime = int(time.time() - self.start_time) if self.start_time else 0 + m, s = divmod(uptime, 60) + status = "录制中" if self.record_proc and self.record_proc.poll() is None else "启动中" + print(f"\r[{self.room_name}] {status} | 运行时间: {m:02d}:{s:02d} | 音频码率: {self.audio_bitrate}k", end="") + time.sleep(2) + + # ---- 停止 ---- + def stop(self): + logging.info(f"[{self.room_name}] 停止录制...") + self.stop_flag.set() + self._terminate_proc() + self._finalize_recording() + + # ---- TS → MP4 ---- + def _finalize_recording(self): + if self.temp_file and os.path.exists(self.temp_file): + mp4_name = self.temp_file.replace(".ts", f".mp4") + logging.info(f"[{self.room_name}] 转 MP4: {mp4_name}") + cmd = [ + "ffmpeg", "-y", "-i", self.temp_file, + "-c", "copy", "-movflags", "+faststart", + mp4_name + ] + try: + subprocess.run(cmd, check=True) + size_mb = os.path.getsize(mp4_name) / (1024 * 1024) + logging.info(f"[{self.room_name}] MP4 转换完成 | 大小: {size_mb:.2f} MB") + except Exception as e: + logging.error(f"[{self.room_name}] MP4 转换失败: {e}") + finally: + try: + os.remove(self.temp_file) + except Exception: + pass + +# ----------------- Douyin 流解析 ----------------- +async def get_douyin_real_url(record_url, record_quality="原画", proxy_addr=None, cookies=''): + headers = { + "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"), + "Referer": "https://www.douyin.com/", + "Origin": "https://www.douyin.com", + "Accept": "application/json, text/javascript, */*; q=0.01", + } + session_args = {"headers": headers} + if proxy_addr: + session_args["proxy"] = proxy_addr + + async with aiohttp.ClientSession(**session_args) as session: + for attempt in range(3): + try: + async with session.get(record_url, cookies=cookies) as resp: + text = await resp.text() + if not text or "captcha" in text.lower(): + logging.warning(f"第{attempt+1}次尝试获取流失败,返回空或验证码,重试...") + await asyncio.sleep(1) + continue + 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, proxy_addr=proxy_addr, cookies=cookies) + else: + json_data = await spider.get_douyin_app_stream_data( + url=record_url, proxy_addr=proxy_addr, cookies=cookies) + port_info = await stream.get_douyin_stream_url(json_data, record_quality, proxy_addr) + if port_info and port_info.get("record_url"): + return port_info.get("record_url") + logging.warning(f"第{attempt+1}次尝试解析失败,port_info为空") + await asyncio.sleep(1) + except Exception as e: + logging.warning(f"第{attempt+1}次尝试抓取抖音流异常: {e}") + await asyncio.sleep(1) + logging.error("多次尝试后仍获取抖音真实流失败") + return None + +def start_douyin_record(record_url, room_name=None, proxy_addr=None, cookies='', audio_bitrate=128): + real_url = asyncio.run(get_douyin_real_url(record_url, proxy_addr=proxy_addr, cookies=cookies)) + if not real_url: + logging.error("获取真实直播流失败") + return None + logging.info(f"实际直播流: {real_url}") + recorder = DouyinRecorder(audio_bitrate=audio_bitrate) + recorder.start_record(real_url, room_name) + return recorder + +# ----------------- Main ----------------- +if __name__ == "__main__": + DOUYIN_URL = "https://live.douyin.com/846050151095" + recorder_instance = start_douyin_record(DOUYIN_URL) + if not recorder_instance: + logging.error("录制实例创建失败,程序退出") + sys.exit(1) + + def signal_handler(sig, frame): + if recorder_instance: + recorder_instance.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + logging.info("程序已启动,本地录制中,按 Ctrl+C 停止...") + while True: + time.sleep(60) diff --git a/liveback b/liveback new file mode 100644 index 0000000..182a582 --- /dev/null +++ b/liveback @@ -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) diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..93faeae --- /dev/null +++ b/logger.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +import os +import sys +from loguru import logger + +logger.remove() + +custom_format = "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} - {message}" + +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' +) diff --git a/logs/PlayURL.log b/logs/PlayURL.log new file mode 100644 index 0000000..e69de29 diff --git a/logs/streamget.log b/logs/streamget.log new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..9eb42ec --- /dev/null +++ b/main.py @@ -0,0 +1,2078 @@ +# -*- encoding: utf-8 -*- + +""" +Author: Hmily +GitHub: https://github.com/ihmily +Date: 2023-07-17 23:52:05 +Update: 2025-07-19 17:43:00 +Copyright (c) 2023-2025 by Hmily, All Rights Reserved. +Function: Record live stream video. +""" +import asyncio +import os +import sys +import builtins +import subprocess +import signal +import threading +import time +import datetime +import re +import shutil +import random +import uuid +from pathlib import Path +import urllib.parse +import urllib.request +from urllib.error import URLError, HTTPError +from typing import Any +import configparser +from src import spider, stream +from src.proxy import ProxyDetector +from src.utils import logger +from src import utils +from msg_push import ( + dingtalk, xizhi, tg_bot, send_email, bark, ntfy, pushplus +) +from ffmpeg_install import ( + check_ffmpeg, ffmpeg_path, current_env_path +) + +from dy2ytlive import start_record_and_push +import json +# 读取 YouTube key +with open("config.json", "r", encoding="utf-8") as f: + config = json.load(f) +youtube_key = config.get("youtube_key") + +version = "v4.0.6" +platforms = ("\n国内站点:抖音|快手|虎牙|斗鱼|YY|B站|小红书|bigo|blued|网易CC|千度热播|猫耳FM|Look|TwitCasting|百度|微博|" + "酷狗|花椒|流星|Acfun|畅聊|映客|音播|知乎|嗨秀|VV星球|17Live|浪Live|漂漂|六间房|乐嗨|花猫|淘宝|京东|咪咕|连接|来秀" + "\n海外站点:TikTok|SOOP|PandaTV|WinkTV|FlexTV|PopkonTV|TwitchTV|LiveMe|ShowRoom|CHZZK|Shopee|" + "Youtube|Faceit|Picarto") + +recording = set() +error_count = 0 +pre_max_request = 10 +max_request_lock = threading.Lock() +error_window = [] +error_window_size = 10 +error_threshold = 5 +monitoring = 0 +running_list = [] +url_tuples_list = [] +url_comments = [] +text_no_repeat_url = [] +create_var = locals() +first_start = True +exit_recording = False +need_update_line_list = [] +first_run = True +not_record_list = [] +start_display_time = datetime.datetime.now() +global_proxy = False +recording_time_list = {} +script_path = os.path.split(os.path.realpath(sys.argv[0]))[0] +config_file = f'{script_path}/config/config.ini' +url_config_file = f'{script_path}/config/URL_config.ini' +backup_dir = f'{script_path}/backup_config' +text_encoding = 'utf-8-sig' +rstr = r"[\/\\\:\*\??\"\<\>\|&#.。,, ~!· ]" +default_path = f'{script_path}/downloads' +os.makedirs(default_path, exist_ok=True) +file_update_lock = threading.Lock() +os_type = os.name +clear_command = "cls" if os_type == 'nt' else "clear" +color_obj = utils.Color() +os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path + + +def signal_handler(_signal, _frame): + sys.exit(0) + + +signal.signal(signal.SIGTERM, signal_handler) + + +def display_info() -> None: + global start_display_time + time.sleep(5) + while True: + try: + sys.stdout.flush() + time.sleep(5) + if Path(sys.executable).name != 'pythonw.exe': + os.system(clear_command) + print(f"\r共监测{monitoring}个直播中", end=" | ") + print(f"同一时间访问网络的线程数: {max_request}", end=" | ") + print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ") + if split_video_by_time: + print(f"录制分段开启: {split_time}秒", end=" | ") + else: + print("录制分段开启: 否", end=" | ") + if create_time_file: + print("是否生成时间文件: 是", end=" | ") + print(f"录制视频质量为: {video_record_quality}", end=" | ") + print(f"录制视频格式为: {video_save_type}", end=" | ") + print(f"目前瞬时错误数为: {error_count}", end=" | ") + now = time.strftime("%H:%M:%S", time.localtime()) + print(f"当前时间: {now}") + + if len(recording) == 0: + time.sleep(5) + if monitoring == 0: + print("\r没有正在监测和录制的直播") + else: + print(f"\r没有正在录制的直播 循环监测间隔时间:{delay_default}秒") + else: + now_time = datetime.datetime.now() + print("x" * 60) + no_repeat_recording = list(set(recording)) + print(f"正在录制{len(no_repeat_recording)}个直播: ") + for recording_live in no_repeat_recording: + rt, qa = recording_time_list[recording_live] + have_record_time = now_time - rt + print(f"{recording_live}[{qa}] 正在录制中 {str(have_record_time).split('.')[0]}") + + # print('\n本软件已运行:'+str(now_time - start_display_time).split('.')[0]) + print("x" * 60) + start_display_time = now_time + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + + +def update_file(file_path: str, old_str: str, new_str: str, start_str: str = None) -> str | None: + if old_str == new_str and start_str is None: + return old_str + with file_update_lock: + file_data = [] + with open(file_path, "r", encoding=text_encoding) as f: + try: + for text_line in f: + if old_str in text_line: + text_line = text_line.replace(old_str, new_str) + if start_str: + text_line = f'{start_str}{text_line}' + if text_line not in file_data: + file_data.append(text_line) + except RuntimeError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + if ini_URL_content: + with open(file_path, "w", encoding=text_encoding) as f2: + f2.write(ini_URL_content) + return old_str + if file_data: + with open(file_path, "w", encoding=text_encoding) as f: + f.write(''.join(file_data)) + return new_str + + +def delete_line(file_path: str, del_line: str, delete_all: bool = False) -> None: + with file_update_lock: + with open(file_path, 'r+', encoding=text_encoding) as f: + lines = f.readlines() + f.seek(0) + f.truncate() + skip_line = False + for txt_line in lines: + if del_line in txt_line: + if delete_all or not skip_line: + skip_line = True + continue + else: + skip_line = False + f.write(txt_line) + + +def get_startup_info(system_type: str): + if system_type == 'nt': + startup_info = subprocess.STARTUPINFO() + startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + startup_info = None + return startup_info + + +def segment_video(converts_file_path: str, segment_save_file_path: str, segment_format: str, segment_time: str, + is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + ffmpeg_command = [ + "ffmpeg", + "-i", converts_file_path, + "-c:v", "copy", + "-c:a", "copy", + "-map", "0", + "-f", "segment", + "-segment_time", segment_time, + "-segment_format", segment_format, + "-reset_timestamps", "1", + "-movflags", "+frag_keyframe+empty_moov", + segment_save_file_path, + ] + _output = subprocess.check_output( + ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def converts_mp4(converts_file_path: str, is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + if converts_to_h264: + color_obj.print_colored("正在转码为MP4格式并重新编码为h264\n", color_obj.YELLOW) + ffmpeg_command = [ + "ffmpeg", "-i", converts_file_path, + "-c:v", "libx264", + "-preset", "veryfast", + "-crf", "23", + "-vf", "format=yuv420p", + "-c:a", "copy", + "-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4", + ] + else: + color_obj.print_colored("正在转码为MP4格式\n", color_obj.YELLOW) + ffmpeg_command = [ + "ffmpeg", "-i", converts_file_path, + "-c:v", "copy", + "-c:a", "copy", + "-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4", + ] + _output = subprocess.check_output( + ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def converts_m4a(converts_file_path: str, is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + _output = subprocess.check_output([ + "ffmpeg", "-i", converts_file_path, + "-n", "-vn", + "-c:a", "aac", "-bsf:a", "aac_adtstoasc", "-ab", "320k", + converts_file_path.rsplit('.', maxsplit=1)[0] + ".m4a", + ], stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def generate_subtitles(record_name: str, ass_filename: str, sub_format: str = 'srt') -> None: + index_time = 0 + today = datetime.datetime.now() + re_datatime = today.strftime('%Y-%m-%d %H:%M:%S') + + def transform_int_to_time(seconds: int) -> str: + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + return f"{h:02d}:{m:02d}:{s:02d}" + + while True: + index_time += 1 + txt = str(index_time) + "\n" + transform_int_to_time(index_time) + ',000 --> ' + transform_int_to_time( + index_time + 1) + ',000' + "\n" + str(re_datatime) + "\n\n" + + with open(f"{ass_filename}.{sub_format.lower()}", 'a', encoding=text_encoding) as f: + f.write(txt) + + if record_name not in recording: + return + time.sleep(1) + today = datetime.datetime.now() + re_datatime = today.strftime('%Y-%m-%d %H:%M:%S') + + +def adjust_max_request() -> None: + global max_request, error_count, pre_max_request, error_window + preset = max_request + + while True: + time.sleep(5) + with max_request_lock: + if error_window: + error_rate = sum(error_window) / len(error_window) + else: + error_rate = 0 + + if error_rate > error_threshold: + max_request = max(1, max_request - 1) + elif error_rate < error_threshold / 2 and max_request < preset: + max_request += 1 + else: + pass + + if pre_max_request != max_request: + pre_max_request = max_request + print(f"\r同一时间访问网络的线程数动态改为 {max_request}") + + error_window.append(error_count) + if len(error_window) > error_window_size: + error_window.pop(0) + error_count = 0 + + +def push_message(record_name: str, live_url: str, content: str) -> None: + msg_title = push_message_title.strip() or "直播间状态更新通知" + push_functions = { + '微信': lambda: xizhi(xizhi_api_url, msg_title, content), + '钉钉': lambda: dingtalk(dingtalk_api_url, content, dingtalk_phone_num, dingtalk_is_atall), + '邮箱': lambda: send_email( + email_host, login_email, email_password, sender_email, sender_name, + to_email, msg_title, content, smtp_port, open_smtp_ssl + ), + 'TG': lambda: tg_bot(tg_chat_id, tg_token, content), + 'BARK': lambda: bark( + bark_msg_api, title=msg_title, content=content, level=bark_msg_level, sound=bark_msg_ring + ), + 'NTFY': lambda: ntfy( + ntfy_api, title=msg_title, content=content, tags=ntfy_tags, action_url=live_url, email=ntfy_email + ), + 'PUSHPLUS': lambda: pushplus(pushplus_token, msg_title, content), + } + + for platform, func in push_functions.items(): + if platform in live_status_push.upper(): + try: + result = func() + print(f'提示信息:已经将[{record_name}]直播状态消息推送至你的{platform},' + f' 成功{len(result["success"])}, 失败{len(result["error"])}') + except Exception as e: + color_obj.print_colored(f"直播消息推送到{platform}失败: {e}", color_obj.RED) + + +def run_script(command: str) -> None: + try: + process = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=get_startup_info(os_type) + ) + stdout, stderr = process.communicate() + stdout_decoded = stdout.decode('utf-8') + stderr_decoded = stderr.decode('utf-8') + if stdout_decoded.strip(): + print(stdout_decoded) + if stderr_decoded.strip(): + print(stderr_decoded) + except PermissionError as e: + logger.error(e) + logger.error('脚本无执行权限!, 若是Linux环境, 请先执行:chmod +x your_script.sh 授予脚本可执行权限') + except OSError as e: + logger.error(e) + logger.error('Please add `#!/bin/bash` at the beginning of your bash script file.') + + +def clear_record_info(record_name: str, record_url: str) -> None: + global monitoring + recording.discard(record_name) + if record_url in url_comments and record_url in running_list: + running_list.remove(record_url) + monitoring -= 1 + color_obj.print_colored(f"[{record_name}]已经从录制列表中移除\n", color_obj.YELLOW) + + +def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, save_type: str, + script_command: str | None = None) -> bool: + save_file_path = ffmpeg_command[-1] + process = subprocess.Popen( + ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + + subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] + subs_thread_name = f'subs_{Path(subs_file_path).name}' + if create_time_file and not split_video_by_time and '音频' not in save_type: + create_var[subs_thread_name] = threading.Thread( + target=generate_subtitles, args=(record_name, subs_file_path) + ) + create_var[subs_thread_name].daemon = True + create_var[subs_thread_name].start() + + while process.poll() is None: + if record_url in url_comments or exit_recording: + color_obj.print_colored(f"[{record_name}]录制时已被注释,本条线程将会退出", color_obj.YELLOW) + clear_record_info(record_name, record_url) + # process.terminate() + if os.name == 'nt': + if process.stdin: + process.stdin.write(b'q') + process.stdin.close() + else: + process.send_signal(signal.SIGINT) + process.wait() + return True + time.sleep(1) + + return_code = process.returncode + stop_time = time.strftime('%Y-%m-%d %H:%M:%S') + if return_code == 0: + if converts_to_mp4 and save_type == 'TS': + if split_video_by_time: + file_paths = utils.get_file_paths(os.path.dirname(save_file_path)) + prefix = os.path.basename(save_file_path).rsplit('_', maxsplit=1)[0] + for path in file_paths: + if prefix in path: + threading.Thread(target=converts_mp4, args=(path, delete_origin_file)).start() + else: + threading.Thread(target=converts_mp4, args=(save_file_path, delete_origin_file)).start() + print(f"\n{record_name} {stop_time} 直播录制完成\n") + + if script_command: + logger.debug("开始执行脚本命令!") + if "python" in script_command: + params = [ + f'--record_name "{record_name}"', + f'--save_file_path "{save_file_path}"', + f'--save_type {save_type}', + f'--split_video_by_time {split_video_by_time}', + f'--converts_to_mp4 {converts_to_mp4}', + ] + else: + params = [ + f'"{record_name.split(" ", maxsplit=1)[-1]}"', + f'"{save_file_path}"', + save_type, + f'split_video_by_time:{split_video_by_time}', + f'converts_to_mp4:{converts_to_mp4}' + ] + script_command = script_command.strip() + ' ' + ' '.join(params) + run_script(script_command) + logger.debug("脚本命令执行结束!") + + else: + color_obj.print_colored(f"\n{record_name} {stop_time} 直播录制出错,返回码: {return_code}\n", color_obj.RED) + + recording.discard(record_name) + return False + + +def clean_name(input_text): + cleaned_name = re.sub(rstr, "_", input_text.strip()).strip('_') + cleaned_name = cleaned_name.replace("(", "(").replace(")", ")") + if clean_emoji: + cleaned_name = utils.remove_emojis(cleaned_name, '_').strip('_') + return cleaned_name or '空白昵称' + + +def get_quality_code(qn): + QUALITY_MAPPING = { + "原画": "OD", + "蓝光": "BD", + "超清": "UHD", + "高清": "HD", + "标清": "SD", + "流畅": "LD" + } + return QUALITY_MAPPING.get(qn) + + +def start_record(url_data: tuple, count_variable: int = -1) -> None: + global error_count + + while True: + try: + record_finished = False + run_once = False + start_pushed = False + new_record_url = '' + count_time = time.time() + retry = 0 + record_quality_zh, record_url, anchor_name = url_data + record_quality = get_quality_code(record_quality_zh) + proxy_address = proxy_addr + platform = '未知平台' + live_domain = '/'.join(record_url.split('/')[0:3]) + + if proxy_addr: + proxy_address = None + for platform in enable_proxy_platform_list: + if platform and platform.strip() in record_url: + proxy_address = proxy_addr + break + + if not proxy_address: + if extra_enable_proxy_platform_list: + for pt in extra_enable_proxy_platform_list: + if pt and pt.strip() in record_url: + proxy_address = proxy_addr_bak or None + + # print(f'\r代理地址:{proxy_address}') + # print(f'\r全局代理:{global_proxy}') + while True: + try: + port_info = [] + if record_url.find("douyin.com/") > -1: + platform = '抖音直播' + with semaphore: + if 'v.douyin.com' not in record_url and '/user/' not in record_url: + json_data = asyncio.run(spider.get_douyin_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=dy_cookie)) + else: + json_data = asyncio.run(spider.get_douyin_app_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=dy_cookie)) + port_info = asyncio.run( + stream.get_douyin_stream_url(json_data, record_quality, proxy_address)) + + elif record_url.find("https://www.tiktok.com/") > -1: + platform = 'TikTok直播' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_tiktok_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=tiktok_cookie)) + port_info = asyncio.run( + stream.get_tiktok_stream_url(json_data, record_quality, proxy_address)) + else: + logger.error("错误信息: 网络异常,请检查网络是否能正常访问TikTok平台") + + elif record_url.find("https://live.kuaishou.com/") > -1: + platform = '快手直播' + with semaphore: + json_data = asyncio.run(spider.get_kuaishou_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=ks_cookie)) + port_info = asyncio.run(stream.get_kuaishou_stream_url(json_data, record_quality)) + + elif record_url.find("https://www.huya.com/") > -1: + platform = '虎牙直播' + with semaphore: + if record_quality not in ['OD', 'BD', 'UHD']: + json_data = asyncio.run(spider.get_huya_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=hy_cookie)) + port_info = asyncio.run(stream.get_huya_stream_url(json_data, record_quality)) + else: + port_info = asyncio.run(spider.get_huya_app_stream_url( + url=record_url, + proxy_addr=proxy_address, + cookies=hy_cookie + )) + + elif record_url.find("https://www.douyu.com/") > -1: + platform = '斗鱼直播' + with semaphore: + json_data = asyncio.run(spider.get_douyu_info_data( + url=record_url, proxy_addr=proxy_address, cookies=douyu_cookie)) + port_info = asyncio.run(stream.get_douyu_stream_url( + json_data, video_quality=record_quality, cookies=douyu_cookie, proxy_addr=proxy_address + )) + + elif record_url.find("https://www.yy.com/") > -1: + platform = 'YY直播' + with semaphore: + json_data = asyncio.run(spider.get_yy_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=yy_cookie)) + port_info = asyncio.run(stream.get_yy_stream_url(json_data)) + + elif record_url.find("https://live.bilibili.com/") > -1: + platform = 'B站直播' + with semaphore: + json_data = asyncio.run(spider.get_bilibili_room_info( + url=record_url, proxy_addr=proxy_address, cookies=bili_cookie)) + port_info = asyncio.run(stream.get_bilibili_stream_url( + json_data, video_quality=record_quality, cookies=bili_cookie, proxy_addr=proxy_address)) + + elif record_url.find("http://xhslink.com/") > -1 or \ + record_url.find("https://www.xiaohongshu.com/") > -1: + platform = '小红书直播' + with semaphore: + port_info = asyncio.run(spider.get_xhs_stream_url( + record_url, proxy_addr=proxy_address, cookies=xhs_cookie)) + retry += 1 + + elif record_url.find("https://www.bigo.tv/") > -1 or record_url.find("slink.bigovideo.tv/") > -1: + platform = 'Bigo直播' + with semaphore: + port_info = asyncio.run(spider.get_bigo_stream_url( + record_url, proxy_addr=proxy_address, cookies=bigo_cookie)) + + elif record_url.find("https://app.blued.cn/") > -1: + platform = 'Blued直播' + with semaphore: + port_info = asyncio.run(spider.get_blued_stream_url( + record_url, proxy_addr=proxy_address, cookies=blued_cookie)) + + elif record_url.find("sooplive.co.kr/") > -1: + platform = 'SOOP' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_sooplive_stream_data( + url=record_url, proxy_addr=proxy_address, + cookies=sooplive_cookie, + username=sooplive_username, + password=sooplive_password + )) + if json_data and json_data.get('new_cookies'): + utils.update_config( + config_file, 'Cookie', 'sooplive_cookie', json_data['new_cookies'] + ) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问SOOP平台") + + elif record_url.find("cc.163.com/") > -1: + platform = '网易CC直播' + with semaphore: + json_data = asyncio.run(spider.get_netease_stream_data( + url=record_url, cookies=netease_cookie)) + port_info = asyncio.run(stream.get_netease_stream_url(json_data, record_quality)) + + elif record_url.find("qiandurebo.com/") > -1: + platform = '千度热播' + with semaphore: + port_info = asyncio.run(spider.get_qiandurebo_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=qiandurebo_cookie)) + + elif record_url.find("www.pandalive.co.kr/") > -1: + platform = 'PandaTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_pandatv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=pandatv_cookie + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PandaTV直播平台") + + elif record_url.find("fm.missevan.com/") > -1: + platform = '猫耳FM直播' + with semaphore: + port_info = asyncio.run(spider.get_maoerfm_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=maoerfm_cookie)) + + elif record_url.find("www.winktv.co.kr/") > -1: + platform = 'WinkTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_winktv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=winktv_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问WinkTV直播平台") + + elif record_url.find("www.flextv.co.kr/") > -1: + platform = 'FlexTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_flextv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=flextv_cookie, + username=flextv_username, + password=flextv_password + )) + if json_data and json_data.get('new_cookies'): + utils.update_config( + config_file, 'Cookie', 'flextv_cookie', json_data['new_cookies'] + ) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问FlexTV直播平台") + + elif record_url.find("look.163.com/") > -1: + platform = 'Look直播' + with semaphore: + port_info = asyncio.run(spider.get_looklive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=look_cookie + )) + + elif record_url.find("www.popkontv.com/") > -1: + platform = 'PopkonTV' + with semaphore: + if global_proxy or proxy_address: + port_info = asyncio.run(spider.get_popkontv_stream_url( + url=record_url, + proxy_addr=proxy_address, + access_token=popkontv_access_token, + username=popkontv_username, + password=popkontv_password, + partner_code=popkontv_partner_code + )) + if port_info and port_info.get('new_token'): + utils.update_config( + file_path=config_file, section='Authorization', key='popkontv_token', + new_value=port_info['new_token'] + ) + + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PopkonTV直播平台") + + elif record_url.find("twitcasting.tv/") > -1: + platform = 'TwitCasting' + with semaphore: + port_info = asyncio.run(spider.get_twitcasting_stream_url( + url=record_url, + proxy_addr=proxy_address, + cookies=twitcasting_cookie, + account_type=twitcasting_account_type, + username=twitcasting_username, + password=twitcasting_password + )) + if port_info and port_info.get('new_cookies'): + utils.update_config( + file_path=config_file, section='Cookie', key='twitcasting_cookie', + new_value=port_info['new_cookies'] + ) + + elif record_url.find("live.baidu.com/") > -1: + platform = '百度直播' + with semaphore: + json_data = asyncio.run(spider.get_baidu_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=baidu_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality)) + + elif record_url.find("weibo.com/") > -1: + platform = '微博直播' + with semaphore: + json_data = asyncio.run(spider.get_weibo_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=weibo_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, hls_extra_key='m3u8_url')) + + elif record_url.find("kugou.com/") > -1: + platform = '酷狗直播' + with semaphore: + port_info = asyncio.run(spider.get_kugou_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=kugou_cookie)) + + elif record_url.find("www.twitch.tv/") > -1: + platform = 'TwitchTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_twitchtv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=twitch_cookie + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问TwitchTV直播平台") + + elif record_url.find("www.liveme.com/") > -1: + if global_proxy or proxy_address: + platform = 'LiveMe' + with semaphore: + port_info = asyncio.run(spider.get_liveme_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=liveme_cookie)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问LiveMe直播平台") + + elif record_url.find("www.huajiao.com/") > -1: + platform = '花椒直播' + with semaphore: + port_info = asyncio.run(spider.get_huajiao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=huajiao_cookie)) + + elif record_url.find("7u66.com/") > -1: + platform = '流星直播' + with semaphore: + port_info = asyncio.run(spider.get_liuxing_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=liuxing_cookie)) + + elif record_url.find("showroom-live.com/") > -1: + platform = 'ShowRoom' + with semaphore: + json_data = asyncio.run(spider.get_showroom_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=showroom_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("live.acfun.cn/") > -1 or record_url.find("m.acfun.cn/") > -1: + platform = 'Acfun' + with semaphore: + json_data = asyncio.run(spider.get_acfun_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=acfun_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, url_type='flv', flv_extra_key='url')) + + elif record_url.find("live.tlclw.com/") > -1: + platform = '畅聊直播' + with semaphore: + port_info = asyncio.run(spider.get_changliao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=changliao_cookie)) + + elif record_url.find("ybw1666.com/") > -1: + platform = '音播直播' + with semaphore: + port_info = asyncio.run(spider.get_yinbo_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yinbo_cookie)) + + elif record_url.find("www.inke.cn/") > -1: + platform = '映客直播' + with semaphore: + port_info = asyncio.run(spider.get_yingke_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yingke_cookie)) + + elif record_url.find("www.zhihu.com/") > -1: + platform = '知乎直播' + with semaphore: + port_info = asyncio.run(spider.get_zhihu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=zhihu_cookie)) + + elif record_url.find("chzzk.naver.com/") > -1: + platform = 'CHZZK' + with semaphore: + json_data = asyncio.run(spider.get_chzzk_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=chzzk_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("www.haixiutv.com/") > -1: + platform = '嗨秀直播' + with semaphore: + port_info = asyncio.run(spider.get_haixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=haixiu_cookie)) + + elif record_url.find("vvxqiu.com/") > -1: + platform = 'VV星球' + with semaphore: + port_info = asyncio.run(spider.get_vvxqiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=vvxqiu_cookie)) + + elif record_url.find("17.live/") > -1: + platform = '17Live' + with semaphore: + port_info = asyncio.run(spider.get_17live_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yiqilive_cookie)) + + elif record_url.find("www.lang.live/") > -1: + platform = '浪Live' + with semaphore: + port_info = asyncio.run(spider.get_langlive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=langlive_cookie)) + + elif record_url.find("m.pp.weimipopo.com/") > -1: + platform = '漂漂直播' + with semaphore: + port_info = asyncio.run(spider.get_pplive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=pplive_cookie)) + + elif record_url.find(".6.cn/") > -1: + platform = '六间房直播' + with semaphore: + port_info = asyncio.run(spider.get_6room_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=six_room_cookie)) + + elif record_url.find("lehaitv.com/") > -1: + platform = '乐嗨直播' + with semaphore: + port_info = asyncio.run(spider.get_haixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=lehaitv_cookie)) + + elif record_url.find("h.catshow168.com/") > -1: + platform = '花猫直播' + with semaphore: + port_info = asyncio.run(spider.get_pplive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=huamao_cookie)) + + elif record_url.find("live.shopee") > -1 or record_url.find("shp.ee/") > -1: + platform = 'shopee' + with semaphore: + port_info = asyncio.run(spider.get_shopee_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=shopee_cookie)) + if port_info.get('uid'): + new_record_url = record_url.split('?')[0] + '?' + str(port_info['uid']) + + elif record_url.find("www.youtube.com/") > -1 or record_url.find("youtu.be/") > -1: + platform = 'Youtube' + with semaphore: + json_data = asyncio.run(spider.get_youtube_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=youtube_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("tb.cn") > -1: + platform = '淘宝直播' + with semaphore: + json_data = asyncio.run(spider.get_taobao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=taobao_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, + url_type='all', hls_extra_key='hlsUrl', flv_extra_key='flvUrl' + )) + + elif record_url.find("3.cn") > -1 or record_url.find("m.jd.com") > -1: + platform = '京东直播' + with semaphore: + port_info = asyncio.run(spider.get_jd_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=jd_cookie)) + + elif record_url.find("faceit.com/") > -1: + platform = 'faceit' + with semaphore: + if global_proxy or proxy_address: + with semaphore: + json_data = asyncio.run(spider.get_faceit_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=faceit_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问faceit直播平台") + + elif record_url.find("www.miguvideo.com") > -1 or record_url.find("m.miguvideo.com") > -1: + platform = '咪咕直播' + with semaphore: + port_info = asyncio.run(spider.get_migu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=migu_cookie)) + + elif record_url.find("show.lailianjie.com") > -1: + platform = '连接直播' + with semaphore: + port_info = asyncio.run(spider.get_lianjie_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=lianjie_cookie)) + + elif record_url.find("www.imkktv.com") > -1: + platform = '来秀直播' + with semaphore: + port_info = asyncio.run(spider.get_laixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=laixiu_cookie)) + + elif record_url.find("www.picarto.tv") > -1: + platform = 'Picarto' + with semaphore: + port_info = asyncio.run(spider.get_picarto_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=picarto_cookie)) + + elif record_url.find(".m3u8") > -1 or record_url.find(".flv") > -1: + platform = '自定义录制直播' + port_info = { + "anchor_name": platform + '_' + str(uuid.uuid4())[:8], + "is_live": True, + "record_url": record_url, + } + if '.flv' in record_url: + port_info['flv_url'] = record_url + else: + port_info['m3u8_url'] = record_url + + else: + logger.error(f'{record_url} {platform}直播地址') + return + + if anchor_name: + if '主播:' in anchor_name: + anchor_split: list = anchor_name.split('主播:') + if len(anchor_split) > 1 and anchor_split[1].strip(): + anchor_name = anchor_split[1].strip() + else: + anchor_name = port_info.get("anchor_name", '') + else: + anchor_name = port_info.get("anchor_name", '') + + if not port_info.get("anchor_name", ''): + print(f'序号{count_variable} 网址内容获取失败,进行重试中...获取失败的地址是:{url_data}') + with max_request_lock: + error_count += 1 + error_window.append(1) + else: + anchor_name = clean_name(anchor_name) + record_name = f'序号{count_variable} {anchor_name}' + + if record_url in url_comments: + print(f"[{anchor_name}]已被注释,本条线程将会退出") + clear_record_info(record_name, record_url) + return + + if not url_data[-1] and run_once is False: + if new_record_url: + need_update_line_list.append( + f'{record_url}|{new_record_url},主播: {anchor_name.strip()}') + not_record_list.append(new_record_url) + else: + need_update_line_list.append(f'{record_url}|{record_url},主播: {anchor_name.strip()}') + run_once = True + + push_at = datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S') + if port_info['is_live'] is False: + print(f"\r{record_name} 等待直播... ") + + if start_pushed: + if over_show_push: + push_content = "直播间状态更新:[直播间名称] 直播已结束!时间:[时间]" + if over_push_message_text: + push_content = over_push_message_text + + push_content = (push_content.replace('[直播间名称]', record_name). + replace('[时间]', push_at)) + threading.Thread( + target=push_message, + args=(record_name, record_url, push_content.replace(r'\n', '\n')), + daemon=True + ).start() + start_pushed = False + + else: + content = f"\r{record_name} 正在直播中..." + print(content) + + if live_status_push and not start_pushed: + if begin_show_push: + push_content = "直播间状态更新:[直播间名称] 正在直播中,时间:[时间]" + if begin_push_message_text: + push_content = begin_push_message_text + + push_content = (push_content.replace('[直播间名称]', record_name). + replace('[时间]', push_at)) + threading.Thread( + target=push_message, + args=(record_name, record_url, push_content.replace(r'\n', '\n')), + daemon=True + ).start() + start_pushed = True + + if disable_record: + time.sleep(push_check_seconds) + continue + + real_url = port_info.get('record_url') + full_path = f'{default_path}/{platform}' + if real_url: + now = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S") + live_title = port_info.get('title') + title_in_name = '' + if live_title: + live_title = clean_name(live_title) + title_in_name = live_title + '_' if filename_by_title else '' + + try: + if len(video_save_path) > 0: + if not video_save_path.endswith(('/', '\\')): + full_path = f'{video_save_path}/{platform}' + else: + full_path = f'{video_save_path}{platform}' + + full_path = full_path.replace("\\", '/') + if folder_by_author: + full_path = f'{full_path}/{anchor_name}' + if folder_by_time: + full_path = f'{full_path}/{now[:10]}' + if folder_by_title and port_info.get('title'): + if folder_by_time: + full_path = f'{full_path}/{live_title}_{anchor_name}' + else: + full_path = f'{full_path}/{now[:10]}_{live_title}' + if not os.path.exists(full_path): + os.makedirs(full_path) + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + + if platform != '自定义录制直播': + if enable_https_recording and real_url.startswith("http://"): + real_url = real_url.replace("http://", "https://") + + http_record_list = ['shopee'] + if platform in http_record_list: + real_url = real_url.replace("https://", "http://") + + start_record_and_push(real_url, youtube_key) + + 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") + + rw_timeout = "15000000" + analyzeduration = "20000000" + probesize = "10000000" + bufsize = "8000k" + max_muxing_queue_size = "1024" + for pt_host in overseas_platform_host: + if pt_host in record_url: + rw_timeout = "50000000" + analyzeduration = "40000000" + probesize = "20000000" + bufsize = "15000k" + max_muxing_queue_size = "2048" + break + + ffmpeg_command = [ + 'ffmpeg', "-y", + "-v", "verbose", + "-rw_timeout", rw_timeout, + "-loglevel", "error", + "-hide_banner", + "-user_agent", user_agent, + "-protocol_whitelist", "rtmp,crypto,file,http,https,tcp,tls,udp,rtp,httpproxy", + "-thread_queue_size", "1024", + "-analyzeduration", analyzeduration, + "-probesize", probesize, + "-fflags", "+discardcorrupt", + "-re", "-i", real_url, + # 视频编码 & 音频编码 + # "-c:v", "libx264", "-preset", "veryfast", + # "-c:a", "aac", + + # 输出到 YouTube + # "-f", "flv", "rtmp://a.rtmp.youtube.com/live2/9km5-20gc-9db9-mp7p-ecsg", + "-bufsize", bufsize, + "-sn", "-dn", + "-reconnect_delay_max", "60", + "-reconnect_streamed", "-reconnect_at_eof", + "-max_muxing_queue_size", max_muxing_queue_size, + "-correct_ts_overflow", "1", + "-avoid_negative_ts", "1" + ] + + + + # ffmpeg_command = [ + # "ffmpeg", "-re", "-i", real_url, + + # # 视频编码 + # "-c:v", "libx264", + # "-preset", "veryfast", + # "-b:v", "5000k", + # "-maxrate", "5000k", + # "-bufsize", "10000k", + + # # 音频编码 + # "-c:a", "aac", + # "-b:a", "192k", + + # # 容错 & 时间戳修复 + # "-fflags", "+genpts+discardcorrupt", + # "-avoid_negative_ts", "1", + + # # HLS 重连 + # "-rw_timeout", "10000000", + # "-reconnect", "1", + # "-reconnect_at_eof", "1", + # "-reconnect_streamed", "1", + # "-thread_queue_size", "1024", + # "-analyzeduration", "1000000", + # "-probesize", "5000000", + + # # 输出到 YouTube + # "-f", "flv", + # "rtmp://a.rtmp.youtube.com/live2/9km5-20gc-9db9-mp7p-ecsg" + # ] + + + + + record_headers = { + 'PandaTV': 'origin:https://www.pandalive.co.kr', + 'WinkTV': 'origin:https://www.winktv.co.kr', + 'PopkonTV': 'origin:https://www.popkontv.com', + 'FlexTV': 'origin:https://www.flextv.co.kr', + '千度热播': 'referer:https://qiandurebo.com', + '17Live': 'referer:https://17.live/en/live/6302408', + '浪Live': 'referer:https://www.lang.live', + 'shopee': f'origin:{live_domain}', + 'Blued直播': 'referer:https://app.blued.cn' + } + + headers = record_headers.get(platform) + if headers: + ffmpeg_command.insert(11, "-headers") + ffmpeg_command.insert(12, headers) + + if proxy_address: + ffmpeg_command.insert(1, "-http_proxy") + ffmpeg_command.insert(2, proxy_address) + + recording.add(record_name) + start_record_time = datetime.datetime.now() + recording_time_list[record_name] = [start_record_time, record_quality_zh] + rec_info = f"\r{anchor_name} 准备开始录制视频: {full_path}" + if show_url: + re_plat = ('WinkTV', 'PandaTV', 'ShowRoom', 'CHZZK', 'Youtube') + if platform in re_plat: + logger.info(f"{platform} | {anchor_name} | 直播源地址: {port_info['m3u8_url']}") + else: + logger.info( + f"{platform} | {anchor_name} | 直播源地址: {real_url}") + + only_flv_record = False + only_flv_platform_list = ['shopee', '花椒直播'] + if platform in only_flv_platform_list: + logger.debug(f"提示: {platform} 将强制使用FLV格式录制") + only_flv_record = True + + only_audio_record = False + only_audio_platform_list = ['猫耳FM直播', 'Look直播'] + if platform in only_audio_platform_list: + only_audio_record = True + + if only_audio_record: + try: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + extension = "mp3" if "m4a" not in video_save_type.lower () else "m4a" + name_format = "_%03d" if split_video_by_time else "" + save_file_path = (f"{full_path}/{anchor_name}_{title_in_name}{now}" + f"{name_format}.{extension}") + + if split_video_by_time: + print(f'\r{anchor_name} 准备开始录制音频: {save_file_path}') + + if "MP3" in video_save_type: + command = [ + "-map", "0:a", + "-c:a", "libmp3lame", + "-ab", "320k", + "-f", "segment", + "-segment_time", split_time, + "-reset_timestamps", "1", + save_file_path, + ] + else: + command = [ + "-map", "0:a", + "-c:a", "aac", + "-bsf:a", "aac_adtstoasc", + "-ab", "320k", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", 'mpegts', + "-reset_timestamps", "1", + save_file_path, + ] + + else: + if "MP3" in video_save_type: + command = [ + "-map", "0:a", + "-c:a", "libmp3lame", + "-ab", "320k", + save_file_path, + ] + + else: + command = [ + "-map", "0:a", + "-c:a", "aac", + "-bsf:a", "aac_adtstoasc", + "-ab", "320k", + "-movflags", "+faststart", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + video_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + if video_save_type == "FLV" or only_flv_record: + filename = anchor_name + f'_{title_in_name}' + now + '.flv' + save_file_path = f'{full_path}/{filename}' + print(f'{rec_info}/{filename}') + + subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] + subs_thread_name = f'subs_{Path(subs_file_path).name}' + if create_time_file: + create_var[subs_thread_name] = threading.Thread( + target=generate_subtitles, args=(record_name, subs_file_path) + ) + create_var[subs_thread_name].daemon = True + create_var[subs_thread_name].start() + + try: + flv_url = port_info.get('flv_url') + if flv_url: + _filepath, _ = urllib.request.urlretrieve(flv_url, save_file_path) + record_finished = True + recording.discard(record_name) + print( + f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制完成\n") + else: + logger.debug("未找到FLV直播流,跳过录制") + except Exception as e: + clear_record_info(record_name, record_url) + color_obj.print_colored( + f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制出错,请检查网络\n", + color_obj.RED) + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + try: + if converts_to_mp4: + seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mp4" + if split_video_by_time: + segment_video( + save_file_path, seg_file_path, + segment_format='mp4', segment_time=split_time, + is_original_delete=delete_origin_file + ) + else: + threading.Thread( + target=converts_mp4, + args=(save_file_path, delete_origin_file) + ).start() + + else: + seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.flv" + if split_video_by_time: + segment_video( + save_file_path, seg_file_path, + segment_format='flv', segment_time=split_time, + is_original_delete=delete_origin_file + ) + except Exception as e: + logger.error(f"转码失败: {e} ") + + elif video_save_type == "MKV": + filename = anchor_name + f'_{title_in_name}' + now + ".mkv" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mkv" + command = [ + "-flags", "global_header", + "-c:v", "copy", + "-c:a", "aac", + "-map", "0", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "matroska", + "-reset_timestamps", "1", + save_file_path, + ] + + else: + command = [ + "-flags", "global_header", + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-f", "matroska", + "{path}".format(path=save_file_path), + ] + ffmpeg_command.extend(command) + + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + video_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + elif video_save_type == "MP4": + filename = anchor_name + f'_{title_in_name}' + now + ".mp4" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mp4" + command = [ + "-c:v", "copy", + "-c:a", "aac", + "-map", "0", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "mp4", + "-reset_timestamps", "1", + "-movflags", "+frag_keyframe+empty_moov", + save_file_path, + ] + + else: + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-f", "mp4", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + video_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + else: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + filename = anchor_name + f'_{title_in_name}' + now + ".ts" + print(f'{rec_info}/{filename}') + + try: + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.ts" + command = [ + "-c:v", "copy", + "-c:a", "copy", + "-map", "0", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", 'mpegts', + "-reset_timestamps", "1", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + video_save_type, + custom_script + ) + if comment_end: + if converts_to_mp4: + file_paths = utils.get_file_paths(os.path.dirname(save_file_path)) + prefix = os.path.basename(save_file_path).rsplit('_', maxsplit=1)[0] + for path in file_paths: + if prefix in path: + try: + threading.Thread( + target=converts_mp4, + args=(path, delete_origin_file) + ).start() + except subprocess.CalledProcessError as e: + logger.error(f"转码失败: {e} ") + return + + except subprocess.CalledProcessError as e: + logger.error( + f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + else: + filename = anchor_name + f'_{title_in_name}' + now + ".ts" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + command = [ + "-c:v", "copy", + "-c:a", "copy", + "-map", "0", + "-f", "mpegts", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + video_save_type, + custom_script + ) + if comment_end: + threading.Thread( + target=converts_mp4, args=(save_file_path, delete_origin_file) + ).start() + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + count_time = time.time() + + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + num = random.randint(-5, 5) + delay_default + if num < 0: + num = 0 + x = num + + if error_count > 20: + x = x + 60 + color_obj.print_colored("\r瞬时错误太多,延迟加60秒", color_obj.YELLOW) + + # 这里是.如果录制结束后,循环时间会暂时变成30s后检测一遍. 这样一定程度上防止主播卡顿造成少录 + # 当30秒过后检测一遍后. 会回归正常设置的循环秒数 + if record_finished: + count_time_end = time.time() - count_time + if count_time_end < 60: + x = 30 + record_finished = False + + else: + x = num + + # 这里是正常循环 + while x: + x = x - 1 + if loop_time: + print(f'\r{anchor_name}循环等待{x}秒 ', end="") + time.sleep(1) + if loop_time: + print('\r检测直播间中...', end="") + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + time.sleep(2) + + +def backup_file(file_path: str, backup_dir_path: str, limit_counts: int = 6) -> None: + try: + if not os.path.exists(backup_dir_path): + os.makedirs(backup_dir_path) + + timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backup_file_name = os.path.basename(file_path) + '_' + timestamp + backup_file_path = os.path.join(backup_dir_path, backup_file_name).replace("\\", "/") + shutil.copy2(file_path, backup_file_path) + + files = os.listdir(backup_dir_path) + _files = [f for f in files if f.startswith(os.path.basename(file_path))] + _files.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir_path, x))) + + while len(_files) > limit_counts: + oldest_file = _files[0] + os.remove(os.path.join(backup_dir_path, oldest_file)) + _files = _files[1:] + + except Exception as e: + logger.error(f'\r备份配置文件 {file_path} 失败:{str(e)}') + + +def backup_file_start() -> None: + config_md5 = '' + url_config_md5 = '' + + while True: + try: + if os.path.exists(config_file): + new_config_md5 = utils.check_md5(config_file) + if new_config_md5 != config_md5: + backup_file(config_file, backup_dir) + config_md5 = new_config_md5 + + if os.path.exists(url_config_file): + new_url_config_md5 = utils.check_md5(url_config_file) + if new_url_config_md5 != url_config_md5: + backup_file(url_config_file, backup_dir) + url_config_md5 = new_url_config_md5 + time.sleep(600) + except Exception as e: + logger.error(f"备份配置文件失败, 错误信息: {e}") + + +def check_ffmpeg_existence() -> bool: + try: + result = subprocess.run(['ffmpeg', '-version'], check=True, capture_output=True, text=True) + if result.returncode == 0: + lines = result.stdout.splitlines() + version_line = lines[0] + built_line = lines[1] + print(version_line) + print(built_line) + except subprocess.CalledProcessError as e: + logger.error(e) + except FileNotFoundError: + pass + finally: + if check_ffmpeg(): + time.sleep(1) + return True + return False + + +# --------------------------初始化程序------------------------------------- +print("-----------------------------------------------------") +print("| DouyinLiveRecorder |") +print("-----------------------------------------------------") + +print(f"版本号: {version}") +print("GitHub: https://github.com/ihmily/DouyinLiveRecorder") +print(f'支持平台: {platforms}') +print('.....................................................') +if not check_ffmpeg_existence(): + logger.error("缺少ffmpeg无法进行录制,程序退出") + sys.exit(1) +os.makedirs(os.path.dirname(config_file), exist_ok=True) +t3 = threading.Thread(target=backup_file_start, args=(), daemon=True) +t3.start() +utils.remove_duplicate_lines(url_config_file) + + +def read_config_value(config_parser: configparser.RawConfigParser, section: str, option: str, default_value: Any) \ + -> Any: + try: + + config_parser.read(config_file, encoding=text_encoding) + if '录制设置' not in config_parser.sections(): + config_parser.add_section('录制设置') + if '推送配置' not in config_parser.sections(): + config_parser.add_section('推送配置') + if 'Cookie' not in config_parser.sections(): + config_parser.add_section('Cookie') + if 'Authorization' not in config_parser.sections(): + config_parser.add_section('Authorization') + if '账号密码' not in config_parser.sections(): + config_parser.add_section('账号密码') + return config_parser.get(section, option) + except (configparser.NoSectionError, configparser.NoOptionError): + config_parser.set(section, option, str(default_value)) + with open(config_file, 'w', encoding=text_encoding) as f: + config_parser.write(f) + return default_value + + +options = {"是": True, "否": False} +config = configparser.RawConfigParser() +language = read_config_value(config, '录制设置', 'language(zh_cn/en)', "zh_cn") +skip_proxy_check = options.get(read_config_value(config, '录制设置', '是否跳过代理检测(是/否)', "否"), False) +if language and 'en' not in language.lower(): + from i18n import translated_print + + builtins.print = translated_print + +try: + if skip_proxy_check: + global_proxy = True + else: + print('系统代理检测中,请耐心等待...') + response_g = urllib.request.urlopen("https://www.google.com/", timeout=15) + global_proxy = True + print('\r全局/规则网络代理已开启√') + pd = ProxyDetector() + if pd.is_proxy_enabled(): + proxy_info = pd.get_proxy_info() + print("System Proxy: http://{}:{}".format(proxy_info.ip, proxy_info.port)) +except HTTPError as err: + print(f"HTTP error occurred: {err.code} - {err.reason}") +except URLError: + color_obj.print_colored("INFO:未检测到全局/规则网络代理,请检查代理配置(若无需录制海外直播请忽略此条提示)", + color_obj.YELLOW) +except Exception as err: + print("An unexpected error occurred:", err) + +while True: + + try: + if not os.path.isfile(config_file): + with open(config_file, 'w', encoding=text_encoding) as file: + pass + + ini_URL_content = '' + if os.path.isfile(url_config_file): + with open(url_config_file, 'r', encoding=text_encoding) as file: + ini_URL_content = file.read().strip() + + if not ini_URL_content.strip(): + input_url = input('请输入要录制的主播直播间网址(尽量使用PC网页端的直播间地址):\n') + with open(url_config_file, 'w', encoding=text_encoding) as file: + file.write(input_url) + except OSError as err: + logger.error(f"发生 I/O 错误: {err}") + + video_save_path = read_config_value(config, '录制设置', '直播保存路径(不填则默认)', "") + folder_by_author = options.get(read_config_value(config, '录制设置', '保存文件夹是否以作者区分', "是"), False) + folder_by_time = options.get(read_config_value(config, '录制设置', '保存文件夹是否以时间区分', "否"), False) + folder_by_title = options.get(read_config_value(config, '录制设置', '保存文件夹是否以标题区分', "否"), False) + filename_by_title = options.get(read_config_value(config, '录制设置', '保存文件名是否包含标题', "否"), False) + clean_emoji = options.get(read_config_value(config, '录制设置', '是否去除名称中的表情符号', "是"), True) + video_save_type = read_config_value(config, '录制设置', '视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频', "ts") + video_record_quality = read_config_value(config, '录制设置', '原画|超清|高清|标清|流畅', "原画") + use_proxy = options.get(read_config_value(config, '录制设置', '是否使用代理ip(是/否)', "是"), False) + proxy_addr_bak = read_config_value(config, '录制设置', '代理地址', "") + proxy_addr = None if not use_proxy else proxy_addr_bak + max_request = int(read_config_value(config, '录制设置', '同一时间访问网络的线程数', 3)) + semaphore = threading.Semaphore(max_request) + delay_default = int(read_config_value(config, '录制设置', '循环时间(秒)', 120)) + local_delay_default = int(read_config_value(config, '录制设置', '排队读取网址时间(秒)', 0)) + loop_time = options.get(read_config_value(config, '录制设置', '是否显示循环秒数', "否"), False) + show_url = options.get(read_config_value(config, '录制设置', '是否显示直播源地址', "否"), False) + split_video_by_time = options.get(read_config_value(config, '录制设置', '分段录制是否开启', "否"), False) + enable_https_recording = options.get(read_config_value(config, '录制设置', '是否强制启用https录制', "否"), False) + disk_space_limit = float(read_config_value(config, '录制设置', '录制空间剩余阈值(gb)', 1.0)) + split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 1800)) + converts_to_mp4 = options.get(read_config_value(config, '录制设置', '录制完成后自动转为mp4格式', "否"), False) + converts_to_h264 = options.get(read_config_value(config, '录制设置', 'mp4格式重新编码为h264', "否"), False) + delete_origin_file = options.get(read_config_value(config, '录制设置', '追加格式后删除原文件', "否"), False) + create_time_file = options.get(read_config_value(config, '录制设置', '生成时间字幕文件', "否"), False) + is_run_script = options.get(read_config_value(config, '录制设置', '是否录制完成后执行自定义脚本', "否"), False) + custom_script = read_config_value(config, '录制设置', '自定义脚本执行命令', "") if is_run_script else None + enable_proxy_platform = read_config_value( + config, '录制设置', '使用代理录制的平台(逗号分隔)', + 'tiktok, soop, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu, faceit' + ) + enable_proxy_platform_list = enable_proxy_platform.replace(',', ',').split(',') if enable_proxy_platform else None + extra_enable_proxy = read_config_value(config, '录制设置', '额外使用代理录制的平台(逗号分隔)', '') + extra_enable_proxy_platform_list = extra_enable_proxy.replace(',', ',').split(',') if extra_enable_proxy else None + live_status_push = read_config_value(config, '推送配置', '直播状态推送渠道', "") + dingtalk_api_url = read_config_value(config, '推送配置', '钉钉推送接口链接', "") + xizhi_api_url = read_config_value(config, '推送配置', '微信推送接口链接', "") + bark_msg_api = read_config_value(config, '推送配置', 'bark推送接口链接', "") + bark_msg_level = read_config_value(config, '推送配置', 'bark推送中断级别', "active") + bark_msg_ring = read_config_value(config, '推送配置', 'bark推送铃声', "bell") + dingtalk_phone_num = read_config_value(config, '推送配置', '钉钉通知@对象(填手机号)', "") + dingtalk_is_atall = options.get(read_config_value(config, '推送配置', '钉钉通知@全体(是/否)', "否"), False) + tg_token = read_config_value(config, '推送配置', 'tgapi令牌', "") + tg_chat_id = read_config_value(config, '推送配置', 'tg聊天id(个人或者群组id)', "") + email_host = read_config_value(config, '推送配置', 'SMTP邮件服务器', "") + open_smtp_ssl = options.get(read_config_value(config, '推送配置', '是否使用SMTP服务SSL加密(是/否)', "是"), True) + smtp_port = read_config_value(config, '推送配置', 'SMTP邮件服务器端口', "") + login_email = read_config_value(config, '推送配置', '邮箱登录账号', "") + email_password = read_config_value(config, '推送配置', '发件人密码(授权码)', "") + sender_email = read_config_value(config, '推送配置', '发件人邮箱', "") + sender_name = read_config_value(config, '推送配置', '发件人显示昵称', "") + to_email = read_config_value(config, '推送配置', '收件人邮箱', "") + ntfy_api = read_config_value(config, '推送配置', 'ntfy推送地址', "") + ntfy_tags = read_config_value(config, '推送配置', 'ntfy推送标签', "tada") + ntfy_email = read_config_value(config, '推送配置', 'ntfy推送邮箱', "") + pushplus_token = read_config_value(config, '推送配置', 'pushplus推送token', "") + push_message_title = read_config_value(config, '推送配置', '自定义推送标题', "直播间状态更新通知") + begin_push_message_text = read_config_value(config, '推送配置', '自定义开播推送内容', "") + over_push_message_text = read_config_value(config, '推送配置', '自定义关播推送内容', "") + disable_record = options.get(read_config_value(config, '推送配置', '只推送通知不录制(是/否)', "否"), False) + push_check_seconds = int(read_config_value(config, '推送配置', '直播推送检测频率(秒)', 1800)) + begin_show_push = options.get(read_config_value(config, '推送配置', '开播推送开启(是/否)', "是"), True) + over_show_push = options.get(read_config_value(config, '推送配置', '关播推送开启(是/否)', "否"), False) + sooplive_username = read_config_value(config, '账号密码', 'sooplive账号', '') + sooplive_password = read_config_value(config, '账号密码', 'sooplive密码', '') + flextv_username = read_config_value(config, '账号密码', 'flextv账号', '') + flextv_password = read_config_value(config, '账号密码', 'flextv密码', '') + popkontv_username = read_config_value(config, '账号密码', 'popkontv账号', '') + popkontv_partner_code = read_config_value(config, '账号密码', 'partner_code', 'P-00001') + popkontv_password = read_config_value(config, '账号密码', 'popkontv密码', '') + twitcasting_account_type = read_config_value(config, '账号密码', 'twitcasting账号类型', 'normal') + twitcasting_username = read_config_value(config, '账号密码', 'twitcasting账号', '') + twitcasting_password = read_config_value(config, '账号密码', 'twitcasting密码', '') + popkontv_access_token = read_config_value(config, 'Authorization', 'popkontv_token', '') + dy_cookie = read_config_value(config, 'Cookie', '抖音cookie', '') + ks_cookie = read_config_value(config, 'Cookie', '快手cookie', '') + tiktok_cookie = read_config_value(config, 'Cookie', 'tiktok_cookie', '') + hy_cookie = read_config_value(config, 'Cookie', '虎牙cookie', '') + douyu_cookie = read_config_value(config, 'Cookie', '斗鱼cookie', '') + yy_cookie = read_config_value(config, 'Cookie', 'yy_cookie', '') + bili_cookie = read_config_value(config, 'Cookie', 'B站cookie', '') + xhs_cookie = read_config_value(config, 'Cookie', '小红书cookie', '') + bigo_cookie = read_config_value(config, 'Cookie', 'bigo_cookie', '') + blued_cookie = read_config_value(config, 'Cookie', 'blued_cookie', '') + sooplive_cookie = read_config_value(config, 'Cookie', 'sooplive_cookie', '') + netease_cookie = read_config_value(config, 'Cookie', 'netease_cookie', '') + qiandurebo_cookie = read_config_value(config, 'Cookie', '千度热播_cookie', '') + pandatv_cookie = read_config_value(config, 'Cookie', 'pandatv_cookie', '') + maoerfm_cookie = read_config_value(config, 'Cookie', '猫耳fm_cookie', '') + winktv_cookie = read_config_value(config, 'Cookie', 'winktv_cookie', '') + flextv_cookie = read_config_value(config, 'Cookie', 'flextv_cookie', '') + look_cookie = read_config_value(config, 'Cookie', 'look_cookie', '') + twitcasting_cookie = read_config_value(config, 'Cookie', 'twitcasting_cookie', '') + baidu_cookie = read_config_value(config, 'Cookie', 'baidu_cookie', '') + weibo_cookie = read_config_value(config, 'Cookie', 'weibo_cookie', '') + kugou_cookie = read_config_value(config, 'Cookie', 'kugou_cookie', '') + twitch_cookie = read_config_value(config, 'Cookie', 'twitch_cookie', '') + liveme_cookie = read_config_value(config, 'Cookie', 'liveme_cookie', '') + huajiao_cookie = read_config_value(config, 'Cookie', 'huajiao_cookie', '') + liuxing_cookie = read_config_value(config, 'Cookie', 'liuxing_cookie', '') + showroom_cookie = read_config_value(config, 'Cookie', 'showroom_cookie', '') + acfun_cookie = read_config_value(config, 'Cookie', 'acfun_cookie', '') + changliao_cookie = read_config_value(config, 'Cookie', 'changliao_cookie', '') + yinbo_cookie = read_config_value(config, 'Cookie', 'yinbo_cookie', '') + yingke_cookie = read_config_value(config, 'Cookie', 'yingke_cookie', '') + zhihu_cookie = read_config_value(config, 'Cookie', 'zhihu_cookie', '') + chzzk_cookie = read_config_value(config, 'Cookie', 'chzzk_cookie', '') + haixiu_cookie = read_config_value(config, 'Cookie', 'haixiu_cookie', '') + vvxqiu_cookie = read_config_value(config, 'Cookie', 'vvxqiu_cookie', '') + yiqilive_cookie = read_config_value(config, 'Cookie', '17live_cookie', '') + langlive_cookie = read_config_value(config, 'Cookie', 'langlive_cookie', '') + pplive_cookie = read_config_value(config, 'Cookie', 'pplive_cookie', '') + six_room_cookie = read_config_value(config, 'Cookie', '6room_cookie', '') + lehaitv_cookie = read_config_value(config, 'Cookie', 'lehaitv_cookie', '') + huamao_cookie = read_config_value(config, 'Cookie', 'huamao_cookie', '') + shopee_cookie = read_config_value(config, 'Cookie', 'shopee_cookie', '') + youtube_cookie = read_config_value(config, 'Cookie', 'youtube_cookie', '') + taobao_cookie = read_config_value(config, 'Cookie', 'taobao_cookie', '') + jd_cookie = read_config_value(config, 'Cookie', 'jd_cookie', '') + faceit_cookie = read_config_value(config, 'Cookie', 'faceit_cookie', '') + migu_cookie = read_config_value(config, 'Cookie', 'migu_cookie', '') + lianjie_cookie = read_config_value(config, 'Cookie', 'lianjie_cookie', '') + laixiu_cookie = read_config_value(config, 'Cookie', 'laixiu_cookie', '') + picarto_cookie = read_config_value(config, 'Cookie', 'picarto_cookie', '') + + video_save_type_list = ("FLV", "MKV", "TS", "MP4", "MP3音频", "M4A音频") + if video_save_type and video_save_type.upper() in video_save_type_list: + video_save_type = video_save_type.upper() + else: + video_save_type = "TS" + + check_path = video_save_path or default_path + if utils.check_disk_capacity(check_path, show=first_run) < disk_space_limit: + exit_recording = True + if not recording: + logger.warning(f"Disk space remaining is below {disk_space_limit} GB. " + f"Exiting program due to the disk space limit being reached.") + sys.exit(-1) + + + def contains_url(string: str) -> bool: + pattern = r"(https?://)?(www\.)?[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(:\d+)?(/.*)?" + return re.search(pattern, string) is not None + + + try: + url_comments, line_list, url_line_list = [[] for _ in range(3)] + with (open(url_config_file, "r", encoding=text_encoding, errors='ignore') as file): + for origin_line in file: + if origin_line in line_list: + delete_line(url_config_file, origin_line) + line_list.append(origin_line) + line = origin_line.strip() + if len(line) < 18: + continue + + line_spilt = line.split('主播: ') + if len(line_spilt) > 2: + line = update_file(url_config_file, line, f'{line_spilt[0]}主播: {line_spilt[-1]}') + + is_comment_line = line.startswith("#") + if is_comment_line: + line = line.lstrip('#') + + if re.search('[,,]', line): + split_line = re.split('[,,]', line) + else: + split_line = [line, ''] + + if len(split_line) == 1: + url = split_line[0] + quality, name = [video_record_quality, ''] + elif len(split_line) == 2: + if contains_url(split_line[0]): + quality = video_record_quality + url, name = split_line + else: + quality, url = split_line + name = '' + else: + quality, url, name = split_line + + if quality not in ("原画", "蓝光", "超清", "高清", "标清", "流畅"): + quality = '原画' + + if url not in url_line_list: + url_line_list.append(url) + else: + delete_line(url_config_file, origin_line) + + url = 'https://' + url if '://' not in url else url + url_host = url.split('/')[2] + + platform_host = [ + 'live.douyin.com', + 'v.douyin.com', + 'www.douyin.com', + 'live.kuaishou.com', + 'www.huya.com', + 'www.douyu.com', + 'www.yy.com', + 'live.bilibili.com', + 'www.redelight.cn', + 'www.xiaohongshu.com', + 'xhslink.com', + 'www.bigo.tv', + 'slink.bigovideo.tv', + 'app.blued.cn', + 'cc.163.com', + 'qiandurebo.com', + 'fm.missevan.com', + 'look.163.com', + 'twitcasting.tv', + 'live.baidu.com', + 'weibo.com', + 'fanxing.kugou.com', + 'fanxing2.kugou.com', + 'mfanxing.kugou.com', + 'www.huajiao.com', + 'www.7u66.com', + 'wap.7u66.com', + 'live.acfun.cn', + 'm.acfun.cn', + 'live.tlclw.com', + 'wap.tlclw.com', + 'live.ybw1666.com', + 'wap.ybw1666.com', + 'www.inke.cn', + 'www.zhihu.com', + 'www.haixiutv.com', + "h5webcdnp.vvxqiu.com", + "17.live", + 'www.lang.live', + "m.pp.weimipopo.com", + "v.6.cn", + "m.6.cn", + 'www.lehaitv.com', + 'h.catshow168.com', + 'e.tb.cn', + 'huodong.m.taobao.com', + '3.cn', + 'eco.m.jd.com', + 'www.miguvideo.com', + 'm.miguvideo.com', + 'show.lailianjie.com', + 'www.imkktv.com', + 'www.picarto.tv' + ] + overseas_platform_host = [ + 'www.tiktok.com', + 'play.sooplive.co.kr', + 'm.sooplive.co.kr', + 'www.pandalive.co.kr', + 'www.winktv.co.kr', + 'www.flextv.co.kr', + 'www.popkontv.com', + 'www.twitch.tv', + 'www.liveme.com', + 'www.showroom-live.com', + 'chzzk.naver.com', + 'm.chzzk.naver.com', + 'live.shopee.', + '.shp.ee', + 'www.youtube.com', + 'youtu.be', + 'www.faceit.com' + ] + + platform_host.extend(overseas_platform_host) + clean_url_host_list = ( + "live.douyin.com", + "live.bilibili.com", + "www.huajiao.com", + "www.zhihu.com", + "www.huya.com", + "chzzk.naver.com", + "www.liveme.com", + "www.haixiutv.com", + "v.6.cn", + "m.6.cn", + 'www.lehaitv.com' + ) + + if 'live.shopee.' in url_host or '.shp.ee' in url_host: + url_host = 'live.shopee.' if 'live.shopee.' in url_host else '.shp.ee' + + if url_host in platform_host or any(ext in url for ext in (".flv", ".m3u8")): + if url_host in clean_url_host_list: + url = update_file(url_config_file, old_str=url, new_str=url.split('?')[0]) + + if 'xiaohongshu' in url: + host_id = re.search('&host_id=(.*?)(?=&|$)', url) + if host_id: + new_url = url.split('?')[0] + f'?host_id={host_id.group(1)}' + url = update_file(url_config_file, old_str=url, new_str=new_url) + + url_comments = [i for i in url_comments if url not in i] + if is_comment_line: + url_comments.append(url) + else: + new_line = (quality, url, name) + url_tuples_list.append(new_line) + else: + if not origin_line.startswith('#'): + color_obj.print_colored(f"\r{origin_line.strip()} 本行包含未知链接.此条跳过", color_obj.YELLOW) + update_file(url_config_file, old_str=origin_line, new_str=origin_line, start_str='#') + + while len(need_update_line_list): + a = need_update_line_list.pop() + replace_words = a.split('|') + if replace_words[0] != replace_words[1]: + if replace_words[1].startswith("#"): + start_with = '#' + new_word = replace_words[1][1:] + else: + start_with = None + new_word = replace_words[1] + update_file(url_config_file, old_str=replace_words[0], new_str=new_word, start_str=start_with) + + text_no_repeat_url = list(set(url_tuples_list)) + + if len(text_no_repeat_url) > 0: + for url_tuple in text_no_repeat_url: + monitoring = len(running_list) + + if url_tuple[1] in not_record_list: + continue + + if url_tuple[1] not in running_list: + print(f"\r{'新增' if not first_start else '传入'}地址: {url_tuple[1]}") + monitoring += 1 + args = [url_tuple, monitoring] + create_var[f'thread_{monitoring}'] = threading.Thread(target=start_record, args=args) + create_var[f'thread_{monitoring}'].daemon = True + create_var[f'thread_{monitoring}'].start() + running_list.append(url_tuple[1]) + time.sleep(local_delay_default) + url_tuples_list = [] + first_start = False + + except Exception as err: + logger.error(f"错误信息: {err} 发生错误的行数: {err.__traceback__.tb_lineno}") + + if first_run: + t = threading.Thread(target=display_info, args=(), daemon=True) + t.start() + t2 = threading.Thread(target=adjust_max_request, args=(), daemon=True) + t2.start() + first_run = False + + time.sleep(3) \ No newline at end of file diff --git a/proxy.py b/proxy.py new file mode 100644 index 0000000..85385d6 --- /dev/null +++ b/proxy.py @@ -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) diff --git a/room.py b/room.py new file mode 100644 index 0000000..1724a1a --- /dev/null +++ b/room.py @@ -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) diff --git a/spider.py b/spider.py new file mode 100644 index 0000000..36e645d --- /dev/null +++ b/spider.py @@ -0,0 +1,3232 @@ +# -*- encoding: utf-8 -*- + +""" +Author: Hmily +GitHub: https://github.com/ihmily +Date: 2023-07-15 23:15:00 +Update: 2025-07-19 17:43:00 +Copyright (c) 2023-2025 by Hmily, All Rights Reserved. +Function: Get live stream data. +""" + +import hashlib +import random +import subprocess +import time +import uuid +from operator import itemgetter +import urllib.parse +import urllib.error +from typing import List +import httpx +import ssl +import re +import json +import execjs +import urllib.request +# from . import JS_SCRIPT_PATH +import utils + +from utils import trace_error_decorator, generate_random_string +from logger import script_path +from room import get_sec_user_id, get_unique_id, UnsupportedUrlError +from http_clients.async_http import async_req +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' +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 get_params(url: str, params: str) -> OptionalStr: + parsed_url = urllib.parse.urlparse(url) + query_params = urllib.parse.parse_qs(parsed_url.query) + + if params in query_params: + return query_params[params][0] + + +async def get_play_url_list(m3u8: str, proxy: OptionalStr = None, header: OptionalDict = None, + abroad: bool = False) -> List[str]: + resp = await async_req(url=m3u8, proxy_addr=proxy, headers=header, abroad=abroad) + play_url_list = [] + for i in resp.split('\n'): + if i.startswith('https://'): + play_url_list.append(i.strip()) + if not play_url_list: + for i in resp.split('\n'): + if i.strip().endswith('m3u8'): + play_url_list.append(i.strip()) + bandwidth_pattern = re.compile(r'BANDWIDTH=(\d+)') + bandwidth_list = bandwidth_pattern.findall(resp) + url_to_bandwidth = {url: int(bandwidth) for bandwidth, url in zip(bandwidth_list, play_url_list)} + play_url_list = sorted(play_url_list, key=lambda url: url_to_bandwidth[url], reverse=True) + return play_url_list + + +@trace_error_decorator +async def get_douyin_app_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + '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', + 'Referer': 'https://live.douyin.com/', + 'Cookie': 'ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986' + } + if cookies: + headers['Cookie'] = cookies + + async def get_app_data(room_id: str, sec_uid: str) -> dict: + app_params = { + "verifyFp": "verify_lxj5zv70_7szNlAB7_pxNY_48Vh_ALKF_GA1Uf3yteoOY", + "type_id": "0", + "live_id": "1", + "room_id": room_id, + "sec_user_id": sec_uid, + "version_code": "99.99.99", + "app_id": "1128" + } + api2 = f'https://webcast.amemv.com/webcast/room/reflow/info/?{urllib.parse.urlencode(app_params)}' + json_str2 = await async_req(url=api2, proxy_addr=proxy_addr, headers=headers) + json_data2 = json.loads(json_str2)['data'] + room_data2 = json_data2['room'] + room_data2['anchor_name'] = room_data2['owner']['nickname'] + return room_data2 + + try: + web_rid = url.split('?')[0].split('live.douyin.com/') + if len(web_rid) > 1: + web_rid = web_rid[1] + params = { + "aid": "6383", + "app_name": "douyin_web", + "live_id": "1", + "device_platform": "web", + "language": "zh-CN", + "browser_language": "zh-CN", + "browser_platform": "Win32", + "browser_name": "Chrome", + "browser_version": "116.0.0.0", + "web_rid": web_rid + + } + api = f'https://live.douyin.com/webcast/room/web/enter/?{urllib.parse.urlencode(params)}' + json_str = await async_req(url=api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str)['data'] + room_data = json_data['data'][0] + room_data['anchor_name'] = json_data['user']['nickname'] + else: + try: + data = await get_sec_user_id(url, proxy_addr=proxy_addr) + _room_id, _sec_uid = data + room_data = await get_app_data(_room_id, _sec_uid) + except UnsupportedUrlError: + unique_id = await get_unique_id(url, proxy_addr=proxy_addr) + return await get_douyin_stream_data(f'https://live.douyin.com/{unique_id}') + + if room_data['status'] == 2: + if 'stream_url' not in room_data: + raise RuntimeError( + "The live streaming type or gameplay is not supported on the computer side yet, please use the " + "app to share the link for recording." + ) + live_core_sdk_data = room_data['stream_url']['live_core_sdk_data'] + pull_datas = room_data['stream_url']['pull_datas'] + if live_core_sdk_data: + if pull_datas: + key = list(pull_datas.keys())[0] + json_str = pull_datas[key]['stream_data'] + else: + json_str = live_core_sdk_data['pull_data']['stream_data'] + json_data = json.loads(json_str) + if 'origin' in json_data['data']: + stream_data = live_core_sdk_data['pull_data']['stream_data'] + origin_data = json.loads(stream_data)['data']['origin']['main'] + sdk_params = json.loads(origin_data['sdk_params']) + origin_hls_codec = sdk_params.get('VCodec') or '' + + origin_url_list = json_data['data']['origin']['main'] + origin_m3u8 = {'ORIGIN': origin_url_list["hls"] + '&codec=' + origin_hls_codec} + origin_flv = {'ORIGIN': origin_url_list["flv"] + '&codec=' + origin_hls_codec} + hls_pull_url_map = room_data['stream_url']['hls_pull_url_map'] + flv_pull_url = room_data['stream_url']['flv_pull_url'] + room_data['stream_url']['hls_pull_url_map'] = {**origin_m3u8, **hls_pull_url_map} + room_data['stream_url']['flv_pull_url'] = {**origin_flv, **flv_pull_url} + except Exception as e: + print(f"Error message: {e} Error line: {e.__traceback__.tb_lineno}") + room_data = {'anchor_name': ""} + return room_data + + +@trace_error_decorator +async def get_douyin_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + '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', + 'Referer': 'https://live.douyin.com/', + 'Cookie': 'ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986' + } + if cookies: + headers['Cookie'] = cookies + + try: + origin_url_list = None + html_str = await async_req(url=url, proxy_addr=proxy_addr, headers=headers) + match_json_str = re.search(r'(\{\\"state\\":.*?)]\\n"]\)', html_str) + if not match_json_str: + match_json_str = re.search(r'(\{\\"common\\":.*?)]\\n"]\)

', + html_str, re.DOTALL)[0] + except Exception: + raise ConnectionError("Please check if your network can access the TikTok website normally") + json_data = json.loads(json_str) + return json_data + + +@trace_error_decorator +async def get_kuaishou_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + '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', + } + if cookies: + headers['Cookie'] = cookies + try: + html_str = await async_req(url=url, proxy_addr=proxy_addr, headers=headers) + except Exception as e: + print(f"Failed to fetch data from {url}.{e}") + return {"type": 1, "is_live": False} + + try: + json_str = re.search('', html_str)[0] + json_data = json.loads(json_str) + rid = json_data['pageProps']['room']['roomInfo']['roomInfo']['rid'] + + headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0' + url2 = f'https://www.douyu.com/betard/{rid}' + json_str = await async_req(url=url2, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + result = { + "anchor_name": json_data['room']['nickname'], + "is_live": False + } + if json_data['room']['videoLoop'] == 0 and json_data['room']['show_status'] == 1: + result["title"] = json_data['room']['room_name'].replace(' ', '') + result["is_live"] = True + result["room_id"] = json_data['room']['room_id'] + return result + + +@trace_error_decorator +async def get_douyu_stream_data(rid: str, rate: str = '-1', proxy_addr: OptionalStr = None, + cookies: OptionalStr = None) -> dict: + did = '10000000000000000000000000003306' + params_list = await get_token_js(rid, did, proxy_addr=proxy_addr) + headers = { + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + 'Referer': 'https://m.douyu.com/3125893?rid=3125893&dyshid=0-96003918aa5365bc6dcb4933000316p1&dyshci=181', + 'Cookie': 'dy_did=413b835d2ae00270f0c69f6400031601; acf_did=413b835d2ae00270f0c69f6400031601; Hm_lvt_e99aee90ec1b2106afe7ec3b199020a7=1692068308,1694003758; m_did=96003918aa5365bc6dcb4933000316p1; dy_teen_mode=%7B%22uid%22%3A%22472647365%22%2C%22status%22%3A0%2C%22birthday%22%3A%22%22%2C%22password%22%3A%22%22%7D; PHPSESSID=td59qi2fu2gepngb8mlehbeme3; acf_auth=94fc9s%2FeNj%2BKlpU%2Br8tZC3Jo9sZ0wz9ClcHQ1akL2Nhb6ZyCmfjVWSlR3LFFPuePWHRAMo0dt9vPSCoezkFPOeNy4mYcdVOM1a8CbW0ZAee4ipyNB%2Bflr58; dy_auth=bec5yzM8bUFYe%2FnVAjmUAljyrsX%2FcwRW%2FyMHaoArYb5qi8FS9tWR%2B96iCzSnmAryLOjB3Qbeu%2BBD42clnI7CR9vNAo9mva5HyyL41HGsbksx1tEYFOEwxSI; wan_auth37wan=5fd69ed5b27fGM%2FGoswWwDo%2BL%2FRMtnEa4Ix9a%2FsH26qF0sR4iddKMqfnPIhgfHZUqkAk%2FA1d8TX%2B6F7SNp7l6buIxAVf3t9YxmSso8bvHY0%2Fa6RUiv8; acf_uid=472647365; acf_username=472647365; acf_nickname=%E7%94%A8%E6%88%B776576662; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar%2Fdefault%2F24_; acf_ct=0; acf_ltkid=25305099; acf_biz=1; acf_stk=90754f8ed18f0c24; Hm_lpvt_e99aee90ec1b2106afe7ec3b199020a7=1694003778' + } + if cookies: + headers['Cookie'] = cookies + + data = { + 'v': params_list[0], + 'did': params_list[1], + 'tt': params_list[2], + 'sign': params_list[3], # 10分钟有效期 + 'ver': '22011191', + 'rid': rid, + 'rate': rate, # 0蓝光、3超清、2高清、-1默认 + } + + # app_api = 'https://m.douyu.com/hgapi/livenc/room/getStreamUrl' + app_api = f'https://www.douyu.com/lapi/live/getH5Play/{rid}' + json_str = await async_req(url=app_api, proxy_addr=proxy_addr, headers=headers, data=data) + json_data = json.loads(json_str) + return json_data + + +@trace_error_decorator +async def get_yy_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + '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', + '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', + 'Referer': 'https://www.yy.com/', + 'Cookie': 'hd_newui=0.2103068903976506; hdjs_session_id=0.4929014850884579; hdjs_session_time=1694004002636; hiido_ui=0.923076230899782' + } + if cookies: + headers['Cookie'] = cookies + + html_str = await async_req(url=url, proxy_addr=proxy_addr, headers=headers) + anchor_name = re.search('nick: "(.*?)",\n\\s+logo', html_str).group(1) + cid = re.search('sid : "(.*?)",\n\\s+ssid', html_str, re.DOTALL).group(1) + + data = '{"head":{"seq":1701869217590,"appidstr":"0","bidstr":"121","cidstr":"' + cid + '","sidstr":"' + cid + '","uid64":0,"client_type":108,"client_ver":"5.17.0","stream_sys_ver":1,"app":"yylive_web","playersdk_ver":"5.17.0","thundersdk_ver":"0","streamsdk_ver":"5.17.0"},"client_attribute":{"client":"web","model":"web0","cpu":"","graphics_card":"","os":"chrome","osversion":"0","vsdk_version":"","app_identify":"","app_version":"","business":"","width":"1920","height":"1080","scale":"","client_type":8,"h265":0},"avp_parameter":{"version":1,"client_type":8,"service_type":0,"imsi":0,"send_time":1701869217,"line_seq":-1,"gear":4,"ssl":1,"stream_format":0}}' + data_bytes = data.encode('utf-8') + params = { + "uid": "0", + "cid": cid, + "sid": cid, + "appid": "0", + "sequence": "1701869217590", + "encode": "json" + } + url2 = f'https://stream-manager.yy.com/v3/channel/streams?{urllib.parse.urlencode(params)}' + json_str = await async_req(url=url2, data=data_bytes, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + json_data['anchor_name'] = anchor_name + + params = { + 'uid': '', + 'sid': cid, + 'ssid': cid, + '_': int(time.time() * 1000), + } + detail_api = f'https://www.yy.com/live/detail?{urllib.parse.urlencode(params)}' + json_str2 = await async_req(detail_api, proxy_addr=proxy_addr, headers=headers) + json_data2 = json.loads(json_str2) + json_data['title'] = json_data2['data']['roomName'] + return json_data + + +@trace_error_decorator +async def get_bilibili_room_info_h5(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> str: + 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': '', + 'origin': 'https://live.bilibili.com', + 'referer': 'https://live.bilibili.com/26066074', + } + if cookies: + headers['cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + api = f'https://api.live.bilibili.com/xlive/web-room/v1/index/getH5InfoByRoom?room_id={room_id}' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + room_info = json.loads(json_str) + title = room_info['data']['room_info']['title'] if room_info.get('data') else '' + return title + + +@trace_error_decorator +async def get_bilibili_room_info(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + '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', + } + if cookies: + headers['Cookie'] = cookies + + try: + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + json_str = await async_req(f'https://api.live.bilibili.com/room/v1/Room/room_init?id={room_id}', + proxy_addr=proxy_addr, headers=headers) + room_info = json.loads(json_str) + uid = room_info['data']['uid'] + live_status = True if room_info['data']['live_status'] == 1 else False + + api = f'https://api.live.bilibili.com/live_user/v1/Master/info?uid={uid}' + json_str2 = await async_req(url=api, proxy_addr=proxy_addr, headers=headers) + anchor_info = json.loads(json_str2) + anchor_name = anchor_info['data']['info']['uname'] + + title = await get_bilibili_room_info_h5(url, proxy_addr, cookies) + return {"anchor_name": anchor_name, "live_status": live_status, "room_url": url, "title": title} + except Exception as e: + print(e) + return {"anchor_name": '', "live_status": False, "room_url": url} + + +@trace_error_decorator +async def get_bilibili_stream_data(url: str, qn: str = '10000', platform: str = 'web', proxy_addr: OptionalStr = None, + cookies: OptionalStr = None) -> OptionalStr: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', + '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', + 'origin': 'https://live.bilibili.com', + 'referer': 'https://live.bilibili.com/26066074', + } + if cookies: + headers['Cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + params = { + 'cid': room_id, + 'qn': qn, + 'platform': platform, + } + play_api = f'https://api.live.bilibili.com/room/v1/Room/playUrl?{urllib.parse.urlencode(params)}' + json_str = await async_req(play_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + if json_data and json_data['code'] == 0: + for i in json_data['data']['durl']: + if 'd1--cn-gotcha' in i['url']: + return i['url'] + return json_data['data']['durl'][-1]['url'] + else: + params = { + "room_id": room_id, + "protocol": "0,1", + "format": "0,1,2", + "codec": "0,1,2", + "qn": qn, + "platform": "web", + "ptype": "8", + "dolby": "5", + "panorama": "1", + "hdr_type": "0,1" + } + + # 此接口因网页上有限制, 需要配置登录后的cookie才能获取最高画质 + api = f'https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?{urllib.parse.urlencode(params)}' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + if json_data['data']['live_status'] == 0: + print("The anchor did not start broadcasting.") + return + playurl_info = json_data['data']['playurl_info'] + format_list = playurl_info['playurl']['stream'][0]['format'] + stream_data_list = format_list[0]['codec'] + sorted_stream_list = sorted(stream_data_list, key=itemgetter("current_qn"), reverse=True) + # qn: 30000=杜比 20000=4K 10000=原画 400=蓝光 250=超清 150=高清 80=流畅 + video_quality_options = {'10000': 0, '400': 1, '250': 2, '150': 3, '80': 4} + qn_count = len(sorted_stream_list) + select_stream_index = min(video_quality_options[qn], qn_count - 1) + stream_data: dict = sorted_stream_list[select_stream_index] + base_url = stream_data['base_url'] + host = stream_data['url_info'][0]['host'] + extra = stream_data['url_info'][0]['extra'] + m3u8_url = host + base_url + extra + return m3u8_url + +@trace_error_decorator +async def get_xhs_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + 'xy-common-params': 'platform=iOS&sid=session.1722166379345546829388', + 'referer': 'https://app.xhs.cn/', + } + if cookies: + headers['Cookie'] = cookies + + if "xhslink.com" in url: + url = await async_req(url, proxy_addr=proxy_addr, headers=headers, redirect_url=True) + + host_id = get_params(url, "host_id") + user_id = re.search("/user/profile/(.*?)(?=/|\\?|$)", url) + user_id = user_id.group(1) if user_id else host_id + result = {"anchor_name": '', "is_live": False} + html_str = await async_req(url, proxy_addr=proxy_addr, headers=headers) + match_data = re.search("", html_str) + + if match_data: + json_str = match_data.group(1).replace("undefined", "null") + json_data = json.loads(json_str) + + if json_data.get("liveStream"): + stream_data = json_data["liveStream"] + if stream_data.get("liveStatus") == "success": + room_info = stream_data["roomData"]["roomInfo"] + title = room_info.get("roomTitle") + if title and "回放" not in title: + live_link = room_info["deeplink"] + anchor_name = get_params(live_link, "host_nickname") + flv_url = get_params(live_link, "flvUrl") + room_id = flv_url.split('live/')[1].split('.')[0] + flv_url = f"http://live-source-play.xhscdn.com/live/{room_id}.flv" + m3u8_url = flv_url.replace('.flv', '.m3u8') + result |= { + "anchor_name": anchor_name, + "is_live": True, + "title": title, + "flv_url": flv_url, + "m3u8_url": m3u8_url, + 'record_url': flv_url + } + return result + + profile_url = f"https://www.xiaohongshu.com/user/profile/{user_id}" + html_str = await async_req(profile_url, proxy_addr=proxy_addr, headers=headers) + anchor_name = re.search("@(.*?) 的个人主页", html_str) + if anchor_name: + result["anchor_name"] = anchor_name.group(1) + + return result + + +@trace_error_decorator +async def get_bigo_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', + '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', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'Referer': 'https://www.bigo.tv/', + } + if cookies: + headers['Cookie'] = cookies + + if 'bigo.tv' not in url: + html_str = await async_req(url, proxy_addr=proxy_addr, headers=headers) + web_url = re.search( + '', + html_str).group(1) + room_id = web_url.split('&h=')[-1] + else: + if '&h=' in url: + room_id = url.split('&h=')[-1] + else: + room_id = re.search('www.bigo.tv/cn/(\\w+)', url).group(1) + + data = {'siteId': room_id} # roomId + url2 = 'https://ta.bigo.tv/official_website/studio/getInternalStudioInfo' + json_str = await async_req(url=url2, proxy_addr=proxy_addr, headers=headers, data=data) + json_data = json.loads(json_str) + anchor_name = json_data['data']['nick_name'] + live_status = json_data['data']['alive'] + result = {"anchor_name": anchor_name, "is_live": False} + + if live_status == 1: + live_title = json_data['data']['roomTopic'] + m3u8_url = json_data['data']['hls_src'] + result['m3u8_url'] = m3u8_url + result['record_url'] = m3u8_url + result |= {"title": live_title, "is_live": True, "m3u8_url": m3u8_url, 'record_url': m3u8_url} + elif result['anchor_name'] == '': + html_str = await async_req(url=f'https://www.bigo.tv/cn/{room_id}', proxy_addr=proxy_addr, headers=headers) + result['anchor_name'] = re.search('欢迎来到(.*?)的直播间', html_str, re.DOTALL).group(1) + + return result + + +@trace_error_decorator +async def get_blued_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + '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', + } + if cookies: + headers['Cookie'] = cookies + + html_str = await async_req(url=url, proxy_addr=proxy_addr, headers=headers) + json_str = re.search('decodeURIComponent\\(\"(.*?)\"\\)\\),window\\.Promise', html_str, re.DOTALL).group(1) + json_str = urllib.parse.unquote(json_str) + json_data = json.loads(json_str) + anchor_name = json_data['userInfo']['name'] + live_status = json_data['userInfo']['onLive'] + result = {"anchor_name": anchor_name, "is_live": False} + + if live_status: + m3u8_url = json_data['liveInfo']['liveUrl'] + result |= {"is_live": True, "m3u8_url": m3u8_url, 'record_url': m3u8_url} + return result + + +@trace_error_decorator +async def login_sooplive(username: str, password: str, proxy_addr: OptionalStr = None) -> OptionalStr: + if len(username) < 6 or len(password) < 10: + raise RuntimeError("sooplive login failed! Please enter the correct account and password for the sooplive " + "platform in the config.ini file.") + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'Origin': 'https://play.sooplive.co.kr', + 'Referer': 'https://play.sooplive.co.kr/superbsw123/277837074', + } + + data = { + 'szWork': 'login', + 'szType': 'json', + 'szUid': username, + 'szPassword': password, + 'isSaveId': 'true', + 'isSavePw': 'true', + 'isSaveJoin': 'true', + 'isLoginRetain': 'Y', + } + + url = 'https://login.sooplive.co.kr/app/LoginAction.php' + + try: + cookie_dict = await async_req(url, proxy_addr=proxy_addr, headers=headers, + data=data, return_cookies=True, timeout=20) + cookie_str = '; '.join([f"{k}={v}" for k, v in cookie_dict.items()]) + return cookie_str + except Exception as e: + print(f"An error occurred during login: {e}") + raise Exception( + "sooplive login failed, please check if the account password in the configuration file is correct." + ) + + +@trace_error_decorator +async def get_sooplive_cdn_url(broad_no: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', + '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', + 'Origin': 'https://play.sooplive.co.kr', + 'Referer': 'https://play.sooplive.co.kr/oul282/249469582', + 'Content-Type': 'application/x-www-form-urlencoded', + } + if cookies: + headers['Cookie'] = cookies + + params = { + 'return_type': 'gcp_cdn', + 'use_cors': 'false', + 'cors_origin_url': 'play.sooplive.co.kr', + 'broad_key': f'{broad_no}-common-master-hls', + 'time': '8361.086329376785', + } + + url2 = 'http://livestream-manager.sooplive.co.kr/broad_stream_assign.html?' + urllib.parse.urlencode(params) + json_str = await async_req(url=url2, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_data = json.loads(json_str) + + return json_data + + +@trace_error_decorator +async def get_sooplive_tk(url: str, rtype: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> str | tuple: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0', + 'Origin': 'https://play.sooplive.co.kr', + 'Referer': 'https://play.sooplive.co.kr/secretx/250989857', + 'Content-Type': 'application/x-www-form-urlencoded', + } + + if cookies: + headers['Cookie'] = cookies + + split_url = url.split('/') + bj_id = split_url[3] if len(split_url) < 6 else split_url[5] + room_password = get_params(url, "pwd") + if not room_password: + room_password = '' + data = { + 'bid': bj_id, + 'bno': '', + 'type': rtype, + 'pwd': room_password, + 'player_type': 'html5', + 'stream_type': 'common', + 'quality': 'master', + 'mode': 'landing', + 'from_api': '0', + 'is_revive': 'false', + } + + url2 = f'https://live.sooplive.co.kr/afreeca/player_live_api.php?bjid={bj_id}' + json_str = await async_req(url=url2, proxy_addr=proxy_addr, headers=headers, data=data, abroad=True) + json_data = json.loads(json_str) + + if rtype == 'aid': + token = json_data["CHANNEL"]["AID"] + return token + else: + bj_name = json_data['CHANNEL']['BJNICK'] + bj_id = json_data['CHANNEL']['BJID'] + return f"{bj_name}-{bj_id}", json_data['CHANNEL']['BNO'] + + +@trace_error_decorator +async def get_sooplive_stream_data( + url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None, + username: OptionalStr = None, password: OptionalStr = None +) -> dict: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', + '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', + 'Referer': 'https://m.sooplive.co.kr/', + 'Content-Type': 'application/x-www-form-urlencoded', + } + if cookies: + headers['Cookie'] = cookies + + split_url = url.split('/') + bj_id = split_url[3] if len(split_url) < 6 else split_url[5] + + data = { + 'bj_id': bj_id, + 'broad_no': '', + 'agent': 'web', + 'confirm_adult': 'true', + 'player_type': 'webm', + 'mode': 'live', + } + + url2 = 'http://api.m.sooplive.co.kr/broad/a/watch' + + json_str = await async_req(url=url2, proxy_addr=proxy_addr, headers=headers, data=data, abroad=True) + json_data = json.loads(json_str) + + if 'user_nick' in json_data['data']: + anchor_name = json_data['data']['user_nick'] + if "bj_id" in json_data['data']: + anchor_name = f"{anchor_name}-{json_data['data']['bj_id']}" + else: + anchor_name = '' + + result = {"anchor_name": anchor_name or '' ,"is_live": False} + + async def get_url_list(m3u8: str) -> List[str]: + resp = await async_req(url=m3u8, proxy_addr=proxy_addr, headers=headers, abroad=True) + play_url_list = [] + url_prefix = m3u8.rsplit('/', maxsplit=1)[0] + '/' + for i in resp.split('\n'): + if i.startswith('auth_playlist'): + play_url_list.append(url_prefix + i.strip()) + bandwidth_pattern = re.compile(r'BANDWIDTH=(\d+)') + bandwidth_list = bandwidth_pattern.findall(resp) + url_to_bandwidth = {purl: int(bandwidth) for bandwidth, purl in zip(bandwidth_list, play_url_list)} + play_url_list = sorted(play_url_list, key=lambda purl: url_to_bandwidth[purl], reverse=True) + return play_url_list + + if not anchor_name: + async def handle_login() -> OptionalStr: + cookie = await login_sooplive(username, password, proxy_addr=proxy_addr) + if 'AuthTicket=' in cookie: + print("sooplive platform login successful! Starting to fetch live streaming data...") + return cookie + + async def fetch_data(cookie, _result) -> dict: + aid_token = await get_sooplive_tk(url, rtype='aid', proxy_addr=proxy_addr, cookies=cookie) + _anchor_name, _broad_no = await get_sooplive_tk(url, rtype='info', proxy_addr=proxy_addr, cookies=cookie) + _view_url_data = await get_sooplive_cdn_url(_broad_no, proxy_addr=proxy_addr) + _view_url = _view_url_data['view_url'] + _m3u8_url = _view_url + '?aid=' + aid_token + _result |= { + "anchor_name": _anchor_name, + "is_live": True, + "m3u8_url": _m3u8_url, + 'play_url_list': await get_url_list(_m3u8_url), + 'new_cookies': cookie + } + return _result + + if json_data['data']['code'] == -3001: + print("sooplive live stream failed to retrieve, the live stream just ended.") + return result + + elif json_data['data']['code'] == -3002: + print("sooplive live stream retrieval failed, the live needs 19+, you are not logged in.") + print("Attempting to log in to the sooplive live streaming platform with your account and password, " + "please ensure it is configured.") + new_cookie = await handle_login() + if new_cookie and len(new_cookie) > 0: + return await fetch_data(new_cookie, result) + raise RuntimeError("sooplive login failed, please check if the account and password are correct") + + elif json_data['data']['code'] == -3004: + if cookies and len(cookies) > 0: + return await fetch_data(cookies, result) + else: + raise RuntimeError("sooplive login failed, please check if the account and password are correct") + elif json_data['data']['code'] == -6001: + print("error message:Please check if the input sooplive live room address " + "is correct.") + return result + if json_data['result'] == 1 and anchor_name: + broad_no = json_data['data']['broad_no'] + hls_authentication_key = json_data['data']['hls_authentication_key'] + view_url_data = await get_sooplive_cdn_url(broad_no, proxy_addr=proxy_addr) + view_url = view_url_data['view_url'] + m3u8_url = view_url + '?aid=' + hls_authentication_key + result |= {'is_live': True, 'm3u8_url': m3u8_url, 'play_url_list': await get_url_list(m3u8_url)} + result['new_cookies'] = None + return result + + +@trace_error_decorator +async def get_netease_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://cc.163.com/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + if cookies: + headers['Cookie'] = cookies + url = url + '/' if url[-1] != '/' else url + + html_str = await async_req(url=url, proxy_addr=proxy_addr, headers=headers) + json_str = re.search('', + html_str, re.DOTALL).group(1) + json_data = json.loads(json_str) + room_data = json_data['props']['pageProps']['roomInfoInitData'] + live_data = room_data['live'] + result = {"is_live": False} + live_status = live_data.get('status') == 1 + result["anchor_name"] = live_data.get('nickname', room_data.get('nickname')) + if live_status: + result |= { + 'is_live': True, + 'title': live_data['title'], + 'stream_list': live_data.get('quickplay'), + 'm3u8_url': live_data.get('sharefile') + } + return result + + +@trace_error_decorator +async def get_qiandurebo_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,' + 'application/signed-exchange;v=b3;q=0.7', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://qiandurebo.com/web/index.php', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + if cookies: + headers['Cookie'] = cookies + + html_str = await async_req(url=url, proxy_addr=proxy_addr, headers=headers) + data = re.search('var user = (.*?)\r\n\\s+user\\.play_url', html_str, re.DOTALL).group(1) + anchor_name = re.findall('"zb_nickname": "(.*?)",\r\n', data) + + result = {"anchor_name": "", "is_live": False} + if len(anchor_name) > 0: + result['anchor_name'] = anchor_name[0] + play_url = re.findall('"play_url": "(.*?)",\r\n', data) + + if len(play_url) > 0 and 'common-text-center" style="display:block' not in html_str: + result |= { + 'anchor_name': anchor_name[0], + 'is_live': True, + 'flv_url': play_url[0], + 'record_url': play_url[0] + } + return result + + +@trace_error_decorator +async def get_pandatv_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'origin': 'https://www.pandalive.co.kr', + 'referer': 'https://www.pandalive.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + if cookies: + headers['Cookie'] = cookies + + user_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + url2 = 'https://api.pandalive.co.kr/v1/live/play' + data = { + 'userId': user_id, + 'info': 'media fanGrade', + } + room_password = get_params(url, "pwd") + if not room_password: + room_password = '' + data2 = { + 'action': 'watch', + 'userId': user_id, + 'password': room_password, + 'shareLinkType': '', + } + + result = {"anchor_name": "", "is_live": False} + json_str = await async_req('https://api.pandalive.co.kr/v1/member/bj', + proxy_addr=proxy_addr, headers=headers, data=data, abroad=True) + json_data = json.loads(json_str) + if "bjInfo" not in json_data: + raise RuntimeError(json_data.get("message", 'Unknown error')) + anchor_id = json_data['bjInfo']['id'] + anchor_name = f"{json_data['bjInfo']['nick']}-{anchor_id}" + result['anchor_name'] = anchor_name + live_status = 'media' in json_data + + if live_status: + json_str = await async_req(url2, proxy_addr=proxy_addr, headers=headers, data=data2, abroad=True) + json_data = json.loads(json_str) + if 'errorData' in json_data: + if json_data['errorData']['code'] == 'needAdult': + raise RuntimeError(f"{url} The live room requires login and is only accessible to adults. Please " + f"correctly fill in the login cookie in the configuration file.") + else: + raise RuntimeError(json_data['errorData']['code'], json_data['message']) + play_url = json_data['PlayList']['hls'][0]['url'] + play_url_list = await get_play_url_list(m3u8=play_url, proxy=proxy_addr, header=headers, abroad=True) + result |= {'is_live': True, 'm3u8_url': play_url, 'play_url_list': play_url_list} + return result + + +@trace_error_decorator +async def get_maoerfm_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://fm.missevan.com/live/868895007', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + if cookies: + headers['Cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + url2 = f'https://fm.missevan.com/api/v2/live/{room_id}' + + json_str = await async_req(url=url2, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + + anchor_name = json_data['info']['creator']['username'] + live_status = False + if 'room' in json_data['info']: + live_status = json_data['info']['room']['status']['broadcasting'] + + result = {"anchor_name": anchor_name, "is_live": live_status} + if live_status: + stream_list = json_data['info']['room']['channel'] + m3u8_url = stream_list['hls_pull_url'] + flv_url = stream_list['flv_pull_url'] + title = json_data['info']['room']['name'] + result |= {'is_live': True, 'title': title, 'm3u8_url': m3u8_url, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_winktv_bj_info(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> tuple: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'content-type': 'application/x-www-form-urlencoded', + 'referer': 'https://www.winktv.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + if cookies: + headers['Cookie'] = cookies + user_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + data = { + 'userId': user_id, + 'info': 'media', + } + + info_api = 'https://api.winktv.co.kr/v1/member/bj' + json_str = await async_req(url=info_api, proxy_addr=proxy_addr, headers=headers, data=data, abroad=True) + json_data = json.loads(json_str) + live_status = 'media' in json_data + anchor_id = json_data['bjInfo']['id'] + anchor_name = f"{json_data['bjInfo']['nick']}-{anchor_id}" + return anchor_name, live_status + + +@trace_error_decorator +async def get_winktv_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'content-type': 'application/x-www-form-urlencoded', + 'referer': 'https://www.winktv.co.kr', + 'origin': 'https://www.winktv.co.kr', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + + } + if cookies: + headers['Cookie'] = cookies + user_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + room_password = get_params(url, "pwd") + if not room_password: + room_password = '' + data = { + 'action': 'watch', + 'userId': user_id, + 'password': room_password, + 'shareLinkType': '', + } + + anchor_name, live_status = await get_winktv_bj_info(url=url, proxy_addr=proxy_addr, cookies=cookies) + result = {"anchor_name": anchor_name, "is_live": live_status} + if live_status: + play_api = 'https://api.winktv.co.kr/v1/live/play' + json_str = await async_req(url=play_api, proxy_addr=proxy_addr, headers=headers, data=data, abroad=True) + if '403: Forbidden' in json_str: + raise ConnectionError(f"Your network has been banned from accessing WinkTV ({json_str})") + json_data = json.loads(json_str) + if 'errorData' in json_data: + if json_data['errorData']['code'] == 'needAdult': + raise RuntimeError(f"{url} The live stream is only accessible to logged-in adults. Please ensure that " + f"the cookie is correctly filled in the configuration file after logging in.") + else: + raise RuntimeError(json_data['errorData']['code'], json_data['message']) + m3u8_url = json_data['PlayList']['hls'][0]['url'] + play_url_list = await get_play_url_list(m3u8=m3u8_url, proxy=proxy_addr, header=headers, abroad=True) + result['m3u8_url'] = m3u8_url + result['play_url_list'] = play_url_list + return result + + +@trace_error_decorator +async def login_flextv(username: str, password: str, proxy_addr: OptionalStr = None) -> OptionalStr: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'content-type': 'application/json;charset=UTF-8', + 'referer': 'https://www.flextv.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + + data = { + 'loginId': username, + 'password': password, + 'loginKeep': True, + 'saveId': True, + 'device': 'PCWEB', + } + + url = 'https://api.flextv.co.kr/v2/api/auth/signin' + + try: + print("Logging into FlexTV platform...") + cookie_dict = await async_req(url, proxy_addr=proxy_addr, headers=headers, json_data=data, + return_cookies=True, timeout=20) + + if cookie_dict and 'flx_oauth_access' in cookie_dict: + cookie_str = '; '.join([f"{k}={v}" for k, v in cookie_dict.items()]) + return cookie_str + else: + print("Please check if the FlexTV account and password in the configuration file are correct.") + return None + + except Exception as e: + print(f"FlexTV login request exception: {e}") + raise Exception( + "FlexTV login failed, please check if the account and password in the configuration file are correct." + ) + + +async def get_flextv_stream_url( + url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None +) -> str: + async def fetch_data(cookie) -> dict: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://www.flextv.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + user_id = url.split('/live')[0].rsplit('/', maxsplit=1)[-1] + if cookie: + headers['Cookie'] = cookie + play_api = f'https://api.flextv.co.kr/api/channels/{user_id}/stream?option=all' + json_str = await async_req(play_api, proxy_addr=proxy_addr, headers=headers, abroad=True) + if 'HTTP Error 400: Bad Request' in json_str: + raise ConnectionError( + "Failed to retrieve FlexTV live streaming data, please switch to a different proxy and try again." + ) + return json.loads(json_str) + + json_data = await fetch_data(cookies) + if 'sources' in json_data and len(json_data['sources']) > 0: + play_url = json_data['sources'][0]['url'] + return play_url + + +@trace_error_decorator +async def get_flextv_stream_data( + url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None, + username: OptionalStr = None, password: OptionalStr = None +) -> dict: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://www.flextv.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + if cookies: + headers['Cookie'] = cookies + user_id = url.split('/live')[0].rsplit('/', maxsplit=1)[-1] + result = {"anchor_name": '', "is_live": False} + new_cookies = None + try: + url2 = f'https://www.flextv.co.kr/channels/{user_id}/live' + html_str = await async_req(url2, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_str = re.search('', html_str).group(1) + json_data = json.loads(json_str) + channel_data = json_data['props']['pageProps']['channel'] + login_need = 'message' in channel_data and '로그인후 이용이 가능합니다.' in channel_data.get('message') + if login_need: + print("FlexTV live stream retrieval failed [not logged in]: 19+ live streams are only available for " + "logged-in adults.") + print("Attempting to log in to the FlexTV live streaming platform, please ensure your account and " + "password are correctly filled in the configuration file.") + if len(username) < 6 or len(password) < 8: + raise RuntimeError("FlexTV登录失败!请在config.ini配置文件中填写正确的FlexTV平台的账号和密码") + new_cookies = await login_flextv(username, password, proxy_addr=proxy_addr) + if new_cookies: + print("Logged into FlexTV platform successfully! Starting to fetch live streaming data...") + else: + raise RuntimeError("FlexTV login failed") + cookies = new_cookies if new_cookies else cookies + headers['Cookie'] = cookies + html_str = await async_req(url2, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_str = re.search('', html_str).group(1) + json_data = json.loads(json_str) + channel_data = json_data['props']['pageProps']['channel'] + + live_status = 'message' not in channel_data + if live_status: + anchor_id = channel_data['owner']['loginId'] + anchor_name = f"{channel_data['owner']['nickname']}-{anchor_id}" + result["anchor_name"] = anchor_name + play_url = await get_flextv_stream_url(url=url, proxy_addr=proxy_addr, cookies=cookies) + if play_url: + play_url_list = await get_play_url_list(m3u8=play_url, proxy=proxy_addr, header=headers, abroad=True) + if play_url_list: + result['m3u8_url'] = play_url + result['play_url_list'] = play_url_list + result['is_live'] = True + else: + url2 = f'https://www.flextv.co.kr/channels/{user_id}' + html_str = await async_req(url2, proxy_addr=proxy_addr, headers=headers, abroad=True) + anchor_name = re.search(' tuple: + # 本算法参考项目:https://github.com/785415581/MusicBox/blob/b8f716d43d/doc/analysis/analyze_captured_data.md + + modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee' \ + '341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe487' \ + '5d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' + nonce = b'0CoJUm6Qyw8W8jud' + public_key = '010001' + from Crypto.Cipher import AES + from Crypto.Util.Padding import pad + import base64 + import binascii + import secrets + + def create_secret_key(size: int) -> bytes: + charset = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=[]{}|;:,.<>?' + return ''.join(secrets.choice(charset) for _ in range(size)).encode('utf-8') + + def aes_encrypt(_text: str | bytes, _sec_key: str | bytes) -> bytes: + if isinstance(_text, str): + _text = _text.encode('utf-8') + if isinstance(_sec_key, str): + _sec_key = _sec_key.encode('utf-8') + _sec_key = _sec_key[:16] # 16 (AES-128), 24 (AES-192), or 32 (AES-256) bytes + iv = bytes('0102030405060708', 'utf-8') + encryptor = AES.new(_sec_key, AES.MODE_CBC, iv) + padded_text = pad(_text, AES.block_size) + ciphertext = encryptor.encrypt(padded_text) + encoded_ciphertext = base64.b64encode(ciphertext) + return encoded_ciphertext + + def rsa_encrypt(_text: str | bytes, pub_key: str, mod: str) -> str: + if isinstance(_text, str): + _text = _text.encode('utf-8') + text_reversed = _text[::-1] + text_int = int(binascii.hexlify(text_reversed), 16) + encrypted_int = pow(text_int, int(pub_key, 16), int(mod, 16)) + return format(encrypted_int, 'x').zfill(256) + + sec_key = create_secret_key(16) + enc_text = aes_encrypt(aes_encrypt(json.dumps(text), nonce), sec_key) + enc_sec_key = rsa_encrypt(sec_key, public_key, modulus) + return enc_text.decode(), enc_sec_key + + +async def get_looklive_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + """ + 通过PC网页端的接口获取完整直播源,只有params和encSecKey这两个加密请求参数。 + params: 由两次AES加密完成 + ncSecKey: 由一次自写的加密函数完成,值可固定 + """ + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0', + 'Accept': 'application/json, text/javascript', + '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', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': 'https://look.163.com/', + } + + if cookies: + headers['Cookie'] = cookies + + room_id = re.search('live\\?id=(.*?)&', url).group(1) + params, secretkey = get_looklive_secret_data({"liveRoomNo": room_id}) + request_data = {'params': params, 'encSecKey': secretkey} + api = 'https://api.look.163.com/weapi/livestream/room/get/v3' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers, data=request_data) + json_data = json.loads(json_str) + anchor_name = json_data['data']['anchor']['nickName'] + live_status = json_data['data']['liveStatus'] + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + result["is_live"] = True + if json_data['data']['roomInfo']['liveType'] == 1: + print("Look live currently only supports audio live streaming, not video live streaming!") + else: + play_url_list = json_data['data']['roomInfo']['liveUrl'] + live_title = json_data['data']['roomInfo']['title'] + result |= { + "title": live_title, + "flv_url": play_url_list['httpPullUrl'], + "m3u8_url": play_url_list['hlsPullUrl'], + "record_url": play_url_list['hlsPullUrl'], + } + return result + + +@trace_error_decorator +async def login_popkontv( + username: str, password: str, proxy_addr: OptionalStr = None, code: OptionalStr = 'P-00001' +) -> tuple: + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Authorization': 'Basic FpAhe6mh8Qtz116OENBmRddbYVirNKasktdXQiuHfm88zRaFydTsFy63tzkdZY0u', + 'Content-Type': 'application/json', + 'Origin': 'https://www.popkontv.com', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + + data = { + 'partnerCode': code, + 'signId': username, + 'signPwd': password, + } + + url = 'https://www.popkontv.com/api/proxy/member/v1/login' + + try: + proxy_addr = utils.handle_proxy_addr(proxy_addr) + async with httpx.AsyncClient(proxy=proxy_addr, timeout=20, verify=False) as client: + response = await client.post(url, json=data, headers=headers) + response.raise_for_status() + + json_data = response.json() + login_status_code = json_data.get("statusCd") + + if login_status_code == 'E4010': + raise Exception("popkontv login failed, please reconfigure the correct login account or password!") + elif login_status_code == 'S2000': + token = json_data['data'].get("token") + partner_code = json_data['data'].get("partnerCode") + return token, partner_code + else: + raise Exception(f"popkontv login failed, {json_data.get('statusMsg', 'unknown error')}") + except httpx.HTTPStatusError as e: + print(f"HTTP status error occurred during login: {e.response.status_code}") + raise + except Exception as e: + print(f"An exception occurred during popkontv login: {e}") + raise + + +@trace_error_decorator +async def get_popkontv_stream_data( + url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None, + username: OptionalStr = None, code: OptionalStr = 'P-00001' +) -> tuple: + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Content-Type': 'application/json', + 'Origin': 'https://www.popkontv.com', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + if cookies: + headers['Cookie'] = cookies + if 'mcid' in url: + anchor_id = re.search('mcid=(.*?)&', url).group(1) + else: + anchor_id = re.search('castId=(.*?)(?=&|$)', url).group(1) + + data = { + 'partnerCode': code, + 'searchKeyword': anchor_id, + 'signId': username, + } + + api = 'https://www.popkontv.com/api/proxy/broadcast/v1/search/all' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers, json_data=data, abroad=True) + json_data = json.loads(json_str) + + partner_code = '' + anchor_name = 'Unknown' + for item in json_data['data']['broadCastList']: + if item['mcSignId'] == anchor_id: + mc_name = item['nickName'] + anchor_name = f"{mc_name}-{anchor_id}" + partner_code = item['mcPartnerCode'] + break + + if not partner_code: + if 'mcPartnerCode' in url: + regex_result = re.search('mcPartnerCode=(P-\\d+)', url) + else: + regex_result = re.search('partnerCode=(P-\\d+)', url) + partner_code = regex_result.group(1) if regex_result else code + notices_url = f'https://www.popkontv.com/channel/notices?mcid={anchor_id}&mcPartnerCode={partner_code}' + notices_response = await async_req(notices_url, proxy_addr=proxy_addr, headers=headers, abroad=True) + mc_name_match = re.search(r'"mcNickName":"([^"]+)"', notices_response) + mc_name = mc_name_match.group(1) if mc_name_match else 'Unknown' + anchor_name = f"{anchor_id}-{mc_name}" + + live_url = f"https://www.popkontv.com/live/view?castId={anchor_id}&partnerCode={partner_code}" + html_str2 = await async_req(live_url, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_str2 = re.search('', html_str2).group(1) + json_data2 = json.loads(json_str2) + if 'mcData' in json_data2['props']['pageProps']: + room_data = json_data2['props']['pageProps']['mcData']['data'] + is_private = room_data['mc_isPrivate'] + cast_start_date_code = room_data['mc_castStartDate'] + mc_sign_id = room_data['mc_signId'] + cast_type = room_data['castType'] + return anchor_name, [cast_start_date_code, partner_code, mc_sign_id, cast_type, is_private] + else: + return anchor_name, None + + +@trace_error_decorator +async def get_popkontv_stream_url( + url: str, + proxy_addr: OptionalStr = None, + access_token: OptionalStr = None, + username: OptionalStr = None, + password: OptionalStr = None, + partner_code: OptionalStr = 'P-00001' +) -> dict: + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'ClientKey': 'Client FpAhe6mh8Qtz116OENBmRddbYVirNKasktdXQiuHfm88zRaFydTsFy63tzkdZY0u', + 'Content-Type': 'application/json', + 'Origin': 'https://www.popkontv.com', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + + if access_token: + headers['Authorization'] = f'Bearer {access_token}' + + anchor_name, room_info = await get_popkontv_stream_data( + url, proxy_addr=proxy_addr, code=partner_code, username=username) + result = {"anchor_name": anchor_name, "is_live": False} + new_token = None + if room_info: + cast_start_date_code, cast_partner_code, mc_sign_id, cast_type, is_private = room_info + result["is_live"] = True + room_password = get_params(url, "pwd") + if int(is_private) != 0 and not room_password: + raise RuntimeError(f"Failed to retrieve live room data because {anchor_name}'s room is a private room. " + f"Please configure the room password and try again.") + + async def fetch_data(header: dict = None, code: str = None) -> str: + data = { + 'androidStore': 0, + 'castCode': f'{mc_sign_id}-{cast_start_date_code}', + 'castPartnerCode': cast_partner_code, + 'castSignId': mc_sign_id, + 'castType': cast_type, + 'commandType': 0, + 'exePath': 5, + 'isSecret': is_private, + 'partnerCode': code, + 'password': room_password, + 'signId': username, + 'version': '4.6.2', + } + play_api = 'https://www.popkontv.com/api/proxy/broadcast/v1/castwatchonoffguest' + return await async_req(play_api, proxy_addr=proxy_addr, json_data=data, headers=header, abroad=True) + + json_str = await fetch_data(headers, partner_code) + + if 'HTTP Error 400' in json_str or 'statusCd":"E5000' in json_str: + print("Failed to retrieve popkontv live stream [token does not exist or has expired]: Please log in to " + "watch.") + print("Attempting to log in to the popkontv live streaming platform, please ensure your account " + "and password are correctly filled in the configuration file.") + if len(username) < 4 or len(password) < 10: + raise RuntimeError("popkontv login failed! Please enter the correct account and password for the " + "popkontv platform in the config.ini file.") + print("Logging into popkontv platform...") + new_access_token, new_partner_code = await login_popkontv( + username=username, password=password, proxy_addr=proxy_addr, code=partner_code + ) + if new_access_token and len(new_access_token) == 640: + print("Logged into popkontv platform successfully! Starting to fetch live streaming data...") + headers['Authorization'] = f'Bearer {new_access_token}' + new_token = f'Bearer {new_access_token}' + json_str = await fetch_data(headers, new_partner_code) + else: + raise RuntimeError("popkontv login failed, please check if the account and password are correct") + json_data = json.loads(json_str) + status_msg = json_data["statusMsg"] + if json_data['statusCd'] == "L000A": + print("Failed to retrieve live stream source,", status_msg) + raise RuntimeError("You are an unverified member. After logging into the popkontv official website, " + "please verify your mobile phone at the bottom of the 'My Page' > 'Edit My " + "Information' to use the service.") + elif json_data['statusCd'] == "L0001": + cast_start_date_code = int(cast_start_date_code) - 1 + json_str = await fetch_data(headers, partner_code) + json_data = json.loads(json_str) + m3u8_url = json_data['data']['castHlsUrl'] + result |= {"m3u8_url": m3u8_url, "record_url": m3u8_url} + elif json_data['statusCd'] == "L0000": + m3u8_url = json_data['data']['castHlsUrl'] + result |= {"m3u8_url": m3u8_url, "record_url": m3u8_url} + else: + raise RuntimeError("Failed to retrieve live stream source,", status_msg) + result['new_token'] = new_token + return result + + +@trace_error_decorator +async def login_twitcasting( + account_type: str, username: str, password: str, proxy_addr: OptionalStr = None, + cookies: OptionalStr = None +) -> OptionalStr: + headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': 'https://twitcasting.tv/indexcaslogin.php?redir=%2Findexloginwindow.php%3Fnext%3D%252F&keep=1', + 'Cookie': 'hl=zh; did=04fb08f1b15d248644f1dfa82816d323; _ga=GA1.1.1021187740.1709706998; keep=1; mfadid=yrQiEB26ruRg7mlMavABMBZWdOddzojW; _ga_X8R46Y30YM=GS1.1.1709706998.1.1.1709712274.0.0.0', + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + } + + if cookies: + headers['Cookie'] = cookies + + if account_type == "twitter": + login_url = 'https://twitcasting.tv/indexpasswordlogin.php' + login_api = 'https://twitcasting.tv/indexpasswordlogin.php?redir=/indexloginwindow.php?next=%2F&keep=1' + else: + login_url = 'https://twitcasting.tv/indexcaslogin.php?redir=%2F&keep=1' + login_api = 'https://twitcasting.tv/indexcaslogin.php?redir=/indexloginwindow.php?next=%2F&keep=1' + + html_str = await async_req(login_url, proxy_addr=proxy_addr, headers=headers) + cs_session_id = re.search('', html_str).group(1) + + data = { + 'username': username, + 'password': password, + 'action': 'login', + 'cs_session_id': cs_session_id, + } + try: + cookie_dict = await async_req(login_api, proxy_addr=proxy_addr, headers=headers, + json_data=data, return_cookies=True, timeout=20) + if 'tc_ss' in cookie_dict: + cookie = utils.dict_to_cookie_str(cookie_dict) + return cookie + except Exception as e: + print("TwitCasting login error,", e) + + +@trace_error_decorator +async def get_twitcasting_stream_url( + url: str, + proxy_addr: OptionalStr = None, + cookies: OptionalStr = None, + account_type: OptionalStr = None, + username: OptionalStr = None, + password: OptionalStr = None, +) -> dict: + headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Referer': 'https://twitcasting.tv/?ch0', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + } + + anchor_id = url.split('/')[3] + if cookies: + headers['Cookie'] = cookies + + async def get_data(header) -> tuple: + html_str = await async_req(url, proxy_addr=proxy_addr, headers=header) + anchor = re.search("(.*?) \\(@(.*?)\\) 的直播 - Twit", html_str) + title = re.search('<meta name="twitter:title" content="(.*?)">\n\\s+<meta', html_str) + status = re.search('data-is-onlive="(.*?)"\n\\s+data-view-mode', html_str) + movie_id = re.search('data-movie-id="(.*?)" data-audience-id', html_str) + return f'{anchor.group(1).strip()}-{anchor.group(2)}-{movie_id.group(1)}', status.group(1), title.group(1) + + result = {"anchor_name": '', "is_live": False} + new_cookie = None + try: + to_login = get_params(url, "login") + if to_login == 'true': + print("Attempting to log in to TwitCasting...") + new_cookie = await login_twitcasting( + account_type=account_type, username=username, password=password, proxy_addr=proxy_addr, cookies=cookies) + if not new_cookie: + raise RuntimeError("TwitCasting login failed, please check if the account password in the " + "configuration file is correct") + print("TwitCasting login successful! Starting to fetch data...") + headers['Cookie'] = new_cookie + anchor_name, live_status, live_title = get_data(headers) + except AttributeError: + print("Failed to retrieve TwitCasting data, attempting to log in...") + new_cookie = await login_twitcasting( + account_type=account_type, username=username, password=password, proxy_addr=proxy_addr, cookies=cookies) + if not new_cookie: + raise RuntimeError("TwitCasting login failed, please check if the account and password in the " + "configuration file are correct") + print("TwitCasting login successful! Starting to fetch data...") + headers['Cookie'] = new_cookie + anchor_name, live_status, live_title = await get_data(headers) + + result["anchor_name"] = anchor_name + if live_status == 'true': + play_url = f'https://twitcasting.tv/{anchor_id}/metastream.m3u8/?video=1&mode=source' + result |= {'title': live_title, 'is_live': True, "m3u8_url": play_url, "record_url": play_url} + result['new_cookies'] = new_cookie + return result + + +@trace_error_decorator +async def get_baidu_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Connection': 'keep-alive', + 'Referer': 'https://live.baidu.com/', + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + } + if cookies: + headers['Cookie'] = cookies + + uid = random.choice([ + 'h5-683e85bdf741bf2492586f7ca39bf465', + 'h5-c7c6dc14064a136be4215b452fab9eea', + 'h5-4581281f80bb8968bd9a9dfba6050d3a' + ]) + room_id = re.search('room_id=(.*?)&', url).group(1) + params = { + 'cmd': '371', + 'action': 'star', + 'service': 'bdbox', + 'osname': 'baiduboxapp', + 'data': '{"data":{"room_id":"' + room_id + '","device_id":"h5-683e85bdf741bf2492586f7ca39bf465",' + '"source_type":0,"osname":"baiduboxapp"},"replay_slice":0,' + '"nid":"","schemeParams":{"src_pre":"pc","src_suf":"other",' + '"bd_vid":"","share_uid":"","share_cuk":"","share_ecid":"",' + '"zb_tag":"","shareTaskInfo":"{\\"room_id\\":\\"9175031377\\"}",' + '"share_from":"","ext_params":"","nid":""}}', + 'ua': '360_740_ANDROID_0', + 'bd_vid': '', + 'uid': uid, + '_': str(int(time.time() * 1000)), + } + app_api = f'https://mbd.baidu.com/searchbox?{urllib.parse.urlencode(params)}' + json_str = await async_req(url=app_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + key = list(json_data['data'].keys())[0] + data = json_data['data'][key] + anchor_name = data['host']['name'] + result = {"anchor_name": anchor_name, "is_live": False} + if data['status'] == "0": + result["is_live"] = True + live_title = data['video']['title'] + play_url_list = data['video']['url_clarity_list'] + url_list = [] + prefix = 'https://hls.liveshow.bdstatic.com/live/' + if play_url_list: + for i in play_url_list: + url_list.append( + prefix + i['urls']['flv'].rsplit('.', maxsplit=1)[0].rsplit('/', maxsplit=1)[1] + '.m3u8') + else: + play_url_list = data['video']['url_list'] + for i in play_url_list: + url_list.append(prefix + i['urls'][0]['hls'].rsplit('?', maxsplit=1)[0].rsplit('/', maxsplit=1)[1]) + + if url_list: + result |= {"is_live": True, "title": live_title, 'play_url_list': url_list} + return result + + +@trace_error_decorator +async def get_weibo_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Cookie': 'XSRF-TOKEN=qAP-pIY5V4tO6blNOhA4IIOD; SUB=_2AkMRNMCwf8NxqwFRmfwWymPrbI9-zgzEieKnaDFrJRMxHRl-yT9kqmkhtRB6OrTuX5z9N_7qk9C3xxEmNR-8WLcyo2PM; SUBP=0033WrSXqPxfM72-Ws9jqgMF55529P9D9WWemwcqkukCduUO11o9sBqA; WBPSESS=Wk6CxkYDejV3DDBcnx2LOXN9V1LjdSTNQPMbBDWe4lO2HbPmXG_coMffJ30T-Avn_ccQWtEYFcq9fab1p5RR6PEI6w661JcW7-56BszujMlaiAhLX-9vT4Zjboy1yf2l', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + 'Referer': 'https://weibo.com/u/5885340893' + } + if cookies: + headers['Cookie'] = cookies + + room_id = '' + if 'show/' in url: + room_id = url.split('?')[0].split('show/')[1] + else: + uid = url.split('?')[0].rsplit('/u/', maxsplit=1)[1] + web_api = f'https://weibo.com/ajax/statuses/mymblog?uid={uid}&page=1&feature=0' + json_str = await async_req(web_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + for i in json_data['data']['list']: + if 'page_info' in i and i['page_info']['object_type'] == 'live': + room_id = i['page_info']['object_id'] + break + + result = {"anchor_name": '', "is_live": False} + if room_id: + app_api = f'https://weibo.com/l/pc/anchor/live?live_id={room_id}' + # app_api = f'https://weibo.com/l/!/2/wblive/room/show_pc_live.json?live_id={room_id}' + json_str = await async_req(url=app_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + anchor_name = json_data['data']['user_info']['name'] + result["anchor_name"] = anchor_name + live_status = json_data['data']['item']['status'] + if live_status == 1: + result["is_live"] = True + live_title = json_data['data']['item']['desc'] + play_url_list = json_data['data']['item']['stream_info']['pull'] + m3u8_url = play_url_list['live_origin_hls_url'] + flv_url = play_url_list['live_origin_flv_url'] + result['title'] = live_title + result['play_url_list'] = [ + {"m3u8_url": m3u8_url, "flv_url": flv_url}, + {"m3u8_url": m3u8_url.split('_')[0] + '.m3u8', "flv_url": flv_url.split('_')[0] + '.flv'} + ] + return result + + +@trace_error_decorator +async def get_kugou_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + 'Accept': 'application/json', + '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', + 'Referer': 'https://fanxing2.kugou.com/', + } + if cookies: + headers['Cookie'] = cookies + + if 'roomId' in url: + room_id = re.search('roomId=(\\d+)', url).group(1) + else: + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + + app_api = f'https://service2.fanxing.kugou.com/roomcen/room/web/cdn/getEnterRoomInfo?roomId={room_id}' + json_str = await async_req(url=app_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + anchor_name = json_data['data']['normalRoomInfo']['nickName'] + result = {"anchor_name": anchor_name, "is_live": False} + if not anchor_name: + raise RuntimeError( + "Music channel live rooms are not supported for recording, please switch to a different live room." + ) + live_status = json_data['data']['liveType'] + if live_status != -1: + params = { + 'std_rid': room_id, + 'std_plat': '7', + 'std_kid': '0', + 'streamType': '1-2-4-5-8', + 'ua': 'fx-flash', + 'targetLiveTypes': '1-5-6', + 'version': '1000', + 'supportEncryptMode': '1', + 'appid': '1010', + '_': str(int(time.time() * 1000)), + } + api = f'https://fx1.service.kugou.com/video/pc/live/pull/mutiline/streamaddr?{urllib.parse.urlencode(params)}' + json_str2 = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data2 = json.loads(json_str2) + stream_data = json_data2['data']['lines'] + if stream_data: + flv_url = stream_data[-1]['streamProfiles'][0]['httpsFlv'][0] + result |= {"is_live": True, "flv_url": flv_url, "record_url": flv_url} + return result + + +async def get_twitchtv_room_info(url: str, token: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> tuple: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + 'Accept-Language': 'zh-CN', + 'Referer': 'https://www.twitch.tv/', + 'Client-Id': 'kimne78kx3ncx6brgo4mv6wki5h1ko', + 'Client-Integrity': token, + 'Content-Type': 'text/plain;charset=UTF-8', + } + if cookies: + headers['Cookie'] = cookies + uid = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + + data = [ + { + "operationName": "ChannelShell", + "variables": { + "login": uid + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "580ab410bcd0c1ad194224957ae2241e5d252b2c5173d8e0cce9d32d5bb14efe" + } + } + }, + ] + + json_str = await async_req('https://gql.twitch.tv/gql', proxy_addr=proxy_addr, headers=headers, + json_data=data, abroad=True) + json_data = json.loads(json_str) + user_data = json_data[0]['data']['userOrError'] + login_name = user_data["login"] + nickname = f"{user_data['displayName']}-{login_name}" + status = True if user_data['stream'] else False + return nickname, status + + +@trace_error_decorator +async def get_twitchtv_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Accept-Language': 'en-US', + 'Referer': 'https://www.twitch.tv/', + 'Client-ID': 'kimne78kx3ncx6brgo4mv6wki5h1ko', + 'device-id': generate_random_string(16).lower(), + } + + if cookies: + headers['Cookie'] = cookies + uid = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + + data = { + "operationName": "PlaybackAccessToken_Template", + "query": "query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, " + "$isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, " + "params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: " + "$isLive) { value signature authorization { isForbidden forbiddenReasonCode } __typename " + "} videoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", " + "playerType: $playerType}) @include(if: $isVod) { value signature __typename }}", + "variables": { + "isLive": True, + "login": uid, + "isVod": False, + "vodID": "", + "playerType": "site" + } + } + + json_str = await async_req('https://gql.twitch.tv/gql', proxy_addr=proxy_addr, headers=headers, + json_data=data, abroad=True) + json_data = json.loads(json_str) + token = json_data['data']['streamPlaybackAccessToken']['value'] + sign = json_data['data']['streamPlaybackAccessToken']['signature'] + + anchor_name, live_status = await get_twitchtv_room_info(url=url, token=token, proxy_addr=proxy_addr, cookies=cookies) + result = {"anchor_name": anchor_name, "is_live": live_status} + if live_status: + play_session_id = random.choice(["bdd22331a986c7f1073628f2fc5b19da", "064bc3ff1722b6f53b0b5b8c01e46ca5"]) + params = { + "acmb": "e30=", + "allow_source": "true", + "browser_family": "firefox", + "browser_version": "124.0", + "cdm": "wv", + "fast_bread": "true", + "os_name": "Windows", + "os_version": "NT%2010.0", + "p": "3553732", + "platform": "web", + "play_session_id": play_session_id, + "player_backend": "mediaplayer", + "player_version": "1.28.0-rc.1", + "playlist_include_framerate": "true", + "reassignments_supported": "true", + "sig": sign, + "token": token, + "transcode_mode": "cbr_v1" + } + access_key = urllib.parse.urlencode(params) + m3u8_url = f'https://usher.ttvnw.net/api/channel/hls/{uid}.m3u8?{access_key}' + play_url_list = await get_play_url_list(m3u8=m3u8_url, proxy=proxy_addr, header=headers, abroad=True) + result |= {'m3u8_url': m3u8_url, 'play_url_list': play_url_list} + return result + + +@trace_error_decorator +async def get_liveme_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'origin': 'https://www.liveme.com', + 'referer': 'https://www.liveme.com', + 'user-agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + } + if cookies: + headers['Cookie'] = cookies + + room_id = url.split("/index.html")[0].rsplit('/', maxsplit=1)[-1] + sign_data = execjs.compile(open(f'{JS_SCRIPT_PATH}/liveme.js').read()).call('sign', room_id, + f'{JS_SCRIPT_PATH}/crypto-js.min.js') + lm_s_sign = sign_data.pop("lm_s_sign") + tongdun_black_box = sign_data.pop("tongdun_black_box") + platform = sign_data.pop("os") + headers['lm-s-sign'] = lm_s_sign + + params = { + 'alias': 'liveme', + 'tongdun_black_box': tongdun_black_box, + 'os': platform, + } + + api = f'https://live.liveme.com/live/queryinfosimple?{urllib.parse.urlencode(params)}' + json_str = await async_req(api, data=sign_data, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_data = json.loads(json_str) + stream_data = json_data['data']['video_info'] + anchor_name = stream_data['uname'] + live_status = stream_data['status'] + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == "0": + m3u8_url = stream_data['hlsvideosource'] + flv_url = stream_data['videosource'] + result |= { + 'is_live': True, + 'm3u8_url': m3u8_url, + 'flv_url': flv_url, + 'record_url': m3u8_url or flv_url + } + return result + + +async def get_huajiao_sn(url: str, cookies: OptionalStr = None, proxy_addr: OptionalStr = None) -> tuple | None: + headers = { + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://www.huajiao.com/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + + if cookies: + headers['Cookie'] = cookies + + live_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + api = f'https://www.huajiao.com/l/{live_id}' + try: + html_str = await async_req(url=api, proxy_addr=proxy_addr, headers=headers) + json_str = re.search('var feed = (.*?});', html_str).group(1) + json_data = json.loads(json_str) + sn = json_data['feed']['sn'] + uid = json_data['author']['uid'] + nickname = json_data['author']['nickname'] + live_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + return nickname, sn, uid, live_id + except Exception: + utils.replace_url(f'{script_path}/config/URL_config.ini', old=url, new='#' + url) + raise RuntimeError("Failed to retrieve live room data, the Huajiao live room address is not fixed, please use " + "the anchor's homepage address for recording.") + + +async def get_huajiao_user_info(url: str, cookies: OptionalStr = None, proxy_addr: OptionalStr = None) -> OptionalDict: + headers = { + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://www.huajiao.com/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + + if cookies: + headers['Cookie'] = cookies + + if 'user' in url: + uid = url.split('?')[0].split('user/')[1] + params = { + 'uid': uid, + 'fmt': 'json', + '_': str(int(time.time() * 1000)), + } + + api = f'https://webh.huajiao.com/User/getUserFeeds?{urllib.parse.urlencode(params)}' + json_str = await async_req(url=api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + + html_str = await async_req(url=f'https://www.huajiao.com/user/{uid}', proxy_addr=proxy_addr, headers=headers) + anchor_name = re.search('<title>(.*?)的主页.*', html_str).group(1) + if json_data['data'] and 'sn' in json_data['data']['feeds'][0]['feed']: + feed = json_data['data']['feeds'][0]['feed'] + return { + "anchor_name": anchor_name, + "title": feed['title'], + "is_live": True, + "sn": feed['sn'], + "liveid": feed['relateid'], + "uid": uid + } + else: + return {"anchor_name": anchor_name, "is_live": False} + + +async def get_huajiao_stream_url_app(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> OptionalDict: + headers = { + 'User-Agent': 'living/9.4.0 (com.huajiao.seeding; build:2410231746; iOS 17.0.0) Alamofire/9.4.0', + 'accept-language': 'zh-Hans-US;q=1.0', + 'sdk_version': '1', + } + if cookies: + headers['Cookie'] = cookies + room_id = url.rsplit('/', maxsplit=1)[1] + api = f'https://live.huajiao.com/feed/getFeedInfo?relateid={room_id}' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + + if json_data['errmsg'] or not json_data['data'].get('creatime'): + print("Failed to retrieve live room data, the Huajiao live room address is not fixed, please manually change " + "the address for recording.") + return + data = json_data['data'] + return { + "anchor_name": data['author']['nickname'], + "title": data['feed']['title'], + "is_live": True, + "sn": data['feed']['sn'], + "liveid": data['feed']['relateid'], + "uid": data['author']['uid'] + } + + +@trace_error_decorator +async def get_huajiao_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://www.huajiao.com/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + if cookies: + headers['Cookie'] = cookies + + result = {"anchor_name": "", "is_live": False} + + if 'user/' in url: + if not cookies: + return result + room_data = await get_huajiao_user_info(url, cookies, proxy_addr) + else: + url = await async_req(url, proxy_addr=proxy_addr, headers=headers, redirect_url=True) + if url.rstrip('/') == 'https://www.huajiao.com': + print( + "Failed to retrieve live room data, the Huajiao live room address is not fixed, please manually change " + "the address for recording.") + return result + room_data = await get_huajiao_stream_url_app(url, proxy_addr) + + if room_data: + result["anchor_name"] = room_data.pop("anchor_name") + live_status = room_data.pop("is_live") + + if live_status: + result["title"] = room_data.pop("title") + params = { + "time": int(time.time() * 1000), + "version": "1.0.0", + **room_data, + "encode": "h265" + } + + api = f'https://live.huajiao.com/live/substream?{urllib.parse.urlencode(params)}' + json_str = await async_req(url=api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + result |= { + 'is_live': True, + 'flv_url': json_data['data']['h264_url'], + 'record_url': json_data['data']['h264_url'], + } + return result + + +@trace_error_decorator +async def get_liuxing_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Referer': 'https://wap.7u66.com/198189?promoters=0', + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + + } + if cookies: + headers['Cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + params = { + "promoters": "0", + "roomidx": room_id, + "currentUrl": f"https://www.7u66.com/{room_id}?promoters=0" + } + api = f'https://wap.7u66.com/api/ui/room/v1.0.0/live.ashx?{urllib.parse.urlencode(params)}' + json_str = await async_req(url=api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + room_info = json_data['data']['roomInfo'] + anchor_name = room_info['nickname'] + live_status = room_info["live_stat"] + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + idx = room_info['idx'] + live_id = room_info['liveId1'] + flv_url = f'https://txpull1.5see.com/live/{idx}/{live_id}.flv' + result |= {'is_live': True, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_showroom_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + if cookies: + headers['Cookie'] = cookies + + if '/room/profile' in url: + room_id = url.split('room_id=')[-1] + else: + html_str = await async_req(url, proxy_addr=proxy_addr, headers=headers, abroad=True) + room_id = re.search('href="/room/profile\\?room_id=(.*?)"', html_str).group(1) + info_api = f'https://www.showroom-live.com/api/live/live_info?room_id={room_id}' + json_str = await async_req(info_api, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_data = json.loads(json_str) + anchor_name = json_data['room_name'] + result = {"anchor_name": anchor_name, "is_live": False} + live_status = json_data['live_status'] + if live_status == 2: + result["is_live"] = True + web_api = f'https://www.showroom-live.com/api/live/streaming_url?room_id={room_id}&abr_available=1' + json_str = await async_req(web_api, proxy_addr=proxy_addr, headers=headers, abroad=True) + if json_str: + json_data = json.loads(json_str) + streaming_url_list = json_data['streaming_url_list'] + + for i in streaming_url_list: + if i['type'] == 'hls_all': + m3u8_url = i['url'] + result['m3u8_url'] = m3u8_url + if m3u8_url: + m3u8_url_list = await get_play_url_list(m3u8_url, proxy=proxy_addr, header=headers, abroad=True) + if m3u8_url_list: + result['play_url_list'] = [f"{m3u8_url.rsplit('/', maxsplit=1)[0]}/{i}" for i in + m3u8_url_list] + else: + result['play_url_list'] = [m3u8_url] + result['play_url_list'] = [i.replace('https://', 'http://') for i in result['play_url_list']] + break + return result + + +@trace_error_decorator +async def get_acfun_sign_params(proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> tuple: + did = f'web_{utils.generate_random_string(16)}' + headers = { + 'referer': 'https://live.acfun.cn/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'cookie': f'_did={did};', + } + if cookies: + headers['Cookie'] = cookies + data = { + 'sid': 'acfun.api.visitor', + } + api = 'https://id.app.acfun.cn/rest/app/visitor/login' + json_str = await async_req(api, data=data, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + user_id = json_data["userId"] + visitor_st = json_data["acfun.api.visitor_st"] + return user_id, did, visitor_st + + +@trace_error_decorator +async def get_acfun_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'referer': 'https://live.acfun.cn/live/17912421', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + if cookies: + headers['Cookie'] = cookies + + author_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + user_info_api = f'https://live.acfun.cn/rest/pc-direct/user/userInfo?userId={author_id}' + json_str = await async_req(user_info_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + anchor_name = json_data['profile']['name'] + status = 'liveId' in json_data['profile'] + result = {"anchor_name": anchor_name, "is_live": False} + if status: + result["is_live"] = True + user_id, did, visitor_st = await get_acfun_sign_params(proxy_addr=proxy_addr, cookies=cookies) + params = { + 'subBiz': 'mainApp', + 'kpn': 'ACFUN_APP', + 'kpf': 'PC_WEB', + 'userId': user_id, + 'did': did, + 'acfun.api.visitor_st': visitor_st, + } + + data = { + 'authorId': author_id, + 'pullStreamType': 'FLV', + } + play_api = f'https://api.kuaishouzt.com/rest/zt/live/web/startPlay?{urllib.parse.urlencode(params)}' + json_str = await async_req(play_api, data=data, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + live_title = json_data['data']['caption'] + videoPlayRes = json_data['data']['videoPlayRes'] + play_url_list = json.loads(videoPlayRes)['liveAdaptiveManifest'][0]['adaptationSet']['representation'] + play_url_list = sorted(play_url_list, key=itemgetter('bitrate'), reverse=True) + result |= {'play_url_list': play_url_list, 'title': live_title} + return result + + +@trace_error_decorator +async def get_changliao_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + 'Accept': 'application/json, text/plain, */*', + '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', + 'Referer': 'https://wap.tlclw.com/phone/15777?promoters=0', + } + if cookies: + headers['Cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + params = { + 'roomidx': room_id, + 'currentUrl': f'https://wap.tlclw.com/{room_id}', + } + play_api = f'https://wap.tlclw.com/api/ui/room/v1.0.0/live.ashx?{urllib.parse.urlencode(params)}' + json_str = await async_req(play_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + anchor_name = json_data['data']['roomInfo']['nickname'] + live_status = json_data['data']['roomInfo']['live_stat'] + + async def get_live_domain(page_url): + html_str = await async_req(page_url, proxy_addr=proxy_addr, headers=headers) + config_json_str = re.findall("var config = (.*?)config.webskins", + html_str, re.DOTALL)[0].rsplit(";", maxsplit=1)[0].strip() + config_json_data = json.loads(config_json_str) + stream_flv_domain = config_json_data['domainpullstream_flv'] + stream_hls_domain = config_json_data['domainpullstream_hls'] + return stream_flv_domain, stream_hls_domain + + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + flv_domain, hls_domain = await get_live_domain(url) + live_id = json_data['data']['roomInfo']['liveID'] + flv_url = f'{flv_domain}/{live_id}.flv' + m3u8_url = f'{hls_domain}/{live_id}.m3u8' + result |= {'is_live': True, 'm3u8_url': m3u8_url, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_yingke_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'Referer': 'https://www.inke.cn/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + if cookies: + headers['Cookie'] = cookies + + parsed_url = urllib.parse.urlparse(url) + query_params = urllib.parse.parse_qs(parsed_url.query) + uid = query_params['uid'][0] + live_id = query_params['id'][0] + params = { + 'uid': uid, + 'id': live_id, + '_t': str(int(time.time())), + } + + api = f'https://webapi.busi.inke.cn/web/live_share_pc?{urllib.parse.urlencode(params)}' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + anchor_name = json_data['data']['media_info']['nick'] + live_status = json_data['data']['status'] + + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + m3u8_url = json_data['data']['live_addr'][0]['hls_stream_addr'] + flv_url = json_data['data']['live_addr'][0]['stream_addr'] + result |= {'is_live': True, 'm3u8_url': m3u8_url, 'flv_url': flv_url, 'record_url': m3u8_url} + return result + + +@trace_error_decorator +async def get_yinbo_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + 'Accept': 'application/json, text/plain, */*', + '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', + 'Referer': 'https://live.ybw1666.com/800005143?promoters=0', + } + if cookies: + headers['Cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + params = { + 'roomidx': room_id, + 'currentUrl': f'https://wap.ybw1666.com/{room_id}', + } + play_api = f'https://wap.ybw1666.com/api/ui/room/v1.0.0/live.ashx?{urllib.parse.urlencode(params)}' + json_str = await async_req(play_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + room_data = json_data['data']['roomInfo'] + anchor_name = room_data['nickname'] + live_status = room_data['live_stat'] + + async def get_live_domain(page_url): + html_str = await async_req(page_url, proxy_addr=proxy_addr, headers=headers) + config_json_str = re.findall("var config = (.*?)config.webskins", + html_str, re.DOTALL)[0].rsplit(";", maxsplit=1)[0].strip() + config_json_data = json.loads(config_json_str) + stream_flv_domain = config_json_data['domainpullstream_flv'] + stream_hls_domain = config_json_data['domainpullstream_hls'] + return stream_flv_domain, stream_hls_domain + + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + flv_domain, hls_domain = await get_live_domain(url) + live_id = room_data['liveID'] + flv_url = f'{flv_domain}/{live_id}.flv' + m3u8_url = f'{hls_domain}/{live_id}.m3u8' + result |= {'is_live': True, 'm3u8_url': m3u8_url, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_zhihu_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + if cookies: + headers['Cookie'] = cookies + + if 'people/' in url: + user_id = url.split('people/')[1] + api = f'https://api.zhihu.com/people/{user_id}/profile?profile_new_version=' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + live_page_url = json_data['drama']['living_theater']['theater_url'] + else: + live_page_url = url + + web_id = live_page_url.split('?')[0].rsplit('/', maxsplit=1)[-1] + html_str = await async_req(live_page_url, proxy_addr=proxy_addr, headers=headers) + json_str2 = re.findall('', html_str)[0] + json_data2 = json.loads(json_str2) + live_data = json_data2['initialState']['theater']['theaters'][web_id] + anchor_name = live_data['actor']['name'] + live_status = live_data['drama']['status'] + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + live_title = live_data['theme'] + play_url = live_data['drama']['playInfo'] + result |= { + 'is_live': True, + 'title': live_title, + 'm3u8_url': play_url['hlsUrl'], + 'flv_url': play_url['playUrl'], + 'record_url': play_url['hlsUrl'] + } + return result + + +@trace_error_decorator +async def get_chzzk_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'origin': 'https://chzzk.naver.com', + 'referer': 'https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + if cookies: + headers['Cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + play_api = f'https://api.chzzk.naver.com/service/v3/channels/{room_id}/live-detail' + json_str = await async_req(play_api, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_data = json.loads(json_str) + live_data = json_data['content'] + anchor_name = live_data['channel']['channelName'] + live_status = live_data['status'] + + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 'OPEN': + play_data = json.loads(live_data['livePlaybackJson']) + m3u8_url = play_data['media'][0]['path'] + m3u8_url_list = await get_play_url_list(m3u8_url, proxy=proxy_addr, header=headers, abroad=True) + prefix = m3u8_url.split('?')[0].rsplit('/', maxsplit=1)[0] + m3u8_url_list = [prefix + '/' + i for i in m3u8_url_list] + result |= {"is_live": True, "m3u8_url": m3u8_url, "play_url_list": m3u8_url_list} + return result + + +@trace_error_decorator +async def get_haixiu_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'origin': 'https://www.haixiutv.com', + 'referer': 'https://www.haixiutv.com/', + 'user-agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + } + if cookies: + headers['Cookie'] = cookies + + room_id = url.split("?")[0].rsplit('/', maxsplit=1)[-1] + if 'haixiutv' in url: + access_token = "pLXSC%252FXJ0asc1I21tVL5FYZhNJn2Zg6d7m94umCnpgL%252BuVm31GQvyw%253D%253D" + else: + access_token = "s7FUbTJ%252BjILrR7kicJUg8qr025ZVjd07DAnUQd8c7g%252Fo4OH9pdSX6w%253D%253D" + + params = { + "accessToken": access_token, + "tku": "3000006", + "c": "10138100100000", + "_st1": int(time.time() * 1000) + } + ajax_data = execjs.compile(open(f'{JS_SCRIPT_PATH}/haixiu.js').read()).call('sign', params, + f'{JS_SCRIPT_PATH}/crypto-js.min.js') + + params["accessToken"] = urllib.parse.unquote(urllib.parse.unquote(access_token)) + params['_ajaxData1'] = ajax_data + params['_'] = int(time.time() * 1000) + + if 'haixiutv' in url: + api = f'https://service.haixiutv.com/v2/room/{room_id}/media/advanceInfoRoom?{urllib.parse.urlencode(params)}' + else: + headers['origin'] = 'https://www.lehaitv.com' + headers['referer'] = 'https://www.lehaitv.com' + api = f'https://service.lehaitv.com/v2/room/{room_id}/media/advanceInfoRoom?{urllib.parse.urlencode(params)}' + + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_data = json.loads(json_str) + + stream_data = json_data['data'] + anchor_name = stream_data['nickname'] + live_status = stream_data['live_status'] + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + flv_url = stream_data['media_url_web'] + result |= {'is_live': True, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_vvxqiu_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + 'Access-Control-Request-Method': 'GET', + 'Origin': 'https://h5webcdn-pro.vvxqiu.com', + 'Referer': 'https://h5webcdn-pro.vvxqiu.com/', + } + + if cookies: + headers['Cookie'] = cookies + + room_id = get_params(url, "roomId") + api_1 = f'https://h5p.vvxqiu.com/activity-center/fanclub/activity/captain/banner?roomId={room_id}&product=vvstar' + json_str = await async_req(api_1, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + anchor_name = json_data['data']['anchorName'] + if not anchor_name: + params = { + 'sessionId': '', + 'userId': '', + 'product': 'vvstar', + 'tickToken': '', + 'roomId': room_id, + } + json_str = await async_req( + f'https://h5p.vvxqiu.com/activity-center/halloween2023/banner?{urllib.parse.urlencode(params)}', + proxy_addr=proxy_addr, headers=headers + ) + json_data = json.loads(json_str) + anchor_name = json_data['data']['memberVO']['memberName'] + + result = {"anchor_name": anchor_name, "is_live": False} + m3u8_url = f'https://liveplay-pro.wasaixiu.com/live/1400442770_{room_id}_{room_id[2:]}_single.m3u8' + resp = await async_req(m3u8_url, proxy_addr=proxy_addr, headers=headers) + if 'Not Found' not in resp: + result |= {'is_live': True, 'm3u8_url': m3u8_url, 'record_url': m3u8_url} + return result + + +@trace_error_decorator +async def get_17live_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'origin': 'https://17.live', + 'referer': 'https://17.live/', + 'user-agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + } + + if cookies: + headers['Cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + api_1 = f'https://wap-api.17app.co/api/v1/user/room/{room_id}' + json_str = await async_req(api_1, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + anchor_name = json_data["displayName"] + result = {"anchor_name": anchor_name, "is_live": False} + json_data = { + 'liveStreamID': room_id, + } + api_1 = f'https://wap-api.17app.co/api/v1/lives/{room_id}/viewers/alive' + json_str = await async_req(api_1, json_data=json_data, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + live_status = json_data.get("status") + if live_status and live_status == 2: + flv_url = json_data['pullURLsInfo']['rtmpURLs'][0]['urlHighQuality'] + result |= {'is_live': True, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_langlive_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'origin': 'https://www.lang.live', + 'referer': 'https://www.lang.live/', + 'user-agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + } + + if cookies: + headers['Cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + api_1 = f'https://api.lang.live/langweb/v1/room/liveinfo?room_id={room_id}' + json_str = await async_req(api_1, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_data = json.loads(json_str) + live_info = json_data['data']['live_info'] + anchor_name = live_info['nickname'] + live_status = live_info['live_status'] + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + flv_url = json_data['data']['live_info']['liveurl'] + m3u8_url = json_data['data']['live_info']['liveurl_hls'] + result |= {'is_live': True, 'm3u8_url': m3u8_url, 'flv_url': flv_url, 'record_url': m3u8_url} + return result + + +@trace_error_decorator +async def get_pplive_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'Content-Type': 'application/json', + 'Origin': 'https://m.pp.weimipopo.com', + 'Referer': 'https://m.pp.weimipopo.com/', + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + } + + if cookies: + headers['Cookie'] = cookies + + room_id = get_params(url, 'anchorUid') + json_data = { + 'inviteUuid': '', + 'anchorUuid': room_id, + } + + if 'catshow' in url: + api = 'https://api.catshow168.com/live/preview' + headers['Origin'] = 'https://h.catshow168.com' + headers['Referer'] = 'https://h.catshow168.com' + else: + api = 'https://api.pp.weimipopo.com/live/preview' + json_str = await async_req(api, json_data=json_data, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + live_info = json_data['data'] + anchor_name = live_info['name'] + live_status = live_info['living'] + result = {"anchor_name": anchor_name, "is_live": False} + if live_status: + m3u8_url = live_info['pullUrl'] + result |= {'is_live': True, 'm3u8_url': m3u8_url, 'record_url': m3u8_url} + return result + + +@trace_error_decorator +async def get_6room_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://ios.6.cn/?ver=8.0.3&build=4', + 'user-agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + } + + if cookies: + headers['Cookie'] = cookies + + room_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] + html_str = await async_req(f'https://v.6.cn/{room_id}', proxy_addr=proxy_addr, headers=headers) + room_id = re.search('rid: \'(.*?)\',\n\\s+roomid', html_str).group(1) + data = { + 'av': '3.1', + 'encpass': '', + 'logiuid': '', + 'project': 'v6iphone', + 'rate': '1', + 'rid': '', + 'ruid': room_id, + } + api = 'https://v.6.cn/coop/mobile/index.php?padapi=coop-mobile-inroom.php' + json_str = await async_req(api, data=data, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + flv_title = json_data['content']['liveinfo']['flvtitle'] + anchor_name = json_data['content']['roominfo']['alias'] + result = {"anchor_name": anchor_name, "is_live": False} + if flv_title: + flv_url = f'https://wlive.6rooms.com/httpflv/{flv_title}.flv' + result |= {'is_live': True, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_shopee_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://live.shopee.sg/share?from=live&session=802458&share_user_id=', + 'user-agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + } + + if cookies: + headers['Cookie'] = cookies + + result = {"anchor_name": "", "is_live": False} + is_living = False + + if 'live.shopee' not in url and 'uid' not in url: + url = await async_req(url, proxy_addr=proxy_addr, headers=headers, redirect_url=True, abroad=True) + + if 'live.shopee' in url: + host_suffix = url.split('/')[2].rsplit('.', maxsplit=1)[1] + is_living = get_params(url, 'uid') is None + else: + host_suffix = url.split('/')[2].split('.', maxsplit=1)[0] + + uid = get_params(url, 'uid') + api_host = f'https://live.shopee.{host_suffix}' + session_id = get_params(url, 'session') + if uid: + json_str = await async_req(f'{api_host}/api/v1/shop_page/live/ongoing?uid={uid}', + proxy_addr=proxy_addr, headers=headers, abroad=True) + json_data = json.loads(json_str) + if json_data['data']['ongoing_live']: + session_id = json_data['data']['ongoing_live']['session_id'] + is_living = True + else: + json_str = await async_req(f'{api_host}/api/v1/shop_page/live/replay_list?offset=0&limit=1&uid={uid}', + proxy_addr=proxy_addr, headers=headers, abroad=True) + json_data = json.loads(json_str) + if json_data['data']['replay']: + result['anchor_name'] = json_data['data']['replay'][0]['nick_name'] + return result + + json_str = await async_req(f'{api_host}/api/v1/session/{session_id}', proxy_addr=proxy_addr, headers=headers, abroad=True) + json_data = json.loads(json_str) + if not json_data.get('data'): + print("Fetch shopee live data failed, please update the address of the live broadcast room and try again.") + return result + uid = json_data['data']['session']['uid'] + anchor_name = json_data['data']['session']['nickname'] + live_status = json_data['data']['session']['status'] + result["anchor_name"] = anchor_name + result['uid'] = f'uid={uid}&session={session_id}' + if live_status == 1 and is_living: + flv_url = json_data['data']['session']['play_url'] + title = json_data['data']['session']['title'] + result |= {'is_live': True, 'title': title, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_youtube_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + + if cookies: + headers['Cookie'] = cookies + + html_str = await async_req(url, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_str = re.search('var ytInitialPlayerResponse = (.*?);var meta = document\\.createElement', html_str).group(1) + json_data = json.loads(json_str) + result = {"anchor_name": "", "is_live": False} + if 'videoDetails' not in json_data: + print("Error: Please log in to YouTube on your device's webpage and configure cookies in the config.ini") + return result + result['anchor_name'] = json_data['videoDetails']['author'] + live_status = json_data['videoDetails'].get('isLive') + if live_status: + live_title = json_data['videoDetails']['title'] + m3u8_url = json_data['streamingData']["hlsManifestUrl"] + play_url_list = await get_play_url_list(m3u8_url, proxy=proxy_addr, header=headers, abroad=True) + result |= {"is_live": True, "title": live_title, "m3u8_url": m3u8_url, "play_url_list": play_url_list} + return result + + +@trace_error_decorator +async def get_taobao_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'Referer': 'https://huodong.m.taobao.com/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Cookie': '', + } + + if cookies: + headers['Cookie'] = cookies + + if '_m_h5_tk' not in headers['Cookie']: + print('Error: Cookies is empty! please input correct cookies') + + live_id = get_params(url, 'id') + if not live_id: + html_str = await async_req(url, proxy_addr=proxy_addr, headers=headers) + redirect_url = re.findall("var url = '(.*?)';", html_str)[0] + live_id = get_params(redirect_url, 'id') + + params = { + 'jsv': '2.7.0', + 'appKey': '12574478', + 't': '1733104933120', + 'sign': '', + 'AntiFlood': 'true', + 'AntiCreep': 'true', + 'api': 'mtop.mediaplatform.live.livedetail', + 'v': '4.0', + 'preventFallback': 'true', + 'type': 'jsonp', + 'dataType': 'jsonp', + 'callback': 'mtopjsonp1', + 'data': '{"liveId":"' + live_id + '","creatorId":null}', + } + + for i in range(2): + app_key = '12574478' + _m_h5_tk = re.findall('_m_h5_tk=(.*?);', headers['Cookie'])[0] + t13 = int(time.time() * 1000) + pre_sign_str = f'{_m_h5_tk.split("_")[0]}&{t13}&{app_key}&' + params['data'] + sign = execjs.compile(open(f'{JS_SCRIPT_PATH}/taobao-sign.js').read()).call('sign', pre_sign_str) + params |= {'sign': sign, 't': t13} + api = f'https://h5api.m.taobao.com/h5/mtop.mediaplatform.live.livedetail/4.0/?{urllib.parse.urlencode(params)}' + jsonp_str, new_cookie = await async_req(url=api, proxy_addr=proxy_addr, headers=headers, timeout=20, + return_cookies=True, include_cookies=True) + json_data = utils.jsonp_to_json(jsonp_str) + + ret_msg = json_data['ret'] + if ret_msg == ['SUCCESS::调用成功']: + anchor_name = json_data['data']['broadCaster']['accountName'] + result = {"anchor_name": anchor_name, "is_live": False} + live_status = json_data['data']['streamStatus'] + if live_status == '1': + live_title = json_data['data']['title'] + play_url_list = json_data['data']['liveUrlList'] + + def get_sort_key(item): + definition_priority = { + "lld": 0, "ld": 1, "md": 2, "hd": 3, "ud": 4 + } + def_value = item.get('definition') or item.get('newDefinition') + priority = definition_priority.get(def_value, -1) + return priority + + play_url_list = sorted(play_url_list, key=get_sort_key, reverse=True) + result |= {"is_live": True, "title": live_title, "play_url_list": play_url_list, 'live_id': live_id} + + return result + else: + print(f'Error: Taobao live data fetch failed, {ret_msg[0]}') + + if '_m_h5_tk' in new_cookie and '_m_h5_tk_enc' in new_cookie: + headers['Cookie'] = re.sub('_m_h5_tk=(.*?);', new_cookie['_m_h5_tk'], headers['Cookie']) + headers['Cookie'] = re.sub('_m_h5_tk_enc=(.*?);', new_cookie['_m_h5_tk_enc'], headers['Cookie']) + else: + print('Error: Try to update cookie failed, please update the cookies in the configuration file') + + +@trace_error_decorator +async def get_jd_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'User-Agent': 'ios/7.830 (ios 17.0; ; iPhone 15 (A2846/A3089/A3090/A3092))', + 'origin': 'https://lives.jd.com', + 'referer': 'https://lives.jd.com/', + 'x-referer-page': 'https://lives.jd.com/', + } + + if cookies: + headers['Cookie'] = cookies + + redirect_url = await async_req(url, proxy_addr=proxy_addr, headers=headers, redirect_url=True) + author_id = get_params(redirect_url, 'authorId') + result = {"anchor_name": '', "is_live": False} + if not author_id: + live_id = re.search('#/(.*?)\\?origin', redirect_url) + if not live_id: + return result + live_id = live_id.group(1) + result['anchor_name'] = f'jd_{live_id}' + else: + data = { + 'functionId': 'talent_head_findTalentMsg', + 'appid': 'dr_detail', + 'body': '{"authorId":"' + author_id + '","monitorSource":"1","userId":""}', + } + info_api = 'https://api.m.jd.com/talent_head_findTalentMsg' + json_str = await async_req(info_api, data=data, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + anchor_name = json_data['result']['talentName'] + result['anchor_name'] = anchor_name + if 'livingRoomJump' not in json_data['result']: + return result + live_id = json_data['result']['livingRoomJump']['params']['id'] + params = { + "body": '{"liveId": "' + live_id + '"}', + "functionId": "getImmediatePlayToM", + "appid": "h5-live" + } + + api = f'https://api.m.jd.com/client.action?{urllib.parse.urlencode(params)}' + # backup_api: https://api.m.jd.com/api + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + live_status = json_data['data']['status'] + if live_status == 1: + if author_id: + data = { + 'functionId': 'jdTalentContentList', + 'appid': 'dr_detail', + 'body': '{"authorId":"' + author_id + '","type":1,"userId":"","page":1,"offset":"-1",' + '"monitorSource":"1","pageSize":1}', + } + json_str2 = await async_req('https://api.m.jd.com/jdTalentContentList', data=data, + proxy_addr=proxy_addr, headers=headers) + json_data2 = json.loads(json_str2) + result['title'] = json_data2['result']['content'][0]['title'] + + flv_url = json_data['data']['videoUrl'] + m3u8_url = json_data['data']['h5VideoUrl'] + result |= {"is_live": True, "m3u8_url": m3u8_url, "flv_url": flv_url, "record_url": m3u8_url} + return result + + +@trace_error_decorator +async def get_faceit_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'Referer': 'https://www.faceit.com/zh/players/qpjzz/stream', + 'faceit-referer': 'web-next', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + } + + if cookies: + headers['Cookie'] = cookies + nickname = re.findall('/players/(.*?)/stream', url)[0] + api = f'https://www.faceit.com/api/users/v1/nicknames/{nickname}' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + user_id = json_data['payload']['id'] + api2 = f'https://www.faceit.com/api/stream/v1/streamings?userId={user_id}' + json_str2 = await async_req(api2, proxy_addr=proxy_addr, headers=headers) + json_data2 = json.loads(json_str2) + platform_info = json_data2['payload'][0] + anchor_name = platform_info.get('userNickname') + anchor_id = platform_info.get('platformId') + platform = platform_info.get('platform') + if platform == 'twitch': + result = await get_twitchtv_stream_data(f'https://www.twitch.tv/{anchor_id}') + result['anchor_name'] = anchor_name + else: + result = {'anchor_name': anchor_name, 'is_live': False} + return result + + +@trace_error_decorator +async def get_migu_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + 'origin': 'https://www.miguvideo.com', + 'referer': 'https://www.miguvideo.com/', + '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', + 'appCode': 'miguvideo_default_www', + 'appId': 'miguvideo', + 'channel': 'H5', + } + + if cookies: + headers['Cookie'] = cookies + + web_id = url.split('?')[0].rsplit('/')[-1] + api = f'https://vms-sc.miguvideo.com/vms-match/v6/staticcache/basic/basic-data/{web_id}/miguvideo' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + + anchor_name = json_data['body']['title'] + live_title = json_data['body'].get('title') + '-' + json_data['body'].get('detailPageTitle', '') + room_id = json_data['body'].get('pId') + + result = {"anchor_name": anchor_name, "is_live": False} + if not room_id: + return result + + params = { + 'contId': room_id, + 'rateType': '3', + 'clientId': str(uuid.uuid4()), + 'timestamp': int(time.time() * 1000), + 'flvEnable': 'true', + 'xh265': 'false', + 'chip': 'mgwww', + 'channelId': '', + } + + api = f'https://webapi.miguvideo.com/gateway/playurl/v3/play/playurl?{urllib.parse.urlencode(params)}' + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + live_status = json_data['body']['content']['currentLive'] + if live_status != '1': + return result + else: + result['title'] = live_title + source_url = json_data['body']['urlInfo']['url'] + + async def _get_dd_calcu(url): + try: + result = subprocess.run( + ["node", f"{JS_SCRIPT_PATH}/migu.js", url], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except execjs.ProgramError: + raise execjs.ProgramError('Failed to execute JS code. Please check if the Node.js environment') + + ddCalcu = await _get_dd_calcu(source_url) + real_source_url = f'{source_url}&ddCalcu={ddCalcu}&sv=10010' + if '.m3u8' in real_source_url: + m3u8_url = await async_req( + real_source_url, proxy_addr=proxy_addr, headers=headers, redirect_url=True) + result['m3u8_url'] = m3u8_url + result['record_url'] = m3u8_url + else: + result['flv_url'] = real_source_url + result['record_url'] = real_source_url + result['is_live'] = True + return result + + +@trace_error_decorator +async def get_lianjie_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + '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', + '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', + } + + if cookies: + headers['cookie'] = cookies + + room_id = url.split('?')[0].rsplit('lailianjie.com/', maxsplit=1)[-1] + play_api = f'https://api.lailianjie.com/ApiServices/service/live/getRoomInfo?&_$t=&_sign=&roomNumber={room_id}' + json_str = await async_req(play_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + + room_data = json_data['data'] + anchor_name = room_data['nickname'] + live_status = room_data['isonline'] + + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + title = room_data['defaultRoomTitle'] + webrtc_url = room_data['videoUrl'] + https_url = "https://" + webrtc_url.split('webrtc://')[1] + flv_url = https_url.replace('?', '.flv?') + m3u8_url = https_url.replace('?', '.m3u8?') + result |= {'is_live': True, 'title': title, 'm3u8_url': m3u8_url, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_laixiu_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + def generate_uuid(ua_type: str): + if ua_type == "mobile": + return str(uuid.uuid4()) + return str(uuid.uuid4()).replace('-', '') + + def calculate_sign(ua_type: str = 'pc'): + a = int(time.time() * 1000) + s = generate_uuid(ua_type) + u = 'kk792f28d6ff1f34ec702c08626d454b39pro' + + input_str = f"web{s}{a}{u}" + md5_hash = hashlib.md5(input_str.encode('utf-8')).hexdigest() + + return { + 'timestamp': a, + 'imei': s, + 'requestId': md5_hash, + 'inputString': input_str + } + + sign_data = calculate_sign(ua_type='pc') + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0', + 'mobileModel': 'web', + 'timestamp': str(sign_data['timestamp']), + 'loginType': '2', + 'versionCode': '10003', + 'imei': sign_data['imei'], + 'requestId': sign_data['requestId'], + 'channel': '9', + 'version': '1.0.0', + 'os': 'web', + 'platform': 'WEB', + 'Origin': 'https://www.imkktv.com', + 'Referer': 'https://www.imkktv.com/', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + } + + if cookies: + headers['cookie'] = cookies + + pattern = r"(?:roomId|anchorId)=(.*?)(?=&|$)" + match = re.search(pattern, url) + room_id = match.group(1) if match else '' + play_api = f'https://api.imkktv.com/liveroom/getShareLiveVideo?roomId={room_id}' + json_str = await async_req(play_api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + + room_data = json_data['data'] + anchor_name = room_data['nickname'] + live_status = room_data['playStatus'] == 0 + + result = {"anchor_name": anchor_name, "is_live": False} + if live_status: + flv_url = room_data['playUrl'] + result |= {'is_live': True, 'flv_url': flv_url, 'record_url': flv_url} + return result + + +@trace_error_decorator +async def get_picarto_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict: + headers = { + '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', + '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', + } + + if cookies: + headers['cookie'] = cookies + + anchor_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + api = f'https://ptvintern.picarto.tv/api/channel/detail/{anchor_id}' + + json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers) + json_data = json.loads(json_str) + + anchor_name = json_data['channel']['name'] + live_status = json_data['channel']['online'] + + result = {"anchor_name": anchor_name, "is_live": live_status} + if live_status: + title = json_data['channel']['title'] + m3u8_url = f"https://1-edge1-us-newyork.picarto.tv/stream/hls/golive+{anchor_name}/index.m3u8" + result |= {'is_live': True, 'title': title, 'm3u8_url': m3u8_url, 'record_url': m3u8_url} + return result diff --git a/stream.py b/stream.py new file mode 100644 index 0000000..0860d4c --- /dev/null +++ b/stream.py @@ -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 \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..2125df2 --- /dev/null +++ b/test.py @@ -0,0 +1,66 @@ +import os +import subprocess + +def get_git_objects(repo_path="."): + """获取 Git 仓库中所有对象的 hash 和大小""" + pack_dir = os.path.join(repo_path, ".git", "objects", "pack") + idx_files = [f for f in os.listdir(pack_dir) if f.endswith(".idx")] + + objects = [] + for idx in idx_files: + idx_path = os.path.join(pack_dir, idx) + try: + # 运行 git verify-pack + result = subprocess.check_output( + ["git", "verify-pack", "-v", idx_path], + cwd=repo_path, + text=True, + errors="ignore" + ) + for line in result.splitlines(): + parts = line.split() + # 只处理包含 "blob" 的行(文件对象) + if len(parts) >= 5 and parts[1] == "blob": + sha = parts[0] + size = int(parts[2]) # uncompressed size + objects.append((sha, size)) + except subprocess.CalledProcessError as e: + print(f"Error reading {idx_path}: {e}") + return objects + +def map_objects_to_files(repo_path="."): + """把对象 hash 映射到文件路径""" + result = subprocess.check_output( + ["git", "rev-list", "--objects", "--all"], + cwd=repo_path, + text=True, + errors="ignore" + ) + mapping = {} + for line in result.splitlines(): + parts = line.split(" ", 1) + if len(parts) == 2: + sha, path = parts + mapping[sha] = path + return mapping + +def main(): + repo_path = "." # 当前目录 + objects = get_git_objects(repo_path) + mapping = map_objects_to_files(repo_path) + + if not objects: + print("⚠️ 没找到大对象,请确认仓库里有 pack 文件。") + return + + # 按大小排序,取前 20 个 + objects.sort(key=lambda x: x[1], reverse=True) + top_n = objects[:20] + + print("Git 仓库里最大的 20 个文件:\n") + for sha, size in top_n: + path = mapping.get(sha, "(已删除或不可见文件)") + print(f"{size/1024:.2f} KB\t{sha}\t{path}") + +if __name__ == "__main__": + main() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..01b0d0c --- /dev/null +++ b/utils.py @@ -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)) diff --git a/verify.txt b/verify.txt new file mode 100644 index 0000000..e69de29