Files
torrents/50 TorrentManipulation.py
2025-12-15 06:28:20 +01:00

296 lines
7.6 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
from datetime import datetime, timedelta
import pymysql
import qbittorrentapi
import bencodepy
# ==============================
# ⚙ 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
LOOP_SLEEP_SECONDS = 60
# 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
# ==============================
# 🔧 CONNECT
# ==============================
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor(pymysql.cursors.DictCursor)
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:", e)
raise SystemExit(1)
# ==============================
# 🧪 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():
torrents = qb.torrents_info()
for t in torrents:
completion_dt = None
if getattr(t, "completion_on", 0):
try:
completion_dt = datetime.fromtimestamp(t.completion_on)
except:
pass
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
"""
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
))
# ==============================
# 🧹 HANDLE COMPLETED + DEAD TORRENTS
# ==============================
def handle_completed_and_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"}:
print(f"✅ Completed torrent → remove (keep data): {t.name}")
try:
qb.torrents_delete(torrent_hashes=t_hash, delete_files=False)
except Exception as e:
print("⚠️ delete failed:", 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 TORRENT (never seen_complete)
# ==========================
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():
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():
active = count_active_downloads()
print("DEBUG active =", active)
if active >= MAX_ACTIVE_DOWNLOADS:
print(f"📦 {active}/{MAX_ACTIVE_DOWNLOADS} active → no enqueue")
return
slots = MAX_ACTIVE_DOWNLOADS - active
sql = """
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 DESC -- <── take NEWEST FIRST
LIMIT %s
"""
cursor.execute(sql, (slots,))
rows = cursor.fetchall()
if not rows:
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", "unknown.torrent")
if not blob:
print("⚠️ empty blob, skip")
continue
# ==========================
# 🧪 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)
except Exception as e:
print(f"❌ Failed to add {t_hash}: {e}")
continue
cursor.execute("""
UPDATE torrents
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("🚀 Worker started")
try:
while True:
print(f"\n⏱ Loop {datetime.now():%Y-%m-%d %H:%M:%S}")
sync_qb_to_db()
handle_completed_and_dead()
enqueue_new_torrents()
print(f"🛌 Sleep {LOOP_SLEEP_SECONDS}s\n")
time.sleep(LOOP_SLEEP_SECONDS)
except KeyboardInterrupt:
print("🛑 Stopping worker...")
finally:
db.close()
print("👋 Bye.")