vbnotebook
This commit is contained in:
356
80 TorrentsManipulation.py
Normal file
356
80 TorrentsManipulation.py
Normal file
@@ -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")
|
||||||
@@ -32,7 +32,7 @@ QBT_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MAX_ACTIVE_DOWNLOADS = 10
|
MAX_ACTIVE_DOWNLOADS = 10
|
||||||
DEAD_TORRENT_MINUTES = 5
|
DEAD_TORRENT_MINUTES = 60
|
||||||
DEFAULT_SAVE_PATH = None
|
DEFAULT_SAVE_PATH = None
|
||||||
|
|
||||||
MAIL_TO = "vladimir.buzalka@buzalka.cz"
|
MAIL_TO = "vladimir.buzalka@buzalka.cz"
|
||||||
|
|||||||
Reference in New Issue
Block a user