#!/usr/bin/env python3 # -*- coding: utf-8 -*- import time from datetime import datetime import pymysql import qbittorrentapi # ============================== # ⚙️ CONFIG # ============================== # MySQL (Tower) DB_CONFIG = { "host": "192.168.1.76", "port": 3307, "user": "root", "password": "Vlado9674+", "database": "torrents", "charset": "utf8mb4", "autocommit": True, } # qBittorrent WebUI QBT_CONFIG = { "host": "192.168.1.76", "port": 8080, # pokud máš whitelist a bypass auth, username/password netřeba "username": "admin", "password": "adminadmin", } # Max. počet *aktivních* downloadů v qBittorrentu MAX_ACTIVE_DOWNLOADS = 10 # Jak často běží hlavní smyčka (sekundy) LOOP_SLEEP_SECONDS = 60 # Jak dlouho může být torrent "stalled", než ho smažeme (sekundy) STALLED_MAX_SECONDS = 60 * 60 # 60 minut # Volitelně – kam ukládat data (jinak použije default z qB) DEFAULT_SAVE_PATH = None # např. r"/mnt/torrents/movies" # ============================== # 🔗 CONNECTIONS # ============================== db = pymysql.connect(**DB_CONFIG) cursor = db.cursor(pymysql.cursors.DictCursor) if QBT_CONFIG["username"] and QBT_CONFIG["password"]: qb = qbittorrentapi.Client( host=QBT_CONFIG["host"], port=QBT_CONFIG["port"], username=QBT_CONFIG["username"], password=QBT_CONFIG["password"], ) else: # bez auth (whitelist / bypass) qb = qbittorrentapi.Client( host=QBT_CONFIG["host"], port=QBT_CONFIG["port"], ) try: qb.auth_log_in() print("✅ Connected to qBittorrent.") except Exception as e: print("❌ Could not connect to qBittorrent:", e) raise SystemExit(1) # ============================== # 🧠 HELPER FUNCTIONS # ============================== def sync_qb_to_db(): """Synchronize state of torrents from qBittorrent into MySQL.""" torrents = qb.torrents_info() for t in torrents: # t.hash, t.name, t.state, t.progress (0–1), t.save_path, # t.time_active (s), t.completion_on (unix timestamp, 0 if none) completion_dt = None if getattr(t, "completion_on", 0): try: completion_dt = datetime.fromtimestamp(t.completion_on) except Exception: completion_dt = None sql = """ UPDATE torrents SET qb_added = 1, qb_hash = COALESCE(qb_hash, %s), qb_state = %s, qb_progress = %s, qb_savepath = %s, qb_completed_datetime = IF(%s IS NOT NULL AND qb_completed_datetime IS NULL, %s, qb_completed_datetime), qb_last_update = NOW() WHERE qb_hash = %s OR torrent_hash = %s """ progress_pct = float(t.progress) * 100.0 cursor.execute( sql, ( t.hash, # COALESCE(qb_hash, %s) t.state, progress_pct, getattr(t, "save_path", None), completion_dt, completion_dt, t.hash, # WHERE qb_hash = %s t.hash, # OR torrent_hash = %s ), ) def handle_completed_and_stalled(): """ - Completed torrenty: odstraní z qB (bez smazání dat), v DB označí jako completed. - Dlouho stalled torrenty: odstraní z qB (včetně nedokončených dat), v DB označí jako stalled_removed. """ torrents = qb.torrents_info() completed_states = {"uploading", "stalledUP", "queuedUP", "pausedUP", "completed"} stalled_states = {"stalledDL", "error", "missingFiles"} for t in torrents: t_hash = t.hash state = t.state progress = float(t.progress) time_active = getattr(t, "time_active", 0) # 1) COMPLETED – progress 100% nebo stav v completed_states if progress >= 1.0 or state in completed_states: print(f"✅ Completed torrent, removing from qB (keeping data): {t.name}") try: qb.torrents_delete( torrent_hashes=t_hash, delete_files=False, # data necháme ) except Exception as e: print(f"⚠️ Could not remove completed torrent {t_hash}: {e}") sql = """ UPDATE torrents SET qb_state = 'completed', qb_progress = 100, qb_completed_datetime = IF(qb_completed_datetime IS NULL, NOW(), qb_completed_datetime), qb_last_update = NOW() WHERE qb_hash = %s OR torrent_hash = %s """ cursor.execute(sql, (t_hash, t_hash)) continue # 2) STALLED dlouho if state in stalled_states and time_active and time_active > STALLED_MAX_SECONDS: print(f"⛔ Stalled torrent for too long, removing (with data): {t.name}") try: qb.torrents_delete( torrent_hashes=t_hash, delete_files=True, # nedokončená data smažeme ) except Exception as e: print(f"⚠️ Could not remove stalled torrent {t_hash}: {e}") sql = """ UPDATE torrents SET qb_state = 'stalled_removed', qb_last_update = NOW() WHERE qb_hash = %s OR torrent_hash = %s """ cursor.execute(sql, (t_hash, t_hash)) def count_active_downloads(): """Return number of torrents that are currently downloading/active.""" torrents = qb.torrents_info() active_states = { "downloading", "stalledDL", "queuedDL", "checkingDL", "allocating", "metaDL", "forcedDL", } count = sum(1 for t in torrents if t.state in active_states) return count def enqueue_new_torrents(): """ Select from DB torrents that: - have torrent_content (BLOB) - qb_added = 0 or NULL and add them to qBittorrent, up to free slots. """ active = count_active_downloads() if active >= MAX_ACTIVE_DOWNLOADS: print(f"📦 Active downloads: {active} (max {MAX_ACTIVE_DOWNLOADS}) → no new torrents enqueued.") return slots = MAX_ACTIVE_DOWNLOADS - active print(f"🪣 Active downloads: {active}, free slots: {slots}") sql = """ SELECT id, torrent_hash, torrent_content, torrent_filename FROM torrents WHERE (qb_added IS NULL OR qb_added = 0) AND torrent_content IS NOT NULL ORDER BY added_datetime ASC LIMIT %s """ cursor.execute(sql, (slots,)) rows = cursor.fetchall() if not rows: print("ℹ️ No new torrents in DB to enqueue.") return for row in rows: t_id = row["id"] t_hash = row["torrent_hash"] blob = row["torrent_content"] filename = row.get("torrent_filename") or "unknown.torrent" if not blob: print(f"⚠️ Torrent id={t_id} hash={t_hash} has no content, skipping.") continue print(f"➕ Enqueuing torrent to qB: {filename} ({t_hash})") try: qb.torrents_add( torrent_files=blob, savepath=DEFAULT_SAVE_PATH, ) except Exception as e: print(f"❌ Failed to add torrent {t_hash} to qBittorrent:", e) # můžeš si zde označit v DB jako error, pokud chceš continue # Označíme v DB jako přidaný (qb_added=1), qb_hash=t_hash sql_update = """ UPDATE torrents SET qb_added = 1, qb_hash = COALESCE(qb_hash, %s), qb_state = 'added', qb_last_update = NOW() WHERE id = %s """ cursor.execute(sql_update, (t_hash, t_id)) # ============================== # 🏁 MAIN LOOP # ============================== print("🚀 Torrent worker started. Press Ctrl+C to stop.\n") try: while True: loop_start = datetime.now() print(f"⏱️ Loop start: {loop_start.strftime('%Y-%m-%d %H:%M:%S')}") try: # 1) Sync from qB → DB sync_qb_to_db() # 2) Handle completed & stalled torrents (remove from qB, mark in DB) handle_completed_and_stalled() # 3) Enqueue new torrents from DB (up to MAX_ACTIVE_DOWNLOADS) enqueue_new_torrents() except Exception as e: print(f"💥 Error in main loop: {e}") print(f"🛌 Sleeping {LOOP_SLEEP_SECONDS} seconds...\n") time.sleep(LOOP_SLEEP_SECONDS) except KeyboardInterrupt: print("🛑 Stopping worker (Ctrl+C).") finally: try: db.close() except Exception: pass print("👋 Bye.")