From 8001baabf83d7a6eb7b94d325558929425a69811 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 28 Mar 2026 18:10:43 -0500 Subject: [PATCH] 's' --- backup/robust_relay.py | 2 +- douyin_youtube_ffplay.py | 3 +- ecosystem.config.cjs | 3 +- ffmpeg_install.py | 110 +----------------------- main.py | 85 +++++++++--------- scripts/env_check.py | 6 +- scripts/launch.py | 23 ++--- src/control_plane.py | 29 +++---- src/initializer.py | 107 ++--------------------- src/proxy.py | 69 ++++----------- src/web_process_backend.py | 76 +--------------- tiktok.py | 84 +++++++++--------- tools/bootstrap_remote_repo_paramiko.py | 71 +++++++++++++++ web.py | 13 +-- youtube.py | 35 ++------ 15 files changed, 227 insertions(+), 489 deletions(-) create mode 100644 tools/bootstrap_remote_repo_paramiko.py diff --git a/backup/robust_relay.py b/backup/robust_relay.py index 000d753..025c1df 100644 --- a/backup/robust_relay.py +++ b/backup/robust_relay.py @@ -80,7 +80,7 @@ def _launch_ffmpeg( cmd.append(youtube_rtmp) - creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0 + creationflags = 0 proc = subprocess.Popen( cmd, stdin=subprocess.PIPE, diff --git a/douyin_youtube_ffplay.py b/douyin_youtube_ffplay.py index 71e2a9d..e11b2b6 100644 --- a/douyin_youtube_ffplay.py +++ b/douyin_youtube_ffplay.py @@ -1,4 +1,5 @@ # douyin_youtube_ffplay.py +# Linux:仓库内推流实现,编码由 config/hardware.env(LIVE_VIDEO_ENCODER)与可选 NVENC 探测决定。 import queue import unicodedata import platform @@ -206,7 +207,7 @@ def start_douyin_youtube_ffplay( if not _hw_enc_active: try: - if platform.system().lower() in ["windows", "linux"]: + if platform.system().lower() == "linux": check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=5) if check.returncode == 0: enc = subprocess.run( diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 48da343..bb1db73 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -46,7 +46,6 @@ module.exports = { // interpreter: py, // env: { PYTHONUNBUFFERED: "1" }, // }, - // Linux: { name: "obs", cwd: root, script: "bash", args: `-lc ${JSON.stringify(path.join(root, "obs.sh"))}` }, - // Win: { name: "obs", cwd: root, script: "cmd.exe", args: "/c obs.bat" }, + // { name: "obs", cwd: root, script: "bash", args: `-lc ${JSON.stringify(path.join(root, "obs.sh"))}` }, ], }; diff --git a/ffmpeg_install.py b/ffmpeg_install.py index 7bc3a16..4e2b863 100644 --- a/ffmpeg_install.py +++ b/ffmpeg_install.py @@ -9,111 +9,15 @@ Copyright (c) 2024 by Hmily, All Rights Reserved. from __future__ import annotations import os -import re import subprocess import sys import platform -import zipfile -from pathlib import Path -import requests -from tqdm import tqdm from src.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') -ffmpeg_path = os.path.join(execute_dir, 'ffmpeg') - - -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 get_lanzou_download_link(url: str, password: str | None = None) -> str | None: - try: - headers = { - 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', - 'Origin': 'https://wweb.lanzouv.com', - 'Referer': 'https://wweb.lanzouv.com/iXncv0dly6mh', - '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', - } - response = requests.get(url, headers=headers) - sign = re.search("var skdklds = '(.*?)';", response.text).group(1) - data = { - 'action': 'downprocess', - 'sign': sign, - 'p': password, - 'kd': '1', - } - response = requests.post('https://wweb.lanzouv.com/ajaxm.php', headers=headers, data=data) - json_data = response.json() - download_url = json_data['dom'] + "/file/" + json_data['url'] - response = requests.get(download_url, headers=headers) - return response.url - except Exception as e: - logger.error(f"Failed to obtain ffmpeg download address. {e}") - - -def install_ffmpeg_windows(): - try: - logger.warning("ffmpeg is not installed.") - logger.debug("Installing the latest version of ffmpeg for Windows...") - ffmpeg_url = get_lanzou_download_link('https://wweb.lanzouv.com/iHAc22ly3r3g', 'eots') - if ffmpeg_url: - full_file_name = 'ffmpeg_latest_build_20250124.zip' - version = 'v20250124' - zip_file_path = Path(execute_dir) / full_file_name - if Path(zip_file_path).exists(): - logger.debug("ffmpeg installation file already exists, start install...") - else: - response = requests.get(ffmpeg_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 ffmpeg ({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) - os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path - result = subprocess.run(["ffmpeg", "-version"], capture_output=True) - if result.returncode == 0: - logger.debug('ffmpeg installation was successful') - else: - logger.error('ffmpeg installation failed. Please manually install ffmpeg by yourself') - return True - else: - logger.error("Please manually install ffmpeg by yourself") - except Exception as e: - logger.error(f"type: {type(e).__name__}, ffmpeg installation failed {e}") - - -def install_ffmpeg_mac(): - logger.warning("ffmpeg is not installed.") - logger.debug("Installing the stable version of ffmpeg for macOS...") - try: - result = subprocess.run(["brew", "install", "ffmpeg"], capture_output=True) - if result.returncode == 0: - logger.debug('ffmpeg installation was successful. Restart for changes to take effect.') - return True - else: - logger.error("ffmpeg installation failed") - except subprocess.CalledProcessError as e: - logger.error(f"Failed to install ffmpeg using Homebrew. {e}") - logger.error("Please install ffmpeg manually or check your Homebrew installation.") - except Exception as e: - logger.error(f"An unexpected error occurred: {e}") +current_env_path = os.environ.get("PATH") or "" +ffmpeg_path = os.path.join(execute_dir, "ffmpeg") def install_ffmpeg_linux(): @@ -161,15 +65,9 @@ def install_ffmpeg_linux(): def install_ffmpeg() -> bool: - if current_platform == "Windows": - return install_ffmpeg_windows() - elif current_platform == "Linux": + if current_platform == "Linux": return install_ffmpeg_linux() - elif current_platform == "Darwin": - return install_ffmpeg_mac() - else: - logger.debug(f"ffmpeg auto installation is not supported on this platform: {current_platform}. " - f"Please install ffmpeg manually.") + logger.debug("ffmpeg 自动安装仅支持 Linux,请使用系统包管理器安装 ffmpeg。") return False diff --git a/main.py b/main.py index f27ffbd..80c0165 100644 --- a/main.py +++ b/main.py @@ -83,8 +83,7 @@ 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" +clear_command = "clear" color_obj = utils.Color() os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path @@ -184,8 +183,7 @@ def display_info2() -> None: try: sys.stdout.flush() time.sleep(5) - if Path(sys.executable).name != 'pythonw.exe': - os.system(clear_command) + os.system(clear_command) print(f"\r共监测{monitoring}个直播中", end=" | ") print(f"同一时间访问网络的线程数: {max_request}", end=" | ") print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ") @@ -267,15 +265,6 @@ def delete_line(file_path: str, del_line: str, delete_all: bool = False) -> None 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: @@ -294,7 +283,7 @@ def segment_video(converts_file_path: str, segment_save_file_path: str, segment_ segment_save_file_path, ] _output = subprocess.check_output( - ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ffmpeg_command, stderr=subprocess.STDOUT ) if is_original_delete: time.sleep(1) @@ -329,7 +318,7 @@ def converts_mp4(converts_file_path: str, is_original_delete: bool = True) -> No "-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) + ffmpeg_command, stderr=subprocess.STDOUT ) if is_original_delete: time.sleep(1) @@ -349,7 +338,7 @@ def converts_m4a(converts_file_path: str, is_original_delete: bool = True) -> No "-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)) + ], stderr=subprocess.STDOUT) if is_original_delete: time.sleep(1) if os.path.exists(converts_file_path): @@ -446,7 +435,7 @@ def push_message(record_name: str, live_url: str, content: str) -> None: 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) + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout, stderr = process.communicate() stdout_decoded = stdout.decode('utf-8') @@ -511,7 +500,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa 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) + ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT ) subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] @@ -528,12 +517,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa 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.send_signal(signal.SIGINT) process.wait() return True time.sleep(1) @@ -1621,23 +1605,42 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: error_window.append(1) else: - import sys - sys.path.insert(0, './dist') - from douyin_srs_ffplay import start_douyin_srs_ffplay - # 调用 - start_douyin_srs_ffplay( - title=title_in_name, - anchor_name=anchor_name, - real_url=real_url, - rec_info=rec_info, - record_name=record_name, - recording=recording, - recording_time_list=recording_time_list, - running_list=running_list, - monitoring=monitoring, - clear_record_info=clear_record_info, - color_obj=color_obj - ) + # 可选:项目 dist/ 下提供 douyin_srs_ffplay;否则使用仓库内 YouTube 推流 + _repo_root = Path(__file__).resolve().parent + _dist = _repo_root / "dist" + if _dist.is_dir(): + sys.path.insert(0, str(_dist)) + try: + from douyin_srs_ffplay import start_douyin_srs_ffplay + + start_douyin_srs_ffplay( + title=title_in_name, + anchor_name=anchor_name, + real_url=real_url, + rec_info=rec_info, + record_name=record_name, + recording=recording, + recording_time_list=recording_time_list, + running_list=running_list, + monitoring=monitoring, + clear_record_info=clear_record_info, + color_obj=color_obj, + ) + except ImportError: + from douyin_youtube_ffplay import start_douyin_youtube_ffplay + + start_douyin_youtube_ffplay( + title=title_in_name, + anchor_name=anchor_name, + real_url=real_url, + rec_info=rec_info, + record_name=record_name, + recording=recording, + recording_time_list=recording_time_list, + running_list=running_list, + clear_record_info=clear_record_info, + color_obj=color_obj, + ) count_time = time.time() diff --git a/scripts/env_check.py b/scripts/env_check.py index b9eb947..0b8080c 100644 --- a/scripts/env_check.py +++ b/scripts/env_check.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""启动前环境分析:Python 依赖、ffmpeg、Node/npm、端口提示(Windows / Linux 通用)。""" +"""启动前环境分析:Python 依赖、ffmpeg、Node/npm、端口提示(Linux)。""" from __future__ import annotations import importlib.util @@ -56,11 +56,11 @@ def print_env_report( ok = False if fp: print(f"│ ✓ ffplay: {fp}") - elif sys.platform != "win32": + else: print("│ ⚠ 未找到 ffplay(HDMI 全屏播放需要完整 ffmpeg 套件)") if need_npm: - npm = shutil.which("npm") or shutil.which("npm.cmd") + npm = shutil.which("npm") if npm: print(f"│ ✓ npm: {npm}") else: diff --git a/scripts/launch.py b/scripts/launch.py index 730bd94..adc2067 100644 --- a/scripts/launch.py +++ b/scripts/launch.py @@ -59,18 +59,11 @@ def load_runtime_env() -> None: def _which_npm() -> str | None: - return shutil.which("npm") or shutil.which("npm.cmd") + return shutil.which("npm") def _npm(args: list[str], *, cwd: Path, env: dict | None = None) -> int: full_env = {**os.environ, **(env or {})} - if sys.platform == "win32": - return subprocess.call( - "npm " + " ".join(args), - cwd=cwd, - shell=True, - env=full_env, - ) npm = _which_npm() if not npm: return 127 @@ -138,21 +131,17 @@ def run_dev(host: str, port: str, next_port: str) -> None: os.chdir(ROOT) api: subprocess.Popen | None = None ui: subprocess.Popen | None = None - next_args = f"npm run dev -- --hostname 0.0.0.0 --port {next_port}" try: api = subprocess.Popen(api_cmd, cwd=ROOT) time.sleep(0.6) if api.poll() is not None: print("[launch] API 进程异常退出,请检查端口是否被占用。") sys.exit(api.returncode or 1) - if sys.platform == "win32": - ui = subprocess.Popen(next_args, cwd=CONSOLE, shell=True, env=env) - else: - ui = subprocess.Popen( - ["npm", "run", "dev", "--", "--hostname", "0.0.0.0", "--port", next_port], - cwd=CONSOLE, - env=env, - ) + ui = subprocess.Popen( + ["npm", "run", "dev", "--", "--hostname", "0.0.0.0", "--port", next_port], + cwd=CONSOLE, + env=env, + ) print() print(" —— 开发模式(局域网可访问)——") print(f" 控制台界面: http://0.0.0.0:{next_port} 或用本机 IP + 端口") diff --git a/src/control_plane.py b/src/control_plane.py index 88647d0..e21c771 100644 --- a/src/control_plane.py +++ b/src/control_plane.py @@ -352,8 +352,6 @@ def _default_service_status(spec: ServiceSpec, detail: str) -> dict: async def query_service(spec: ServiceSpec) -> dict: - if sys.platform == "win32": - return _default_service_status(spec, "Linux-only stack integration") if not SERVICE_CTL.is_file(): return _default_service_status(spec, "Service controller script is missing") code, output = await _run_command(["bash", str(SERVICE_CTL), spec.service_id, "status"]) @@ -385,8 +383,6 @@ async def service_action(service_id: str, action: str) -> dict: spec = next((item for item in SERVICE_SPECS if item.service_id == service_id), None) if not spec: raise KeyError(f"Unknown service: {service_id}") - if sys.platform == "win32": - return _default_service_status(spec, "Linux-only stack integration") if not SERVICE_CTL.is_file(): return _default_service_status(spec, "Service controller script is missing") @@ -516,7 +512,7 @@ def network_iface_stats() -> dict[str, Any]: """Linux /proc/net/dev 累计流量(自开机或计数器复位以来)。""" dev = Path("/proc/net/dev") interfaces: list[dict[str, int | str]] = [] - if sys.platform == "win32" or not dev.is_file(): + if not dev.is_file(): return {"interfaces": interfaces} skip_prefixes = ("lo", "docker", "veth", "br-", "virbr", "tun", "tap") try: @@ -699,7 +695,7 @@ def _read_uptime_seconds() -> Optional[float]: def system_snapshot() -> dict: - disk = shutil.disk_usage(BASE_DIR.anchor if sys.platform == "win32" else "/") + disk = shutil.disk_usage("/") meminfo = _read_meminfo() load = os.getloadavg() if hasattr(os, "getloadavg") else (0.0, 0.0, 0.0) snapshot = { @@ -713,17 +709,16 @@ def system_snapshot() -> dict: "uptime_seconds": _read_uptime_seconds(), "netdata": {"available": False}, } - if sys.platform != "win32": - try: - with urlopen("http://127.0.0.1:19999/api/v1/info", timeout=1.5) as response: - payload = json.loads(response.read().decode("utf-8", "replace")) - snapshot["netdata"] = { - "available": True, - "version": payload.get("version", ""), - "mirrored_hosts": payload.get("mirrored_hosts", []), - } - except Exception: - pass + try: + with urlopen("http://127.0.0.1:19999/api/v1/info", timeout=1.5) as response: + payload = json.loads(response.read().decode("utf-8", "replace")) + snapshot["netdata"] = { + "available": True, + "version": payload.get("version", ""), + "mirrored_hosts": payload.get("mirrored_hosts", []), + } + except Exception: + pass return snapshot diff --git a/src/initializer.py b/src/initializer.py index 63a9718..f414c2e 100644 --- a/src/initializer.py +++ b/src/initializer.py @@ -12,81 +12,10 @@ 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(): @@ -136,23 +65,6 @@ def install_nodejs_ubuntu(): 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"]: @@ -162,20 +74,13 @@ def get_package_manager(): 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.") + if current_platform != "Linux": + logger.debug("Node.js 自动安装仅支持 Linux,请手动安装 Node.js。") return False + os_type = get_package_manager() + if os_type == "RHS": + return install_nodejs_centos() + return install_nodejs_ubuntu() def ensure_nodejs_installed(func): diff --git a/src/proxy.py b/src/proxy.py index 28e3148..dd6db8c 100644 --- a/src/proxy.py +++ b/src/proxy.py @@ -1,8 +1,6 @@ import os -import sys from enum import Enum, auto from dataclasses import dataclass, field -from .utils import logger class ProxyType(Enum): @@ -25,68 +23,37 @@ class ProxyInfo: 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 + """仅 Linux:从环境变量读取代理(http_proxy / https_proxy / ftp_proxy)。""" 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() + 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 + return self._is_proxy_enabled_linux() @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') + "http": os.getenv("http_proxy"), + "https": os.getenv("https_proxy"), + "ftp": os.getenv("ftp_proxy"), } ip = port = "" - for proto, proxy in proxies.items(): + for _proto, proxy in proxies.items(): if proxy: - ip, port = proxy.split(':') + # 支持 http://host:port + p = proxy.strip() + if "://" in p: + p = p.split("://", 1)[1] + if "@" in p: + p = p.split("@", 1)[1] + if ":" in p: + ip, _, port = p.rpartition(":") + port = port.split("/")[0] break return ip, port def _is_proxy_enabled_linux(self) -> bool: - proxies = self._get_proxy_info_linux() - return any(proxy != '' for proxy in proxies) + ip, port = self._get_proxy_info_linux() + return bool(ip and port) diff --git a/src/web_process_backend.py b/src/web_process_backend.py index 27a6ef1..1ba0318 100644 --- a/src/web_process_backend.py +++ b/src/web_process_backend.py @@ -1,5 +1,5 @@ """ -Web 控制台进程后端:优先 PM2(Linux/Windows 通用),不可用时回退为本地 PID 注册表。 +Web 控制台进程后端:优先 PM2,不可用时回退为本地 PID 注册表(仅 Linux)。 """ from __future__ import annotations @@ -101,46 +101,8 @@ def _signal_pids(pids: Iterable[int], sig: int) -> None: pass -def _windows_kill_stack_ffmpeg(process_name: str, project_root: Path | None) -> None: - """仅结束命令行命中业务标记的 ffmpeg.exe,避免 taskkill /IM 误杀其它 FFmpeg。""" - markers = _ffmpeg_cmdline_markers(process_name, project_root) - if not markers: - return - env = os.environ.copy() - env["DOUYIN_FFMPEG_MARKERS"] = json.dumps(markers, ensure_ascii=False) - ps = r""" -$raw = $env:DOUYIN_FFMPEG_MARKERS -if (-not $raw) { exit 0 } -$m = $raw | ConvertFrom-Json -foreach ($proc in Get-CimInstance Win32_Process -Filter "Name = 'ffmpeg.exe'") { - $cmd = $proc.CommandLine - if (-not $cmd) { continue } - foreach ($x in $m) { - if ($cmd -like ('*' + $x + '*')) { - Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue - break - } - } -} -""" - try: - subprocess.run( - ["powershell", "-NoProfile", "-NonInteractive", "-Command", ps], - env=env, - capture_output=True, - text=True, - timeout=45, - check=False, - ) - except (OSError, subprocess.TimeoutExpired): - pass - - async def force_kill_ffmpeg(process_name: str, project_root: Path | None = None) -> None: - """结束与本业务相关的 ffmpeg:Linux 按 /proc 命令行匹配 + 可选 pkill;Windows 按命令行标记筛选。""" - if sys.platform == "win32": - await asyncio.to_thread(_windows_kill_stack_ffmpeg, process_name, project_root) - return + """结束与本业务相关的 ffmpeg:按 /proc 命令行匹配 + 可选 pkill。""" def collect() -> list[int]: return _linux_ffmpeg_pids_for_stack(process_name, project_root) @@ -193,18 +155,6 @@ class LocalProcessRegistry: def _pid_alive(self, pid: int) -> bool: if pid <= 0: return False - if sys.platform == "win32": - try: - out = subprocess.check_output( - ["tasklist", "/FI", f"PID eq {pid}", "/FO", "CSV", "/NH"], - stderr=subprocess.DEVNULL, - text=True, - encoding="utf-8", - errors="ignore", - ) - return str(pid) in out - except Exception: - return False try: os.kill(pid, 0) return True @@ -214,14 +164,6 @@ class LocalProcessRegistry: def _kill_tree(self, pid: int) -> None: if pid <= 0: return - if sys.platform == "win32": - subprocess.run( - ["taskkill", "/PID", str(pid), "/T", "/F"], - capture_output=True, - text=True, - timeout=30, - ) - return try: os.killpg(os.getpgid(pid), signal.SIGTERM) except (ProcessLookupError, PermissionError, OSError): @@ -241,8 +183,6 @@ class LocalProcessRegistry: if not bash: return None return [bash, str(path)] - if script.lower().endswith((".bat", ".cmd")): - return ["cmd.exe", "/c", str(path)] return None async def start(self, name: str, script: str) -> str: @@ -256,27 +196,19 @@ class LocalProcessRegistry: cmd = self.build_command(script) if not cmd: - return f"ERROR: 无法启动 {script}(文件不存在或缺少 bash/cmd)" + return f"ERROR: 无法启动 {script}(文件不存在或缺少 bash/sh)" self.log_dir.mkdir(parents=True, exist_ok=True) out_log = open(self.log_dir / f"{name}-out.log", "a", encoding="utf-8", errors="ignore") err_log = open(self.log_dir / f"{name}-error.log", "a", encoding="utf-8", errors="ignore") - preexec_fn = os.setsid if sys.platform != "win32" else None - creationflags = 0 - if sys.platform == "win32": - creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) - try: kw: dict[str, Any] = { "cwd": str(self.base_dir), "stdout": out_log, "stderr": err_log, + "preexec_fn": os.setsid, } - if preexec_fn is not None: - kw["preexec_fn"] = preexec_fn - if creationflags: - kw["creationflags"] = creationflags proc = await asyncio.create_subprocess_exec(*cmd, **kw) except Exception as e: out_log.close() diff --git a/tiktok.py b/tiktok.py index 82a202c..bae3bd7 100644 --- a/tiktok.py +++ b/tiktok.py @@ -83,8 +83,7 @@ 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" +clear_command = "clear" color_obj = utils.Color() os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path @@ -188,8 +187,7 @@ def display_info2() -> None: try: sys.stdout.flush() time.sleep(5) - if Path(sys.executable).name != 'pythonw.exe': - os.system(clear_command) + os.system(clear_command) print(f"\r共监测{monitoring}个直播中", end=" | ") print(f"同一时间访问网络的线程数: {max_request}", end=" | ") print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ") @@ -271,15 +269,6 @@ def delete_line(file_path: str, del_line: str, delete_all: bool = False) -> None 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: @@ -298,7 +287,7 @@ def segment_video(converts_file_path: str, segment_save_file_path: str, segment_ segment_save_file_path, ] _output = subprocess.check_output( - ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ffmpeg_command, stderr=subprocess.STDOUT ) if is_original_delete: time.sleep(1) @@ -333,7 +322,7 @@ def converts_mp4(converts_file_path: str, is_original_delete: bool = True) -> No "-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) + ffmpeg_command, stderr=subprocess.STDOUT ) if is_original_delete: time.sleep(1) @@ -353,7 +342,7 @@ def converts_m4a(converts_file_path: str, is_original_delete: bool = True) -> No "-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)) + ], stderr=subprocess.STDOUT) if is_original_delete: time.sleep(1) if os.path.exists(converts_file_path): @@ -450,7 +439,7 @@ def push_message(record_name: str, live_url: str, content: str) -> None: 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) + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout, stderr = process.communicate() stdout_decoded = stdout.decode('utf-8') @@ -515,7 +504,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa 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) + ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT ) subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] @@ -532,12 +521,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa 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.send_signal(signal.SIGINT) process.wait() return True time.sleep(1) @@ -1625,23 +1609,41 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: error_window.append(1) else: - import sys - sys.path.insert(0, './dist') - from douyin_srs_ffplay import start_douyin_srs_ffplay - # 调用 - start_douyin_srs_ffplay( - title=title_in_name, - anchor_name=anchor_name, - real_url=real_url, - rec_info=rec_info, - record_name=record_name, - recording=recording, - recording_time_list=recording_time_list, - running_list=running_list, - monitoring=monitoring, - clear_record_info=clear_record_info, - color_obj=color_obj - ) + _repo_root = Path(__file__).resolve().parent + _dist = _repo_root / "dist" + if _dist.is_dir(): + sys.path.insert(0, str(_dist)) + try: + from douyin_srs_ffplay import start_douyin_srs_ffplay + + start_douyin_srs_ffplay( + title=title_in_name, + anchor_name=anchor_name, + real_url=real_url, + rec_info=rec_info, + record_name=record_name, + recording=recording, + recording_time_list=recording_time_list, + running_list=running_list, + monitoring=monitoring, + clear_record_info=clear_record_info, + color_obj=color_obj, + ) + except ImportError: + from douyin_youtube_ffplay import start_douyin_youtube_ffplay + + start_douyin_youtube_ffplay( + title=title_in_name, + anchor_name=anchor_name, + real_url=real_url, + rec_info=rec_info, + record_name=record_name, + recording=recording, + recording_time_list=recording_time_list, + running_list=running_list, + clear_record_info=clear_record_info, + color_obj=color_obj, + ) count_time = time.time() diff --git a/tools/bootstrap_remote_repo_paramiko.py b/tools/bootstrap_remote_repo_paramiko.py new file mode 100644 index 0000000..a711060 --- /dev/null +++ b/tools/bootstrap_remote_repo_paramiko.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""新 git clone 后:修正属主、创建 .venv、安装 requirements(需与 deploy_live_paramiko 相同环境变量)。""" +from __future__ import annotations + +import os +import sys + +try: + import paramiko +except ImportError: + print("请先安装: pip install paramiko", file=sys.stderr) + sys.exit(1) + +HOST = os.environ.get("LIVE_DEPLOY_HOST", "192.168.21.105") +USER = os.environ.get("LIVE_DEPLOY_USER", "live") +PASS = os.environ.get("LIVE_DEPLOY_PASS", "12345678") +REMOTE_BASE = os.environ.get("LIVE_DEPLOY_PATH", "/home/live/douyinyoutube").rstrip("/") + + +def main() -> int: + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect(HOST, username=USER, password=PASS, timeout=30, banner_timeout=40, auth_timeout=30) + try: + def run(title: str, cmd: str, timeout: int = 600) -> int: + print(f"\n=== {title} ===") + wrapped = ( + "export LC_ALL=C.UTF-8 LANG=C.UTF-8; " + f"cd {REMOTE_BASE} 2>/dev/null || exit 9; " + + cmd + ) + _, stdout, stderr = c.exec_command(wrapped, timeout=timeout, get_pty=False) + out = (stdout.read() or b"").decode("utf-8", "replace") + err = (stderr.read() or b"").decode("utf-8", "replace") + code = stdout.channel.recv_exit_status() + if out.strip(): + print(out.rstrip()) + if err.strip(): + print(err.rstrip(), file=sys.stderr) + print(f"exit={code}") + return code + + code = run( + "chown (root 克隆后必做)", + f"echo '{PASS}' | sudo -S chown -R {USER}:{USER} {REMOTE_BASE} && echo chown_ok", + timeout=60, + ) + if code != 0: + return 1 + + run("chmod +x 常用脚本", "chmod +x start.sh web.sh dev.sh 2>/dev/null; chmod +x scripts/launch.py 2>/dev/null; echo chmod_ok", timeout=20) + + venv_cmd = ( + f"if [ -x .venv/bin/pip ]; then echo venv_exists; " + f"else python3 -m venv .venv && echo venv_created; fi && " + f".venv/bin/pip install -q --upgrade pip && " + f".venv/bin/pip install -r requirements.txt" + ) + code = run("venv + pip install -r requirements.txt", venv_cmd, timeout=900) + if code != 0: + return 2 + + print("\n完成。接着在本机执行: python tools/deploy_live_paramiko.py") + return 0 + finally: + c.close() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/web.py b/web.py index b5f019c..3891dc1 100644 --- a/web.py +++ b/web.py @@ -81,7 +81,7 @@ def _sort_script_entries(entries: list[dict], web_script_name: str) -> list[dict def discover_script_entries() -> tuple: - """扫描根目录:tiktok*.py、youtube*.py、douyin_youtube*.py;OBS 在 Windows 用 obs*.bat/.cmd,在类 Unix 用 obs*.sh。""" + """扫描根目录:tiktok*.py、youtube*.py、douyin_youtube*.py;OBS 仅 obs*.sh。""" base = BASE_DIR web_path = Path(__file__).resolve() entries: list[dict] = [] @@ -108,14 +108,9 @@ def discover_script_entries() -> tuple: if path.is_file() and path.resolve() != web_path and path.name not in _douyin_youtube_skip: add(path.name, "douyin_youtube") - if sys.platform == "win32": - for path in sorted(list(base.glob("obs*.bat")) + list(base.glob("obs*.cmd"))): - if path.is_file(): - add(path.name, "obs") - else: - for path in sorted(base.glob("obs*.sh")): - if path.is_file(): - add(path.name, "obs") + for path in sorted(base.glob("obs*.sh")): + if path.is_file(): + add(path.name, "obs") entries.append({"script": web_path.name, "label": _label_short("web")}) entries = _sort_script_entries(entries, web_path.name) diff --git a/youtube.py b/youtube.py index 343ca4c..c8bf76e 100644 --- a/youtube.py +++ b/youtube.py @@ -83,8 +83,7 @@ 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" +clear_command = "clear" color_obj = utils.Color() os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path @@ -188,8 +187,7 @@ def display_info2() -> None: try: sys.stdout.flush() time.sleep(5) - if Path(sys.executable).name != 'pythonw.exe': - os.system(clear_command) + os.system(clear_command) print(f"\r共监测{monitoring}个直播中", end=" | ") print(f"同一时间访问网络的线程数: {max_request}", end=" | ") print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ") @@ -271,15 +269,6 @@ def delete_line(file_path: str, del_line: str, delete_all: bool = False) -> None 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: @@ -298,7 +287,7 @@ def segment_video(converts_file_path: str, segment_save_file_path: str, segment_ segment_save_file_path, ] _output = subprocess.check_output( - ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ffmpeg_command, stderr=subprocess.STDOUT ) if is_original_delete: time.sleep(1) @@ -333,7 +322,7 @@ def converts_mp4(converts_file_path: str, is_original_delete: bool = True) -> No "-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) + ffmpeg_command, stderr=subprocess.STDOUT ) if is_original_delete: time.sleep(1) @@ -353,7 +342,7 @@ def converts_m4a(converts_file_path: str, is_original_delete: bool = True) -> No "-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)) + ], stderr=subprocess.STDOUT) if is_original_delete: time.sleep(1) if os.path.exists(converts_file_path): @@ -450,7 +439,7 @@ def push_message(record_name: str, live_url: str, content: str) -> None: 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) + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout, stderr = process.communicate() stdout_decoded = stdout.decode('utf-8') @@ -515,7 +504,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa 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) + ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT ) subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] @@ -532,12 +521,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa 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.send_signal(signal.SIGINT) process.wait() return True time.sleep(1) @@ -1627,11 +1611,8 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: error_window.append(1) else: - import sys - sys.path.insert(0, './dist') from douyin_youtube_ffplay import start_douyin_youtube_ffplay - # 调用(与 douyin_srs_ffplay 调用方式完全一致) start_douyin_youtube_ffplay( title=title_in_name, anchor_name=anchor_name,