diff --git a/50 TorrentManipulation.py b/50 TorrentManipulation.py new file mode 100644 index 0000000..2b2dbfe --- /dev/null +++ b/50 TorrentManipulation.py @@ -0,0 +1,298 @@ +#!/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.")