Files
torrents/50 TorrentManipulation.py
2025-12-08 06:53:29 +01:00

299 lines
8.8 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
import pymysql
import qbittorrentapi
# ==============================
# ⚙️ CONFIG
# ==============================
# MySQL (Tower)
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"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
# Volitelně kam ukládat data (jinak použije default z qB)
DEFAULT_SAVE_PATH = None # např. r"/mnt/torrents/movies"
# ==============================
# 🔗 CONNECTIONS
# ==============================
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"],
)
try:
qb.auth_log_in()
print("✅ Connected to qBittorrent.")
except Exception as e:
print("❌ Could not connect to qBittorrent:", e)
raise SystemExit(1)
# ==============================
# 🧠 HELPER FUNCTIONS
# ==============================
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 (01), 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
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
"""
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
),
)
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.
"""
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}")
try:
qb.torrents_delete(
torrent_hashes=t_hash,
delete_files=False, # data necháme
)
except Exception as e:
print(f"⚠️ Could not remove completed torrent {t_hash}: {e}")
sql = """
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))
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}")
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))
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
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()
if active >= MAX_ACTIVE_DOWNLOADS:
print(f"📦 Active downloads: {active} (max {MAX_ACTIVE_DOWNLOADS}) → no new torrents enqueued.")
return
slots = MAX_ACTIVE_DOWNLOADS - active
print(f"🪣 Active downloads: {active}, free slots: {slots}")
sql = """
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 ASC
LIMIT %s
"""
cursor.execute(sql, (slots,))
rows = cursor.fetchall()
if not rows:
print(" No new torrents in DB to enqueue.")
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"
if not blob:
print(f"⚠️ Torrent id={t_id} hash={t_hash} has no content, skipping.")
continue
print(f" Enqueuing torrent to qB: {filename} ({t_hash})")
try:
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š
continue
# Označíme v DB jako přidaný (qb_added=1), qb_hash=t_hash
sql_update = """
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))
# ==============================
# 🏁 MAIN LOOP
# ==============================
print("🚀 Torrent worker started. Press Ctrl+C to stop.\n")
try:
while True:
loop_start = datetime.now()
print(f"⏱️ Loop start: {loop_start.strftime('%Y-%m-%d %H:%M:%S')}")
try:
# 1) Sync from qB → DB
sync_qb_to_db()
# 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")
time.sleep(LOOP_SLEEP_SECONDS)
except KeyboardInterrupt:
print("🛑 Stopping worker (Ctrl+C).")
finally:
try:
db.close()
except Exception:
pass
print("👋 Bye.")