文件
2026-04-09 19:53:24 +08:00

582 行
24 KiB
Python

#!/usr/bin/env python3
"""
SCL 插件下载器 (scd)
用于下载和更新 SCL 插件的工具
"""
import os
import sys
import json
import time
import argparse
import threading
import concurrent.futures
import urllib.request
import urllib.error
import platform
import subprocess
from datetime import datetime
# 版本信息
VERSION = "1.0.0"
# 默认镜像源
DEFAULT_MIRROR = "https://scl.ecuil.com/run/plugins/"
# 配置文件路径
CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".scd_config.json")
# 多语言支持
LANGUAGES = {
"en": {
"installing_plugin": "Installing plugin: {}",
"downloading_from": "Downloading from: {}",
"plugin_installed": "Plugin {} installed successfully!",
"install_failed": "Failed to install plugin {}",
"installing_from_file": "Installing plugins from file: {}",
"file_not_found": "File {} not found",
"no_plugins_in_file": "No plugins found in file",
"all_plugins_installed": "All plugins installed!",
"reading_file_failed": "Failed to read file: {}",
"updating_plugin": "Updating plugin: {}",
"updating_scd": "Updating scd...",
"local_version": "Local version: {}",
"remote_version": "Remote version: {}",
"found_new_version": "Found new version, downloading...",
"unsupported_os": "Unsupported operating system",
"update_failed": "Failed to download update",
"scd_updated": "scd updated successfully!",
"run_new_scd": "Please run the new scd{}",
"already_latest": "{} is already up to date",
"listing_local_plugins": "Listing local plugins...",
"installed_plugins": "Installed plugins:",
"no_plugins_installed": "No plugins installed",
"plugins_dir_not_found": "Plugins directory not found",
"listing_web_plugins": "Listing web plugins...",
"fetching_plugins_from": "Fetching plugins from: {}",
"available_plugins": "Available plugins:",
"no_available_plugins": "No available plugins",
"invalid_api_response": "Invalid API response",
"install_required_packages": "Error: Please install required packages: pip install requests",
"fetch_failed": "Failed to fetch plugins list: {}",
"check_network": "Please check your network connection or mirror URL",
"available_mirrors": "Available mirrors:",
"adding_mirror": "Mirror {} added and set as current mirror",
"mirror_exists": "Mirror {} already exists",
"scd_info": "SCL Plugin Downloader (scd)",
"version": "Version: 1.0.0",
"current_mirror": "Current mirror: {}",
"available_mirrors_count": "Available mirrors: {}",
"cpu_cores": "CPU cores: {}",
"download_threads": "Download threads: {}",
"loading_config_failed": "Failed to load config file: {}",
"saving_config_failed": "Failed to save config file: {}",
"downloading_file": "Downloading: {}",
"download_failed": "Failed to download file: {}",
"error": "Error: {}",
"warning_fallback": "Warning: Using fallback download method. Install requests and tqdm for better progress bars.",
"language_set": "Language set to: {}",
"language_not_supported": "Language not supported. Available languages: en, zh",
"setting_language": "Setting language to: {}",
"current_language": "Current language: {}"
},
"zh": {
"installing_plugin": "正在安装插件: {}",
"downloading_from": "从以下地址下载: {}",
"plugin_installed": "插件 {} 安装成功!",
"install_failed": "安装插件 {} 失败",
"installing_from_file": "从文件安装插件: {}",
"file_not_found": "文件 {} 不存在",
"no_plugins_in_file": "文件中未找到插件",
"all_plugins_installed": "所有插件安装完成!",
"reading_file_failed": "读取文件失败: {}",
"updating_plugin": "正在更新插件: {}",
"updating_scd": "正在更新 scd...",
"local_version": "本地版本: {}",
"remote_version": "远程版本: {}",
"found_new_version": "发现新版本,正在下载...",
"unsupported_os": "不支持的操作系统",
"update_failed": "下载更新失败",
"scd_updated": "scd 更新成功!",
"run_new_scd": "请运行新的 scd{}",
"already_latest": "{} 已是最新版本,无需更新",
"listing_local_plugins": "正在列出本地插件...",
"installed_plugins": "已安装的插件:",
"no_plugins_installed": "没有安装插件",
"plugins_dir_not_found": "未找到插件目录",
"listing_web_plugins": "正在列出网络插件...",
"fetching_plugins_from": "从以下地址获取插件: {}",
"available_plugins": "可用的插件:",
"no_available_plugins": "没有可用的插件",
"invalid_api_response": "API 响应无效",
"install_required_packages": "错误: 请安装必要的包: pip install requests",
"fetch_failed": "获取插件列表失败: {}",
"check_network": "请检查网络连接或镜像 URL",
"available_mirrors": "可用的镜像源:",
"adding_mirror": "镜像 {} 已添加并设为当前镜像",
"mirror_exists": "镜像 {} 已存在",
"scd_info": "SCL 插件下载器 (scd)",
"version": "版本: 1.0.0",
"current_mirror": "当前镜像: {}",
"available_mirrors_count": "可用镜像: {}",
"cpu_cores": "CPU 核心数: {}",
"download_threads": "下载线程数: {}",
"loading_config_failed": "加载配置文件失败: {}",
"saving_config_failed": "保存配置文件失败: {}",
"downloading_file": "下载中: {}",
"download_failed": "下载文件失败: {}",
"error": "错误: {}",
"warning_fallback": "警告: 使用后备下载方法。安装 requests 和 tqdm 以获得更好的进度条。",
"language_set": "语言已设置为: {}",
"language_not_supported": "不支持的语言。可用语言: en, zh",
"setting_language": "正在设置语言为: {}",
"current_language": "当前语言: {}"
}
}
class SCDownloader:
def __init__(self):
self.config = self.load_config()
self.mirrors = self.config.get("mirrors", [DEFAULT_MIRROR])
self.current_mirror = self.config.get("current_mirror", DEFAULT_MIRROR)
self.language = self.config.get("language", "zh") # 默认中文
# 获取 CPU 核心数,用于多线程下载
self.cpu_cores = os.cpu_count() or 4
self.threads = max(1, self.cpu_cores // 2)
def _(self, key, *args):
"""获取翻译文本"""
lang = self.language if self.language in LANGUAGES else "zh"
return LANGUAGES[lang].get(key, key).format(*args)
def load_config(self):
"""加载配置文件"""
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(self._("loading_config_failed", e))
return {"mirrors": [DEFAULT_MIRROR], "current_mirror": DEFAULT_MIRROR, "language": "zh"}
def save_config(self):
"""保存配置文件"""
self.config["mirrors"] = self.mirrors
self.config["current_mirror"] = self.current_mirror
self.config["language"] = self.language
try:
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(self.config, f, indent=2)
except Exception as e:
print(self._("saving_config_failed", e))
def add_mirror(self, mirror_url):
"""添加镜像源"""
if mirror_url not in self.mirrors:
self.mirrors.append(mirror_url)
self.current_mirror = mirror_url
self.save_config()
print(self._("adding_mirror", mirror_url))
else:
print(self._("mirror_exists", mirror_url))
def set_language(self, lang):
"""设置语言"""
if lang in LANGUAGES:
print(self._("setting_language", lang))
self.language = lang
self.save_config()
print(self._("language_set", lang))
else:
print(self._("language_not_supported"))
def download_file(self, url, output_path, show_progress=True):
"""下载文件并显示进度(模范 PIP 风格)"""
try:
import requests
from tqdm import tqdm
# 发送 HEAD 请求获取文件大小
response = requests.head(url)
total_size = int(response.headers.get('content-length', 0))
# 下载文件并显示进度条
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(output_path, 'wb') as f:
with tqdm(
total=total_size,
unit='B',
unit_scale=True,
unit_divisor=1024,
desc=os.path.basename(output_path),
bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{rate_fmt}]'
) as pbar:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
pbar.update(len(chunk))
return True
except ImportError:
# 如果没有 requests 和 tqdm,使用 urllib 作为后备
print(self._("warning_fallback"))
def progress_hook(count, block_size, total_size):
if total_size > 0:
percent = int(count * block_size * 100 / total_size)
if show_progress:
# 动画效果:旋转指示器 + 进度条
spinner = "|/-\\"
spinner_pos = int(time.time() * 10) % 4
bar_length = 40
filled_length = int(bar_length * percent / 100)
bar = "█" * filled_length + "-" * (bar_length - filled_length)
sys.stdout.write(f"\r{spinner[spinner_pos]} 下载中: [{bar}] {percent}% ")
sys.stdout.flush()
# 使用 urllib 下载文件
urllib.request.urlretrieve(url, output_path, reporthook=progress_hook)
if show_progress:
sys.stdout.write("\r" + " " * 80 + "\r")
sys.stdout.flush()
return True
except urllib.error.URLError as e:
print(f"\n{self._('download_failed', e)}")
return False
except Exception as e:
print(f"\n{self._('error', e)}")
return False
def install_plugin(self, plugin_name):
"""安装插件"""
print(self._("installing_plugin", plugin_name))
# 构建插件下载 URL
plugin_url = f"{self.current_mirror}{plugin_name}.py"
plugin_path = os.path.join("plugins", f"{plugin_name}.py")
# 确保 plugins 目录存在
os.makedirs("plugins", exist_ok=True)
# 下载插件
print(self._("downloading_from", plugin_url))
if self.download_file(plugin_url, plugin_path):
print(self._("plugin_installed", plugin_name))
return True
else:
print(self._("install_failed", plugin_name))
return False
def install_from_file(self, file_path):
"""从文件安装多个插件"""
print(self._("installing_from_file", file_path))
if not os.path.exists(file_path):
print(self._("file_not_found", file_path))
return False
try:
with open(file_path, "r", encoding="utf-8") as f:
plugins = [line.strip() for line in f if line.strip() and not line.strip().startswith("#")]
if not plugins:
print(self._("no_plugins_in_file"))
return False
# 使用线程池下载插件
with concurrent.futures.ThreadPoolExecutor(max_workers=self.threads) as executor:
future_to_plugin = {executor.submit(self.install_plugin, plugin): plugin for plugin in plugins}
for future in concurrent.futures.as_completed(future_to_plugin):
plugin = future_to_plugin[future]
try:
future.result()
except Exception as e:
print(f"{self._('error', e)}")
print(self._("all_plugins_installed"))
return True
except Exception as e:
print(self._("reading_file_failed", e))
return False
def get_plugin_version(self, plugin_name):
"""获取插件版本信息"""
# 尝试从本地插件文件中提取版本信息
plugin_path = os.path.join("plugins", f"{plugin_name}.py")
if os.path.exists(plugin_path):
try:
with open(plugin_path, "r", encoding="utf-8") as f:
content = f.read()
# 查找版本信息
import re
version_match = re.search(r'VERSION\s*=\s*["\'](.*?)["\']', content)
if version_match:
return version_match.group(1)
except Exception as e:
pass
return "0.0.0"
def get_remote_plugin_version(self, plugin_name):
"""获取远程插件版本信息"""
try:
import requests
version_url = f"{self.current_mirror}{plugin_name}.json"
response = requests.get(version_url)
response.raise_for_status()
version_data = response.json()
return version_data.get("version", "0.0.0")
except Exception as e:
print(self._("error", e))
return "0.0.0"
def compare_versions(self, v1, v2):
"""比较版本号"""
def normalize_version(v):
return [int(x) for x in v.split(".") if x.isdigit()]
v1_parts = normalize_version(v1)
v2_parts = normalize_version(v2)
for i in range(max(len(v1_parts), len(v2_parts))):
v1_part = v1_parts[i] if i < len(v1_parts) else 0
v2_part = v2_parts[i] if i < len(v2_parts) else 0
if v1_part < v2_part:
return -1
elif v1_part > v2_part:
return 1
return 0
def get_scd_version(self):
"""获取 scd 版本信息"""
# 尝试从当前文件中提取版本信息
scd_path = os.path.abspath(__file__)
try:
with open(scd_path, "r", encoding="utf-8") as f:
content = f.read()
# 查找版本信息
import re
version_match = re.search(r'VERSION\s*=\s*["\'](.*?)["\']', content)
if version_match:
return version_match.group(1)
except Exception as e:
pass
return "0.0.0"
def get_remote_scd_version(self):
"""获取远程 scd 版本信息"""
try:
import requests
version_url = "https://scl.ecuil.com/scd.json"
response = requests.get(version_url)
response.raise_for_status()
version_data = response.json()
return version_data.get("v", "0.0.0")
except Exception as e:
print(self._("error", e))
return "0.0.0"
def update_plugin(self, plugin_name):
"""更新插件"""
if plugin_name == "scd":
# 更新 scd 本身
print(self._("updating_scd"))
# 获取本地版本
local_version = self.get_scd_version()
print(self._("local_version", local_version))
# 获取远程版本
remote_version = self.get_remote_scd_version()
print(self._("remote_version", remote_version))
# 比较版本
if self.compare_versions(local_version, remote_version) < 0:
print(self._("found_new_version"))
# 根据操作系统类型选择下载文件
import platform
system = platform.system()
if system == "Windows":
scd_url = "https://scl.ecuil.com/scd.exe"
scd_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "scd.exe")
elif system == "Linux":
scd_url = "https://scl.ecuil.com/scd"
scd_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "scd")
else:
print(self._("unsupported_os"))
return False
# 下载更新
if self.download_file(scd_url, scd_path):
# 如果是 Linux,设置可执行权限
if system == "Linux":
try:
import os
os.chmod(scd_path, 0o755)
except Exception as e:
print(self._("error", e))
print(self._("scd_updated"))
print(self._("run_new_scd", "文件" if system == "Windows" else "可执行文件"))
return True
else:
print(self._("update_failed"))
return False
else:
print(self._("already_latest", "scd"))
return True
else:
# 更新普通插件
print(self._("updating_plugin", plugin_name))
# 获取本地版本
local_version = self.get_plugin_version(plugin_name)
print(self._("local_version", local_version))
# 获取远程版本
remote_version = self.get_remote_plugin_version(plugin_name)
print(self._("remote_version", remote_version))
# 比较版本
if self.compare_versions(local_version, remote_version) < 0:
print(self._("found_new_version"))
return self.install_plugin(plugin_name)
else:
print(self._("already_latest", f"插件 {plugin_name}"))
return True
def list_plugins(self, mode="local"):
"""列出可用插件"""
if mode == "local":
print(self._("listing_local_plugins"))
plugins_dir = "plugins"
if os.path.exists(plugins_dir):
plugins = [f[:-3] for f in os.listdir(plugins_dir) if f.endswith(".py")]
if plugins:
print(self._("installed_plugins"))
for plugin in plugins:
print(f" - {plugin}")
else:
print(self._("no_plugins_installed"))
else:
print(self._("plugins_dir_not_found"))
elif mode == "web":
print(self._("listing_web_plugins"))
print(self._("fetching_plugins_from", self.current_mirror))
try:
# 尝试从 API 获取插件列表
import requests
plugins_url = f"{self.current_mirror}plugins.php"
response = requests.get(plugins_url)
response.raise_for_status()
plugins_data = response.json()
if plugins_data.get("status") == "success" and "plugins" in plugins_data:
plugins = plugins_data["plugins"]
if plugins:
print(self._("available_plugins"))
for plugin in plugins:
print(f" - {plugin}")
else:
print(self._("no_available_plugins"))
else:
print(self._("invalid_api_response"))
except ImportError:
print(self._("install_required_packages"))
except Exception as e:
print(self._("fetch_failed", e))
print(self._("check_network"))
def show_mirrors(self):
"""显示可用镜像源"""
print(self._("available_mirrors"))
for i, mirror in enumerate(self.mirrors):
marker = "*" if mirror == self.current_mirror else " "
print(f"{marker} [{i+1}] {mirror}")
def run(self, args):
"""运行命令"""
if args.command == "install":
if args.file:
self.install_from_file(args.file)
else:
for plugin in args.plugins:
self.install_plugin(plugin)
elif args.command == "update":
for plugin in args.plugins:
self.update_plugin(plugin)
elif args.command == "mirror":
if args.url:
self.add_mirror(args.url)
else:
self.show_mirrors()
elif args.command == "list":
if hasattr(args, 'mode') and args.mode:
self.list_plugins(args.mode)
else:
self.list_plugins()
elif args.command == "info":
print(self._("scd_info"))
print(self._("version"))
print(self._("current_mirror", self.current_mirror))
print(self._("available_mirrors_count", len(self.mirrors)))
print(self._("cpu_cores", self.cpu_cores))
print(self._("download_threads", self.threads))
print(self._("current_language", self.language))
elif args.command == "language":
if args.lang:
self.set_language(args.lang)
else:
print(self._("current_language", self.language))
def main():
parser = argparse.ArgumentParser(description="SCL 插件下载器")
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# install 命令
install_parser = subparsers.add_parser("install", help="安装插件")
install_parser.add_argument("plugins", nargs="*", help="要安装的插件名称")
install_parser.add_argument("-f", "--file", help="包含插件名称的文件")
install_parser.add_argument("-t", action="store_true", help="从 plugins.txt 安装")
# update 命令
update_parser = subparsers.add_parser("update", help="更新插件")
update_parser.add_argument("plugins", nargs="*", help="要更新的插件名称")
# mirror 命令
mirror_parser = subparsers.add_parser("mirror", help="管理镜像源")
mirror_parser.add_argument("url", nargs="?", help="要添加的镜像 URL")
# list 命令
list_parser = subparsers.add_parser("list", help="列出插件")
list_parser.add_argument("mode", nargs="?", choices=["local", "web"], default="local", help="列出模式: local 或 web")
# info 命令
subparsers.add_parser("info", help="显示 scd 信息")
# language 命令
language_parser = subparsers.add_parser("language", help="管理语言设置")
language_parser.add_argument("lang", nargs="?", choices=["en", "zh"], help="要设置的语言: en 或 zh")
args = parser.parse_args()
# 处理 -t 参数
if args.command == "install" and args.t:
args.file = "plugins.txt"
if not args.command:
parser.print_help()
return
downloader = SCDownloader()
downloader.run(args)
if __name__ == "__main__":
main()