Refactor 70 Manager.py — multi-client support + scheduled task mode
- Add CLIENTS list: UltraCC Seedbox (max 20) + Local qBittorrent (max 20) - Add 'added' to SELECT_NEXT exclusion list to prevent two clients claiming the same torrent from the shared DB queue - Add qb_client column tracking — each torrent records which client downloaded it; per-client stats shown at startup - Extract process_client() to encapsulate steps 1-3 per client - Remove continuous loop and sleep — script runs once and exits, designed to be triggered by a scheduled task Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Download Manager — UltraCC Seedbox
|
Download Manager — Multi-client (UltraCC + lokální qBittorrent)
|
||||||
Smyčka každých N minut:
|
Smyčka každých N minut pro každý klient:
|
||||||
1. Dokončené torrenty → odeber z qBittorrentu (data zachovej), zapiš do DB
|
1. Dokončené torrenty → odeber z qBittorrentu (data zachovej), zapiš do DB
|
||||||
2. Spočítej volné sloty
|
2. Spočítej volné sloty
|
||||||
3. Doplň nové torrenty dle priority: seeders + velikost
|
3. Doplň nové torrenty dle priority: seeders + velikost
|
||||||
|
|
||||||
|
Oba klienti sdílí stejnou DB frontu. Torrent "nárokovaný" jedním klientem
|
||||||
|
(qb_state='added') nebude nabídnut druhému klientovi.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pymysql
|
import pymysql
|
||||||
import qbittorrentapi
|
import qbittorrentapi
|
||||||
import time
|
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -18,26 +20,45 @@ from datetime import datetime
|
|||||||
# CONFIG
|
# CONFIG
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
MAX_CONCURRENT = 20 # max torrentů najednou v qBittorrentu
|
CLIENTS = [
|
||||||
LOOP_INTERVAL = 300 # sekund mezi iteracemi (5 minut)
|
{
|
||||||
|
"name": "UltraCC Seedbox",
|
||||||
QBT_URL = "https://vladob.zen.usbx.me/qbittorrent"
|
"max_concurrent": 20,
|
||||||
QBT_USER = "vladob"
|
"qbt": {
|
||||||
QBT_PASS = "jCni3U6d#y4bfcm"
|
"host": "https://vladob.zen.usbx.me/qbittorrent",
|
||||||
|
"username": "vladob",
|
||||||
|
"password": "jCni3U6d#y4bfcm",
|
||||||
|
"VERIFY_WEBUI_CERTIFICATE": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Local qBittorrent",
|
||||||
|
"max_concurrent": 20,
|
||||||
|
"qbt": {
|
||||||
|
"host": "192.168.1.76",
|
||||||
|
"port": 8080,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "adminadmin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "192.168.1.76",
|
"host": "192.168.1.76",
|
||||||
"port": 3306,
|
"port": 3306,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
"database": "torrents",
|
"database": "torrents",
|
||||||
"charset": "utf8mb4",
|
"charset": "utf8mb4",
|
||||||
"autocommit": True,
|
"autocommit": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# PRIORITY SQL
|
# PRIORITY SQL
|
||||||
# Pořadí: nejlepší seeders + nejmenší soubory první
|
# Pořadí: nejlepší seeders + nejmenší soubory první
|
||||||
|
#
|
||||||
|
# DŮLEŽITÉ: 'added' je v exclusion listu — torrent nárokovaný
|
||||||
|
# jedním klientem nebude nabídnut druhému.
|
||||||
# ============================================================
|
# ============================================================
|
||||||
SELECT_NEXT = """
|
SELECT_NEXT = """
|
||||||
SELECT id, torrent_hash, torrent_content, title_visible, size_pretty, seeders
|
SELECT id, torrent_hash, torrent_content, title_visible, size_pretty, seeders
|
||||||
@@ -46,7 +67,7 @@ SELECT_NEXT = """
|
|||||||
torrent_content IS NOT NULL
|
torrent_content IS NOT NULL
|
||||||
AND qb_completed_datetime IS NULL
|
AND qb_completed_datetime IS NULL
|
||||||
AND (qb_state IS NULL OR qb_state NOT IN (
|
AND (qb_state IS NULL OR qb_state NOT IN (
|
||||||
'incomplete', 'invalid', 'dead',
|
'added', 'incomplete', 'invalid', 'dead',
|
||||||
'completed', 'completed_removed'
|
'completed', 'completed_removed'
|
||||||
))
|
))
|
||||||
ORDER BY
|
ORDER BY
|
||||||
@@ -72,13 +93,8 @@ def connect_db():
|
|||||||
return pymysql.connect(**DB_CONFIG)
|
return pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
def connect_qbt():
|
def connect_qbt(cfg: dict):
|
||||||
qbt = qbittorrentapi.Client(
|
qbt = qbittorrentapi.Client(**cfg)
|
||||||
host=QBT_URL,
|
|
||||||
username=QBT_USER,
|
|
||||||
password=QBT_PASS,
|
|
||||||
VERIFY_WEBUI_CERTIFICATE=False,
|
|
||||||
)
|
|
||||||
qbt.auth_log_in()
|
qbt.auth_log_in()
|
||||||
return qbt
|
return qbt
|
||||||
|
|
||||||
@@ -137,12 +153,12 @@ def get_active_hashes(qbt):
|
|||||||
# STEP 3: Add new torrents
|
# STEP 3: Add new torrents
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def add_torrents(qbt, cursor, active_hashes, slots):
|
def add_torrents(qbt, cursor, active_hashes, slots, client_name: str):
|
||||||
"""
|
"""
|
||||||
Vybere z DB torrenty dle priority a přidá je do qBittorrentu.
|
Vybere z DB torrenty dle priority a přidá je do qBittorrentu.
|
||||||
Přeskočí ty, které jsou tam již nahrané (dle qb_hash / active_hashes).
|
Přeskočí ty, které jsou tam již nahrané (dle active_hashes).
|
||||||
|
Zapíše qb_client = client_name, aby bylo vidět, kdo torrent stahuje.
|
||||||
"""
|
"""
|
||||||
# Načti více než slots, abychom mohli přeskočit ty v qB
|
|
||||||
cursor.execute(SELECT_NEXT, (slots * 3,))
|
cursor.execute(SELECT_NEXT, (slots * 3,))
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
@@ -155,7 +171,7 @@ def add_torrents(qbt, cursor, active_hashes, slots):
|
|||||||
t_hash = t_hash.lower() if t_hash else ""
|
t_hash = t_hash.lower() if t_hash else ""
|
||||||
|
|
||||||
if t_hash in active_hashes:
|
if t_hash in active_hashes:
|
||||||
# Aktualizuj příznak v DB pokud tam už je
|
# Torrent je v qB, ale DB ho ještě nemá označený
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE torrents SET qb_added=1, qb_last_update=NOW()
|
UPDATE torrents SET qb_added=1, qb_last_update=NOW()
|
||||||
WHERE id=%s AND (qb_added IS NULL OR qb_added=0)
|
WHERE id=%s AND (qb_added IS NULL OR qb_added=0)
|
||||||
@@ -176,18 +192,61 @@ def add_torrents(qbt, cursor, active_hashes, slots):
|
|||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE torrents
|
UPDATE torrents
|
||||||
SET qb_added=1, qb_state='added', qb_last_update=NOW()
|
SET qb_added=1, qb_state='added', qb_client=%s, qb_last_update=NOW()
|
||||||
WHERE id=%s
|
WHERE id=%s
|
||||||
""", (t_id,))
|
""", (client_name, t_id))
|
||||||
|
|
||||||
print(f" ➕ Přidán: {(title or '')[:45]} "
|
print(f" ➕ Přidán: {(title or '')[:45]} "
|
||||||
f"| {size or '?':>10} | seeds={seeders}")
|
f"| {size or '?':>10} | seeds={seeders}")
|
||||||
added += 1
|
added += 1
|
||||||
time.sleep(0.3)
|
|
||||||
|
|
||||||
return added
|
return added
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PROCESS ONE CLIENT (steps 1-3)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def process_client(client_cfg: dict, cursor):
|
||||||
|
name = client_cfg["name"]
|
||||||
|
max_concurrent = client_cfg["max_concurrent"]
|
||||||
|
|
||||||
|
print(f"\n ┌── {name} (max {max_concurrent}) ──")
|
||||||
|
|
||||||
|
try:
|
||||||
|
qbt = connect_qbt(client_cfg["qbt"])
|
||||||
|
except Exception as e:
|
||||||
|
print(f" │ ❌ Nelze se připojit: {e}")
|
||||||
|
print(f" └──")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Krok 1: Dokončené
|
||||||
|
print(f" │ [1] Kontrola dokončených...")
|
||||||
|
removed = handle_completed(qbt, cursor)
|
||||||
|
if removed == 0:
|
||||||
|
print(f" │ Žádné dokončené.")
|
||||||
|
|
||||||
|
# Krok 2: Stav slotů
|
||||||
|
active_hashes = get_active_hashes(qbt)
|
||||||
|
active = len(active_hashes)
|
||||||
|
slots = max(0, max_concurrent - active)
|
||||||
|
|
||||||
|
print(f" │ [2] Sloty: {active}/{max_concurrent} aktivních | volných: {slots}")
|
||||||
|
|
||||||
|
# Krok 3: Doplnění
|
||||||
|
if slots > 0:
|
||||||
|
print(f" │ [3] Doplňuji {slots} torrentů...")
|
||||||
|
added = add_torrents(qbt, cursor, active_hashes, slots, name)
|
||||||
|
if added == 0:
|
||||||
|
print(f" │ Žádné vhodné torrenty k přidání.")
|
||||||
|
else:
|
||||||
|
print(f" │ Přidáno: {added}")
|
||||||
|
else:
|
||||||
|
print(f" │ [3] Sloty plné ({active}/{max_concurrent}), přeskakuji.")
|
||||||
|
|
||||||
|
print(f" └──")
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# MAIN LOOP
|
# MAIN LOOP
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -195,83 +254,51 @@ def add_torrents(qbt, cursor, active_hashes, slots):
|
|||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("DOWNLOAD MANAGER — UltraCC Seedbox")
|
print(f"DOWNLOAD MANAGER — Multi-client {now:%Y-%m-%d %H:%M:%S}")
|
||||||
print(f"MAX_CONCURRENT = {MAX_CONCURRENT} | interval = {LOOP_INTERVAL}s")
|
for c in CLIENTS:
|
||||||
print("Ctrl+C pro zastavení")
|
print(f" • {c['name']} (max {c['max_concurrent']})")
|
||||||
|
print(f"Celkem slotů: {sum(c['max_concurrent'] for c in CLIENTS)}")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
db = connect_db()
|
db = connect_db()
|
||||||
cursor = db.cursor()
|
cursor = db.cursor()
|
||||||
|
|
||||||
iteration = 0
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
# DB statistiky — celkové
|
||||||
iteration += 1
|
cursor.execute("""
|
||||||
now = datetime.now()
|
SELECT
|
||||||
print(f"\n{'─'*60}")
|
SUM(CASE WHEN qb_completed_datetime IS NOT NULL THEN 1 ELSE 0 END),
|
||||||
print(f" Iterace {iteration} — {now:%Y-%m-%d %H:%M:%S}")
|
SUM(CASE WHEN qb_state IS NULL AND torrent_content IS NOT NULL THEN 1 ELSE 0 END),
|
||||||
print(f"{'─'*60}")
|
SUM(CASE WHEN qb_state IN ('incomplete','dead') THEN 1 ELSE 0 END)
|
||||||
|
FROM torrents
|
||||||
|
""")
|
||||||
|
db_completed, db_waiting, db_dead = cursor.fetchone()
|
||||||
|
print(f" DB — staženo: {db_completed or 0} "
|
||||||
|
f"| čeká: {db_waiting or 0} "
|
||||||
|
f"| dead/incomplete: {db_dead or 0}")
|
||||||
|
|
||||||
# Připoj se k qBittorrentu (nové spojení každou iteraci)
|
# DB statistiky — per-client
|
||||||
try:
|
cursor.execute("""
|
||||||
qbt = connect_qbt()
|
SELECT qb_client, COUNT(*) AS cnt
|
||||||
except Exception as e:
|
FROM torrents
|
||||||
print(f" ❌ Nelze se připojit k qBittorrentu: {e}")
|
WHERE qb_client IS NOT NULL
|
||||||
print(f" ⏳ Zkusím za {LOOP_INTERVAL}s...")
|
GROUP BY qb_client
|
||||||
time.sleep(LOOP_INTERVAL)
|
""")
|
||||||
continue
|
per_client = cursor.fetchall()
|
||||||
|
if per_client:
|
||||||
|
parts = " | ".join(f"{name}: {cnt}" for name, cnt in per_client)
|
||||||
|
print(f" DB — per-client: {parts}")
|
||||||
|
|
||||||
# --- Krok 1: Dokončené ---
|
# Zpracuj každý klient
|
||||||
print(" [1] Kontrola dokončených...")
|
# (druhý klient vidí stav DB aktualizovaný prvním)
|
||||||
removed = handle_completed(qbt, cursor)
|
for client_cfg in CLIENTS:
|
||||||
if removed == 0:
|
process_client(client_cfg, cursor)
|
||||||
print(" Žádné dokončené.")
|
|
||||||
|
|
||||||
# --- Krok 2: Stav slotů ---
|
|
||||||
active_hashes = get_active_hashes(qbt)
|
|
||||||
active = len(active_hashes)
|
|
||||||
slots = max(0, MAX_CONCURRENT - active)
|
|
||||||
|
|
||||||
# Statistiky z DB
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT
|
|
||||||
SUM(CASE WHEN qb_completed_datetime IS NOT NULL THEN 1 ELSE 0 END),
|
|
||||||
SUM(CASE WHEN qb_state IS NULL AND torrent_content IS NOT NULL THEN 1 ELSE 0 END),
|
|
||||||
SUM(CASE WHEN qb_state IN ('incomplete','dead') THEN 1 ELSE 0 END)
|
|
||||||
FROM torrents
|
|
||||||
""")
|
|
||||||
db_completed, db_waiting, db_dead = cursor.fetchone()
|
|
||||||
|
|
||||||
print(f" [2] Sloty: {active}/{MAX_CONCURRENT} aktivních "
|
|
||||||
f"| volných: {slots}")
|
|
||||||
print(f" DB — staženo: {db_completed or 0} "
|
|
||||||
f"| čeká: {db_waiting or 0} "
|
|
||||||
f"| dead/incomplete: {db_dead or 0}")
|
|
||||||
|
|
||||||
# --- Krok 3: Doplnění ---
|
|
||||||
if slots > 0:
|
|
||||||
print(f" [3] Doplňuji {slots} torrentů...")
|
|
||||||
added = add_torrents(qbt, cursor, active_hashes, slots)
|
|
||||||
if added == 0:
|
|
||||||
print(" Žádné vhodné torrenty k přidání.")
|
|
||||||
else:
|
|
||||||
print(f" Přidáno: {added}")
|
|
||||||
else:
|
|
||||||
print(f" [3] Sloty plné ({active}/{MAX_CONCURRENT}), přeskakuji.")
|
|
||||||
|
|
||||||
print(f"\n ⏳ Další iterace za {LOOP_INTERVAL // 60} min "
|
|
||||||
f"({(now.replace(second=0, microsecond=0)).__class__.__name__} "
|
|
||||||
f"→ přibližně {LOOP_INTERVAL // 60} min)...")
|
|
||||||
time.sleep(LOOP_INTERVAL)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\n\n🛑 Zastaveno uživatelem.")
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
print("👋 Bye.")
|
print("\n👋 Hotovo.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user