diff --git a/Seedbox/70 Manager.py b/Seedbox/70 Manager.py new file mode 100644 index 0000000..6e91c3a --- /dev/null +++ b/Seedbox/70 Manager.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Download Manager — UltraCC Seedbox +Smyčka každých N minut: + 1. Dokončené torrenty → odeber z qBittorrentu (data zachovej), zapiš do DB + 2. Spočítej volné sloty + 3. Doplň nové torrenty dle priority: seeders + velikost +""" + +import pymysql +import qbittorrentapi +import time +import sys +from datetime import datetime + +# ============================================================ +# CONFIG +# ============================================================ + +MAX_CONCURRENT = 20 # max torrentů najednou v qBittorrentu +LOOP_INTERVAL = 300 # sekund mezi iteracemi (5 minut) + +QBT_URL = "https://vladob.zen.usbx.me/qbittorrent" +QBT_USER = "vladob" +QBT_PASS = "jCni3U6d#y4bfcm" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "torrents", + "charset": "utf8mb4", + "autocommit": True, +} + +# ============================================================ +# PRIORITY SQL +# Pořadí: nejlepší seeders + nejmenší soubory první +# ============================================================ +SELECT_NEXT = """ + SELECT id, torrent_hash, torrent_content, title_visible, size_pretty, seeders + FROM torrents + WHERE + torrent_content IS NOT NULL + AND qb_completed_datetime IS NULL + AND (qb_state IS NULL OR qb_state NOT IN ( + 'incomplete', 'invalid', 'dead', + 'completed', 'completed_removed' + )) + ORDER BY + CASE + WHEN seeders >= 3 THEN 1 + WHEN seeders >= 1 THEN 2 + ELSE 3 + END ASC, + CASE + WHEN size_pretty LIKE '%%MB%%' THEN 0 + ELSE 1 + END ASC, + seeders DESC, + added_datetime DESC + LIMIT %s +""" + +# ============================================================ +# CONNECT +# ============================================================ + +def connect_db(): + return pymysql.connect(**DB_CONFIG) + + +def connect_qbt(): + qbt = qbittorrentapi.Client( + host=QBT_URL, + username=QBT_USER, + password=QBT_PASS, + VERIFY_WEBUI_CERTIFICATE=False, + ) + qbt.auth_log_in() + return qbt + + +# ============================================================ +# STEP 1: Handle completed torrents +# ============================================================ + +def handle_completed(qbt, cursor): + """ + Dokončené torrenty odebere z qBittorrentu (data zůstanou) + a zapíše qb_completed_datetime do DB. + """ + removed = 0 + for t in qbt.torrents_info(): + if not t.completion_on: + continue + + thash = t.hash.lower() + completed_dt = datetime.fromtimestamp(t.completion_on) + + try: + qbt.torrents_delete(torrent_hashes=thash, delete_files=False) + except Exception as e: + print(f" ⚠️ Nelze odebrat {t.name[:40]}: {e}") + continue + + cursor.execute(""" + UPDATE torrents + SET + qb_state = 'completed_removed', + qb_progress = 100, + qb_completed_datetime = COALESCE(qb_completed_datetime, %s), + qb_last_update = NOW() + WHERE torrent_hash = %s OR qb_hash = %s + """, (completed_dt, thash, thash)) + + print(f" ✅ Dokončen a odebrán: {t.name[:50]}") + removed += 1 + + return removed + + +# ============================================================ +# STEP 2: Count active (non-completed) torrents in qBittorrent +# ============================================================ + +def get_active_hashes(qbt): + """ + Vrátí sadu hashů torrentů, které jsou aktuálně v qBittorrentu. + """ + return {t.hash.lower() for t in qbt.torrents_info()} + + +# ============================================================ +# STEP 3: Add new torrents +# ============================================================ + +def add_torrents(qbt, cursor, active_hashes, slots): + """ + 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). + """ + # Načti více než slots, abychom mohli přeskočit ty v qB + cursor.execute(SELECT_NEXT, (slots * 3,)) + rows = cursor.fetchall() + + added = 0 + for row in rows: + if added >= slots: + break + + t_id, t_hash, content, title, size, seeders = row + t_hash = t_hash.lower() if t_hash else "" + + if t_hash in active_hashes: + # Aktualizuj příznak v DB pokud tam už je + cursor.execute(""" + UPDATE torrents SET qb_added=1, qb_last_update=NOW() + WHERE id=%s AND (qb_added IS NULL OR qb_added=0) + """, (t_id,)) + continue + + if not content: + continue + + try: + qbt.torrents_add( + torrent_files={f"{t_hash}.torrent": content}, + is_paused=False, + ) + except Exception as e: + print(f" ❌ Nelze přidat {(title or '')[:40]}: {e}") + continue + + cursor.execute(""" + UPDATE torrents + SET qb_added=1, qb_state='added', qb_last_update=NOW() + WHERE id=%s + """, (t_id,)) + + print(f" ➕ Přidán: {(title or '')[:45]} " + f"| {size or '?':>10} | seeds={seeders}") + added += 1 + time.sleep(0.3) + + return added + + +# ============================================================ +# MAIN LOOP +# ============================================================ + +def main(): + sys.stdout.reconfigure(encoding="utf-8") + + print("=" * 60) + print("DOWNLOAD MANAGER — UltraCC Seedbox") + print(f"MAX_CONCURRENT = {MAX_CONCURRENT} | interval = {LOOP_INTERVAL}s") + print("Ctrl+C pro zastavení") + print("=" * 60) + + db = connect_db() + cursor = db.cursor() + + iteration = 0 + + try: + while True: + iteration += 1 + now = datetime.now() + print(f"\n{'─'*60}") + print(f" Iterace {iteration} — {now:%Y-%m-%d %H:%M:%S}") + print(f"{'─'*60}") + + # Připoj se k qBittorrentu (nové spojení každou iteraci) + try: + qbt = connect_qbt() + except Exception as e: + print(f" ❌ Nelze se připojit k qBittorrentu: {e}") + print(f" ⏳ Zkusím za {LOOP_INTERVAL}s...") + time.sleep(LOOP_INTERVAL) + continue + + # --- Krok 1: Dokončené --- + print(" [1] Kontrola dokončených...") + removed = handle_completed(qbt, cursor) + if removed == 0: + 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: + db.close() + print("👋 Bye.") + + +if __name__ == "__main__": + main()