From a64f4b663f2b585f50a5eef0efd5576b06370033 Mon Sep 17 00:00:00 2001 From: Vladimir Buzalka Date: Sat, 10 Jan 2026 08:56:58 +0100 Subject: [PATCH] vbnotebook --- 80 TorrentsManipulation.py | 356 +++++++++++++++++++++++++++++++ Reporter_TorrentsManipulation.py | 2 +- 2 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 80 TorrentsManipulation.py diff --git a/80 TorrentsManipulation.py b/80 TorrentsManipulation.py new file mode 100644 index 0000000..daca2bd --- /dev/null +++ b/80 TorrentsManipulation.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from datetime import datetime, timedelta + +import pymysql +import qbittorrentapi +import bencodepy + +from EmailMessagingGraph import send_mail + +# ============================== +# ⚙ CONFIGURATION +# ============================== + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "torrents", + "charset": "utf8mb4", + "autocommit": True, +} + +QBT_CONFIG = { + "host": "192.168.1.76", + "port": 8080, + "username": "admin", + "password": "adminadmin", +} + +# ZVÝŠENO NA 100 dle požadavku +MAX_ACTIVE_DOWNLOADS = 100 + +# JAK DLOUHO ČEKAT? +# Doporučuji alespoň 3 dny (4320 minut). +# Pokud se do 3 dnů neobjeví nikdo, kdo má 100% souboru, je to pravděpodobně mrtvé. +DEAD_TORRENT_DAYS = 3 +DEAD_TORRENT_MINUTES = DEAD_TORRENT_DAYS * 24 * 60 + +DEFAULT_SAVE_PATH = None +MAIL_TO = "vladimir.buzalka@buzalka.cz" +MAX_LIST_ITEMS = 50 # cap lists in email + +# ============================== +# 🧮 RUNTIME STATS + LISTS +# ============================== + +RUN_START = datetime.now() + +stat_synced = 0 +stat_completed = 0 +stat_dead = 0 +stat_enqueued = 0 + +deleted_completed = [] # list[str] +deleted_dead = [] # list[str] +added_new = [] # list[str] +active_downloading = [] # list[str] + +# ============================== +# 🔧 CONNECT +# ============================== + +db = pymysql.connect(**DB_CONFIG) +cursor = db.cursor(pymysql.cursors.DictCursor) + +qb = qbittorrentapi.Client(**QBT_CONFIG) + +try: + qb.auth_log_in() + print("✅ Connected to qBittorrent.") +except Exception as e: + raise SystemExit(f"❌ Could not connect to qBittorrent: {e}") + + +# ============================== +# 🧪 TORRENT VALIDATION +# ============================== + +def is_valid_torrent(blob: bytes) -> bool: + 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(): + global stat_synced + + torrents = qb.torrents_info() + stat_synced = len(torrents) + + for t in torrents: + completion_dt = None + if getattr(t, "completion_on", 0): + try: + completion_dt = datetime.fromtimestamp(t.completion_on) + except Exception: + pass + + cursor.execute(""" + 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 + """, ( + t.hash, + t.state, + float(t.progress) * 100.0, + getattr(t, "save_path", None), + completion_dt, + completion_dt, + t.hash, + t.hash, + )) + + +# ============================== +# 🧹 HANDLE COMPLETED + DEAD +# ============================== + +def handle_completed_and_dead(): + global stat_completed, stat_dead + + # Načteme info o torrentech + torrents = qb.torrents_info() + + for t in torrents: + t_hash = t.hash + state = t.state + progress = float(t.progress) + + # Získání dostupnosti (availability) - defaultně -1 pokud není k dispozici + availability = float(getattr(t, "availability", -1)) + + # Získání času přidání + added_ts = getattr(t, "added_on", 0) + added_dt = datetime.fromtimestamp(added_ts) if added_ts > 0 else datetime.now() + age_in_minutes = (datetime.now() - added_dt).total_seconds() / 60 + + # --------------------------- + # 1. ✔ COMPLETED (Hotovo) + # --------------------------- + if progress >= 1.0 or state in {"completed", "uploading", "stalledUP", "queuedUP"}: + stat_completed += 1 + deleted_completed.append(t.name) + + try: + # Smažeme z QB, ale necháme data na disku + qb.torrents_delete(torrent_hashes=t_hash, delete_files=False) + except Exception as e: + print(f"⚠️ delete (keep data) failed for {t.name}: {e}") + + cursor.execute(""" + UPDATE torrents + 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. ❌ DEAD (Mrtvý) + # --------------------------- + # Logika: Je to starší než limit? A ZÁROVEŇ je dostupnost < 1 (nikdo nemá celý soubor)? + is_old_enough = age_in_minutes > DEAD_TORRENT_MINUTES + is_unavailable = availability < 1.0 + + if is_old_enough and is_unavailable: + stat_dead += 1 + deleted_dead.append(f"{t.name} (Avail: {availability:.2f})") + + try: + # Smažeme z QB včetně nedotažených souborů + qb.torrents_delete(torrent_hashes=t_hash, delete_files=True) + except Exception as e: + print(f"⚠️ delete (files) failed for {t.name}: {e}") + + cursor.execute(""" + UPDATE torrents + SET qb_state='dead', + qb_last_update=NOW() + WHERE qb_hash=%s OR torrent_hash=%s + """, (t_hash, t_hash)) + + +# ============================== +# 📊 ACTIVE DOWNLOADS +# ============================== + +def count_active_downloads(): + # Počítáme jen ty, co nejsou hotové (progress < 100%) + return sum(1 for t in qb.torrents_info() if float(t.progress) < 1.0) + + +def snapshot_active_downloading(): + """ + Capture current actively downloading torrents (progress < 100%). + """ + active = [] + for t in qb.torrents_info(): + prog = float(t.progress) + avail = float(getattr(t, "availability", 0)) + if prog < 1.0: + active.append(f"{t.name} — {prog * 100:.1f}% — Avail:{avail:.2f}") + return sorted(active) + + +# ============================== +# ➕ ENQUEUE NEW TORRENTS +# ============================== + +def enqueue_new_torrents(): + global stat_enqueued + + active = count_active_downloads() + + # Pokud máme plno (100+), nic nepřidáváme + if active >= MAX_ACTIVE_DOWNLOADS: + return + + # Kolik slotů zbývá do 100 + slots = MAX_ACTIVE_DOWNLOADS - active + + cursor.execute(""" + 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 + AND (qb_state IS NULL OR qb_state != 'dead') + ORDER BY added_datetime DESC + LIMIT %s + """, (slots,)) + + rows = cursor.fetchall() + + for row in rows: + blob = row["torrent_content"] + if not blob: + continue + + if not is_valid_torrent(blob): + cursor.execute(""" + UPDATE torrents + SET qb_state='invalid', + torrent_content=NULL, + qb_last_update=NOW() + WHERE id=%s + """, (row["id"],)) + continue + + # ➕ Add torrent + try: + qb.torrents_add(torrent_files=blob, savepath=DEFAULT_SAVE_PATH) + except Exception as e: + print(f"❌ Failed to add {row['torrent_hash']}: {e}") + continue + + stat_enqueued += 1 + added_new.append(row.get("torrent_filename") or row["torrent_hash"]) + + cursor.execute(""" + UPDATE torrents + SET qb_added=1, + qb_hash=COALESCE(qb_hash, %s), + qb_state='added', + qb_last_update=NOW() + WHERE id=%s + """, (row["torrent_hash"], row["id"])) + + +# ============================== +# ✉️ EMAIL HELPERS +# ============================== + +def format_list(title: str, items: list[str]) -> list[str]: + lines = [] + if not items: + return [f"{title}: (none)"] + + lines.append(f"{title}: {len(items)}") + shown = items[:MAX_LIST_ITEMS] + for it in shown: + lines.append(f" - {it}") + if len(items) > MAX_LIST_ITEMS: + lines.append(f" ... (+{len(items) - MAX_LIST_ITEMS} more)") + return lines + + +# ============================== +# 🏁 MAIN (ONE RUN) +# ============================== + +print("🚀 QB worker run started") + +try: + sync_qb_to_db() + handle_completed_and_dead() + enqueue_new_torrents() + + # Snapshot after enqueue/deletions, so email reflects end-state + active_downloading = snapshot_active_downloading() + +finally: + db.close() + +# ============================== +# 📧 EMAIL REPORT +# ============================== + +RUN_END = datetime.now() + +body_lines = [ + f"Run started : {RUN_START:%Y-%m-%d %H:%M:%S}", + f"Run finished: {RUN_END:%Y-%m-%d %H:%M:%S}", + "", + f"QB torrents synced : {stat_synced}", + f"Completed removed : {stat_completed}", + f"Dead removed : {stat_dead}", + f"New torrents added : {stat_enqueued}", + f"Active downloads : {len(active_downloading)} (Max: {MAX_ACTIVE_DOWNLOADS})", + "", +] + +body_lines += format_list("Deleted (completed, kept data)", deleted_completed) +body_lines.append("") +body_lines += format_list("Deleted (DEAD > 3 days & Avail < 1.0)", deleted_dead) +body_lines.append("") +body_lines += format_list("Newly added to qBittorrent", added_new) +body_lines.append("") +body_lines += format_list("Actively downloading now", active_downloading) + +send_mail( + to=MAIL_TO, + subject=f"qBittorrent worker – {RUN_START:%Y-%m-%d %H:%M}", + body="\n".join(body_lines), + html=False, +) + +print("📧 Email report sent") +print("🎉 DONE") \ No newline at end of file diff --git a/Reporter_TorrentsManipulation.py b/Reporter_TorrentsManipulation.py index 0b1cc05..3835e37 100644 --- a/Reporter_TorrentsManipulation.py +++ b/Reporter_TorrentsManipulation.py @@ -32,7 +32,7 @@ QBT_CONFIG = { } MAX_ACTIVE_DOWNLOADS = 10 -DEAD_TORRENT_MINUTES = 5 +DEAD_TORRENT_MINUTES = 60 DEFAULT_SAVE_PATH = None MAIL_TO = "vladimir.buzalka@buzalka.cz"