好的,这是一份可以直接复制粘贴到 GitHub README.md 文件中的完整手册。
它整合了您编写的部署指南、我们最终完善的脚本代码,并采用标准的 GitHub Markdown 格式,包含了标题、特性列表、代码折叠等元素,使其看起来非常专业和清晰。
qBittorrent Ratio Management Daemon
一个工程师级的 qBittorrent 守护进程,用于根据分享率自动管理种子的状态。
它会在后台安静地运行,自动监控指定分类的种子。当分享率达到设定阈值后,它会自动暂停该种子;暂停一段时间后,再自动恢复。
本项目遵循 Linux 最佳实践,通过 systemd 部署为一个安全、稳定且能自我恢复的后台服务。一旦配置好,它会默默地为你值班,像个不喝咖啡的老运维。
✨ 特性
自动监控:定期检查指定分类下所有种子的分享率。
智能暂停/恢复:达到分享率阈值后自动暂停,暂停期满后自动恢复。
状态持久化:将自己暂停的种子列表保存在本地,即使脚本或服务器重启,状态也不会丢失。
登录续期:自动处理 qBittorrent Web UI 的登录会话过期问题。
异常自恢复:当无法连接到 qBittorrent 时,会自动进入冷却期并尝试重连,而不是崩溃退出。
守护进程化:通过 systemd 管理,实现开机自启、进程守护和统一的日志记录。
轻量可靠:资源占用极低,逻辑清晰,稳如老狗,不乱报错。
📜 脚本代码
点击展开/折叠 qb_ratio_manager.py 脚本代码
code Python download content_copy expand_less import requests import time import json from datetime import datetime, timedelta import os
QB_HOST = "localhost" QB_PORT = 8080
QB_USERNAME = "admin" QB_PASSWORD = "your_password"
TARGET_CATEGORY = "my-category"
TARGET_RATIO = 2.0
PAUSE_MINUTES = 60
CHECK_INTERVAL_SECONDS = 60
CONNECTION_FAILURE_COOLDOWN = 300
REQUEST_TIMEOUT = 10
STATE_FILE = "qb_ratio_manager_state.json"
BASE_URL = f"http://{QB_HOST}:{QB_PORT}"
session = requests.Session()
paused_by_script = {}
def log(message): """带时间戳的日志输出""" print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}")
def login(): """登录到 qBittorrent API 并保持会话""" log("正在尝试登录...") try: url = f"{BASE_URL}/api/v2/auth/login" data = {"username": QB_USERNAME, "password": QB_PASSWORD} response = session.post(url, data=data, timeout=REQUEST_TIMEOUT) response.raise_for_status() if "Ok." in response.text: log("成功登录到 qBittorrent API。") return True else: log(f"登录失败,响应: {response.text}") return False except requests.exceptions.RequestException as e: log(f"登录时连接 qBittorrent 失败: {e}") return False
def ensure_login(): """检查登录状态,如果过期则重新登录""" try: url = f"{BASE_URL}/api/v2/app/version" response = session.get(url, timeout=REQUEST_TIMEOUT) if response.status_code == 403: # Forbidden log("登录会话已过期,正在重新登录...") return login() response.raise_for_status() return True except requests.exceptions.RequestException: raise
def load_state(): """从文件加载脚本暂停的种子状态""" global paused_by_script if not os.path.exists(STATE_FILE): log("未找到状态文件,将创建新的状态。") return try: with open(STATE_FILE, "r") as f: if os.path.getsize(STATE_FILE) == 0: log("状态文件为空,将使用空的状态开始。") return iso_data = json.load(f) paused_by_script = {h: datetime.fromisoformat(t) for h, t in iso_data.items()} log(f"成功从 {STATE_FILE} 加载了 {len(paused_by_script)} 条状态记录。") except (json.JSONDecodeError, IOError) as e: log(f"加载状态文件失败: {e}。将使用空的状态开始。") paused_by_script = {}
def save_state(): """将脚本暂停的种子状态保存到文件""" try: with open(STATE_FILE, "w") as f: iso_data = {h: t.isoformat() for h, t in paused_by_script.items()} json.dump(iso_data, f, indent=4) except IOError as e: log(f"保存状态文件失败: {e}")
def get_torrents(): """获取指定分类下的所有种子信息""" try: url = f"{BASE_URL}/api/v2/torrents/info" params = {"category": TARGET_CATEGORY, "filter": "all"} response = session.get(url, params=params, timeout=REQUEST_TIMEOUT) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: log(f"获取种子列表失败: {e}") except json.JSONDecodeError: log("解析种子列表失败,收到的不是有效的 JSON。") return None
def pause_torrents(hashes): """暂停指定的种子""" if not hashes: return try: url = f"{BASE_URL}/api/v2/torrents/pause" session.post(url, data={"hashes": "|".join(hashes)}, timeout=REQUEST_TIMEOUT).raise_for_status() log(f"已暂停 {len(hashes)} 个种子: {', '.join(hashes)}") except requests.exceptions.RequestException as e: log(f"暂停种子失败: {e}")
def resume_torrents(hashes): """恢复指定的种子""" if not hashes: return try: url = f"{BASE_URL}/api/v2/torrents/resume" session.post(url, data={"hashes": "|".join(hashes)}, timeout=REQUEST_TIMEOUT).raise_for_status() log(f"已恢复 {len(hashes)} 个种子: {', '.join(hashes)}") except requests.exceptions.RequestException as e: log(f"恢复种子失败: {e}")
def main_loop(): """主循环,执行监控和管理任务""" if not ensure_login(): log("无法维持登录状态,跳过此次检查。") return now = datetime.now() state_changed = False torrents_to_resume = [h for h, t in paused_by_script.items() if now >= t + timedelta(minutes=PAUSE_MINUTES)] if torrents_to_resume: resume_torrents(torrents_to_resume) for h in torrents_to_resume: paused_by_script.pop(h, None) state_changed = True torrents = get_torrents() if torrents is None: return torrents_to_pause = [t['hash'] for t in torrents if t['ratio'] >= TARGET_RATIO and 'paused' not in t['state'].lower() and t['hash'] not in paused_by_script] if torrents_to_pause: pause_torrents(torrents_to_pause) for h in torrents_to_pause: paused_by_script[h] = now state_changed = True if state_changed: save_state() log(f"检查完成。脚本正在管理 {len(paused_by_script)} 个种子。")
if name == "main": load_state() if not login(): log("初始登录失败,请检查配置。脚本将在冷却后重试。") time.sleep(CONNECTION_FAILURE_COOLDOWN) log("开始监控任务... 服务已启动。") while True: try: main_loop() time.sleep(CHECK_INTERVAL_SECONDS) except requests.exceptions.ConnectionError as e: log(f"无法连接到 qBittorrent: {e}。将在 {CONNECTION_FAILURE_COOLDOWN // 60} 分钟后重试。") time.sleep(CONNECTION_FAILURE_COOLDOWN) except KeyboardInterrupt: break log("服务正在停止... 正在保存最终状态...") save_state() log("状态已保存。服务已退出。")
🚀 在 Debian 12 上的部署指南
本指南将引导你将脚本部署为一个稳定、可自恢复的 systemd 后台服务。
🧩 前提条件
你已拥有一台 Debian 12 服务器并可通过 SSH 登录。
你拥有 sudo 权限。
你的 qBittorrent Web UI 已启用。
第 0 步:准备环境和依赖
更新软件包列表
code Bash download content_copy expand_less sudo apt update
安装 Python 3 与 pip
code Bash download content_copy expand_less sudo apt install python3 python3-pip -y
安装 requests 库
code Bash download content_copy expand_less pip3 install requests
(推荐)创建专用用户 创建一个最小权限用户来运行脚本,以增强安全性。
code Bash download content_copy expand_less sudo adduser --system --group --no-create-home qb-manager 第 1 步:部署脚本
创建应用目录
code Bash download content_copy expand_less sudo mkdir /opt/qb-manager
创建脚本文件
code Bash download content_copy expand_less sudo nano /opt/qb-manager/qb_ratio_manager.py
将上面的 Python 脚本代码完整粘贴进去。切记修改文件顶部的用户配置部分,填入你的 qBittorrent 信息。
设置所有权
code Bash download content_copy expand_less sudo chown -R qb-manager:qb-manager /opt/qb-manager 第 2 步:创建 systemd 服务文件
这是部署的核心,它告诉系统如何管理我们的脚本。
创建服务文件
code Bash download content_copy expand_less sudo nano /etc/systemd/system/qb-manager.service
粘贴服务配置
code Ini download content_copy expand_less [Unit] Description=qBittorrent Ratio Management Daemon After=network.target
[Service] User=qb-manager Group=qb-manager WorkingDirectory=/opt/qb-manager ExecStart=/usr/bin/python3 /opt/qb-manager/qb_ratio_manager.py Restart=on-failure RestartSec=30 StandardOutput=journal StandardError=journal
[Install] WantedBy=multi-user.target 第 3 步:启动与管理服务
重载 systemd 配置
code Bash download content_copy expand_less sudo systemctl daemon-reload
启动服务
code Bash download content_copy expand_less sudo systemctl start qb-manager.service
检查服务状态
code Bash download content_copy expand_less sudo systemctl status qb-manager.service
如果一切正常,Active: 状态应显示为 active (running)。
设置开机自启
code Bash download content_copy expand_less sudo systemctl enable qb-manager.service 第 4 步:查看日志与维护
服务的所有输出都由 journald 统一管理。
查看完整日志:
code Bash download content_copy expand_less sudo journalctl -u qb-manager.service
实时跟踪日志:
code Bash download content_copy expand_less sudo journalctl -u qb-manager.service -f 常用管理命令 动作 命令 停止服务 sudo systemctl stop qb-manager.service 重启服务 sudo systemctl restart qb-manager.service 禁止自启 sudo systemctl disable qb-manager.service 💡 可选优化:使用 Python 虚拟环境
为了避免系统级的 Python 依赖冲突,强烈建议使用虚拟环境(venv)。
创建虚拟环境
code Bash download content_copy expand_less sudo apt install python3-venv -y sudo -u qb-manager python3 -m venv /opt/qb-manager/venv
在虚拟环境中安装依赖
code Bash download content_copy expand_less sudo /opt/qb-manager/venv/bin/pip install requests
修改 systemd 服务文件 将 ExecStart 指令修改为使用虚拟环境中的 Python 解释器:
code Ini download content_copy expand_less ExecStart=/opt/qb-manager/venv/bin/python /opt/qb-manager/qb_ratio_manager.py
修改后,记得执行 sudo systemctl daemon-reload 和 sudo systemctl restart qb-manager.service。
📄 授权
本项目采用 MIT License。您可以自由使用、修改和分发。