WIP: torrent manipulation logic

This commit is contained in:
2025-12-18 11:25:10 +01:00
parent 5f1c55243e
commit 5f01729bce

View File

@@ -0,0 +1,337 @@
#!/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",
}
MAX_ACTIVE_DOWNLOADS = 10
DEAD_TORRENT_MINUTES = 5
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
torrents = qb.torrents_info()
for t in torrents:
t_hash = t.hash
state = t.state
progress = float(t.progress)
# ✔ COMPLETED
if progress >= 1.0 or state in {"completed", "uploading", "stalledUP", "queuedUP"}:
stat_completed += 1
deleted_completed.append(t.name)
try:
qb.torrents_delete(torrent_hashes=t_hash, delete_files=False)
except Exception as e:
# keep name in report; just note error in DB state if you want later
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
# ❌ DEAD (never seen_complete)
try:
props = qb.torrents_properties(t_hash)
except Exception:
continue
if getattr(props, "last_seen", 0) == -1:
added_dt = getattr(t, "added_on", 0)
if added_dt:
if datetime.now() - datetime.fromtimestamp(added_dt) > timedelta(minutes=DEAD_TORRENT_MINUTES):
stat_dead += 1
deleted_dead.append(t.name)
try:
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():
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)
if prog < 1.0:
active.append(f"{t.name}{prog*100:.1f}% — {t.state}")
return sorted(active)
# ==============================
# ENQUEUE NEW TORRENTS
# ==============================
def enqueue_new_torrents():
global stat_enqueued
active = count_active_downloads()
if active >= MAX_ACTIVE_DOWNLOADS:
return
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
ORDER BY added_datetime DESC
LIMIT %s
""", (slots,))
for row in cursor.fetchall():
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 : {sum(1 for _ in active_downloading)}",
"",
]
body_lines += format_list("Deleted (completed, kept data)", deleted_completed)
body_lines.append("")
body_lines += format_list("Deleted (dead, deleted files)", 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")