vbnotebook
This commit is contained in:
298
50 TorrentManipulation.py
Normal file
298
50 TorrentManipulation.py
Normal file
@@ -0,0 +1,298 @@
|
||||
#!/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 (0–1), 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.")
|
||||
Reference in New Issue
Block a user