diff --git a/50 TorrentManipulation.py b/50 TorrentManipulation.py index 2b2dbfe..0d7d898 100644 --- a/50 TorrentManipulation.py +++ b/50 TorrentManipulation.py @@ -2,16 +2,17 @@ # -*- coding: utf-8 -*- import time -from datetime import datetime +from datetime import datetime, timedelta + import pymysql import qbittorrentapi +import bencodepy # ============================== -# ⚙️ CONFIG +# ⚙ CONFIGURATION # ============================== -# MySQL (Tower) DB_CONFIG = { "host": "192.168.1.76", "port": 3307, @@ -22,277 +23,273 @@ DB_CONFIG = { "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 +# Torrent označíme jako "dead" pokud nebyl nikdy "seen_complete" +# více než X minut od přidání +DEAD_TORRENT_MINUTES = 5 + +DEFAULT_SAVE_PATH = None -# Volitelně – kam ukládat data (jinak použije default z qB) -DEFAULT_SAVE_PATH = None # např. r"/mnt/torrents/movies" # ============================== -# 🔗 CONNECTIONS +# 🔧 CONNECT # ============================== 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"], - ) +qb = qbittorrentapi.Client( + host=QBT_CONFIG["host"], + port=QBT_CONFIG["port"], + username=QBT_CONFIG["username"], + password=QBT_CONFIG["password"], +) try: qb.auth_log_in() print("✅ Connected to qBittorrent.") except Exception as e: - print("❌ Could not connect to qBittorrent:", e) + print("❌ Could not connect:", e) raise SystemExit(1) + # ============================== -# 🧠 HELPER FUNCTIONS +# 🧪 TORRENT VALIDATION +# ============================== + +def is_valid_torrent(blob: bytes) -> bool: + """ + Returns True only if BLOB is a valid .torrent file. + """ + try: + data = bencodepy.decode(blob) + return isinstance(data, dict) and b"info" in data + except Exception: + return False + + + +# ============================== +# 🔄 SYNC FROM QB → DB # ============================== 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 + except: + pass sql = """ UPDATE torrents - SET - qb_added = 1, + 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_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 - ), - ) + + cursor.execute(sql, ( + t.hash, + t.state, + float(t.progress) * 100.0, + getattr(t, "save_path", None), + completion_dt, + completion_dt, + t.hash, + t.hash + )) -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. - """ + +# ============================== +# 🧹 HANDLE COMPLETED + DEAD TORRENTS +# ============================== + +def handle_completed_and_dead(): 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}") + # ========================== + # ✔ COMPLETED + # ========================== + if progress >= 1.0 or state in {"completed", "uploading", "stalledUP", "queuedUP"}: + print(f"✅ Completed torrent → remove (keep data): {t.name}") try: - qb.torrents_delete( - torrent_hashes=t_hash, - delete_files=False, # data necháme - ) + qb.torrents_delete(torrent_hashes=t_hash, delete_files=False) except Exception as e: - print(f"⚠️ Could not remove completed torrent {t_hash}: {e}") + print("⚠️ delete failed:", e) - sql = """ + cursor.execute(""" 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)) + SET qb_state='completed', + qb_progress=100, + qb_completed_datetime=NOW(), + qb_last_update=NOW() + WHERE qb_hash=%s OR torrent_hash=%s + """, (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}") + # ========================== + # ❌ DEAD TORRENT (never seen_complete) + # ========================== - 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)) + props = qb.torrents_properties(t_hash) + seen = getattr(props, "last_seen", 0) + if seen == -1: # never seen complete + added_dt = getattr(t, "added_on", 0) + if added_dt: + added_time = datetime.fromtimestamp(added_dt) + if datetime.now() - added_time > timedelta(minutes=DEAD_TORRENT_MINUTES): + print(f"💀 Dead torrent (> {DEAD_TORRENT_MINUTES} min unseen): {t.name}") + try: + qb.torrents_delete(torrent_hashes=t_hash, delete_files=True) + except: + pass + + cursor.execute(""" + UPDATE torrents + SET qb_state='dead', + qb_last_update=NOW() + WHERE qb_hash=%s OR torrent_hash=%s + """, (t_hash, t_hash)) + + + +# ============================== +# 📊 COUNT ACTIVE DOWNLOADS +# ============================== 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 + torrents = qb.torrents_info(filter="all") + return sum(1 for t in torrents if float(t.progress) < 1.0) + +# ============================== +# ➕ ENQUEUE NEW TORRENTS +# ============================== + 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() + + print("DEBUG active =", active) + if active >= MAX_ACTIVE_DOWNLOADS: - print(f"📦 Active downloads: {active} (max {MAX_ACTIVE_DOWNLOADS}) → no new torrents enqueued.") + print(f"📦 {active}/{MAX_ACTIVE_DOWNLOADS} active → no enqueue") return slots = MAX_ACTIVE_DOWNLOADS - active - print(f"🪣 Active downloads: {active}, free slots: {slots}") sql = """ - SELECT id, torrent_hash, torrent_content, torrent_filename + SELECT id, torrent_hash, torrent_content, torrent_filename, added_datetime FROM torrents WHERE (qb_added IS NULL OR qb_added = 0) AND torrent_content IS NOT NULL - ORDER BY added_datetime ASC + ORDER BY added_datetime DESC -- <── take NEWEST FIRST LIMIT %s """ cursor.execute(sql, (slots,)) rows = cursor.fetchall() if not rows: - print("ℹ️ No new torrents in DB to enqueue.") + print("ℹ️ No new torrents") 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" + filename = row.get("torrent_filename", "unknown.torrent") if not blob: - print(f"⚠️ Torrent id={t_id} hash={t_hash} has no content, skipping.") + print("⚠️ empty blob, skip") continue - print(f"➕ Enqueuing torrent to qB: {filename} ({t_hash})") + # ========================== + # 🧪 VALIDATION OF .TORRENT + # ========================== + + if not is_valid_torrent(blob): + print(f"❌ INVALID TORRENT id={t_id}, size={len(blob)} → deleting content") + + cursor.execute(""" + UPDATE torrents + SET qb_state='invalid', + torrent_content=NULL, + qb_last_update=NOW() + WHERE id=%s + """, (t_id,)) + continue + + # ========================== + # ➕ ADD TORRENT + # ========================== + + print(f"➕ Adding torrent: {filename} ({t_hash})") try: - qb.torrents_add( - torrent_files=blob, - savepath=DEFAULT_SAVE_PATH, - ) + 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š + print(f"❌ Failed to add {t_hash}: {e}") continue - # Označíme v DB jako přidaný (qb_added=1), qb_hash=t_hash - sql_update = """ + cursor.execute(""" 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)) + SET qb_added=1, + qb_hash=COALESCE(qb_hash, %s), + qb_state='added', + qb_last_update=NOW() + WHERE id=%s + """, (t_hash, t_id)) + # ============================== # 🏁 MAIN LOOP # ============================== -print("🚀 Torrent worker started. Press Ctrl+C to stop.\n") +print("🚀 Worker started") try: while True: - loop_start = datetime.now() - print(f"⏱️ Loop start: {loop_start.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"\n⏱ Loop {datetime.now():%Y-%m-%d %H:%M:%S}") - try: - # 1) Sync from qB → DB - sync_qb_to_db() + sync_qb_to_db() + handle_completed_and_dead() + enqueue_new_torrents() - # 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") + print(f"🛌 Sleep {LOOP_SLEEP_SECONDS}s\n") time.sleep(LOOP_SLEEP_SECONDS) except KeyboardInterrupt: - print("🛑 Stopping worker (Ctrl+C).") + print("🛑 Stopping worker...") finally: - try: - db.close() - except Exception: - pass + db.close() print("👋 Bye.") diff --git a/60 Testcountoftorrents.py b/60 Testcountoftorrents.py new file mode 100644 index 0000000..8f6b8b3 --- /dev/null +++ b/60 Testcountoftorrents.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from datetime import datetime +import qbittorrentapi + +# ============================== +# CONFIG – přizpůsob si podle sebe +# ============================== +QBT_CONFIG = { + "host": "192.168.1.76", + "port": 8080, + "username": "admin", + "password": "adminadmin", +} + +def fmt_ts(ts: int) -> str: + """ + Převod unix timestampu na čitelný string. + qBittorrent vrací -1 pokud hodnota není známá. + """ + if ts is None or ts <= 0: + return "—" + try: + return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return f"invalid({ts})" + + +def main(): + # Připojení + qb = qbittorrentapi.Client( + host=QBT_CONFIG["host"], + port=QBT_CONFIG["port"], + username=QBT_CONFIG["username"], + password=QBT_CONFIG["password"], + ) + + try: + qb.auth_log_in() + print("✅ Connected to qBittorrent\n") + except Exception as e: + print("❌ Could not connect to qBittorrent:", e) + return + + # Všechno, žádný filter na downloading + torrents = qb.torrents_info(filter='all') + print(f"Found {len(torrents)} torrents (filter='all')\n") + + for t in torrents: + # properties – obsahují last_seen + try: + props = qb.torrents_properties(t.hash) + except Exception as e: + print(f"⚠️ Cannot get properties for {t.hash[:8]} {t.name}: {e}") + continue + + seen_complete = getattr(t, "seen_complete", None) # z /torrents/info + last_seen = getattr(props, "last_seen", None) # z /torrents/properties + + print("=" * 80) + print(f"Name : {t.name}") + print(f"Hash : {t.hash}") + print(f"State : {t.state}") + print(f"Progress : {float(t.progress) * 100:.2f}%") + print(f"Seen complete: {fmt_ts(seen_complete)} (t.seen_complete)") + print(f"Last seen : {fmt_ts(last_seen)} (props.last_seen)") + + print("\n✅ Done.") + +if __name__ == "__main__": + main()