z230
This commit is contained in:
@@ -2,16 +2,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pymysql
|
import pymysql
|
||||||
import qbittorrentapi
|
import qbittorrentapi
|
||||||
|
import bencodepy
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# ⚙️ CONFIG
|
# ⚙ CONFIGURATION
|
||||||
# ==============================
|
# ==============================
|
||||||
|
|
||||||
# MySQL (Tower)
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "192.168.1.76",
|
"host": "192.168.1.76",
|
||||||
"port": 3307,
|
"port": 3307,
|
||||||
@@ -22,277 +23,273 @@ DB_CONFIG = {
|
|||||||
"autocommit": True,
|
"autocommit": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# qBittorrent WebUI
|
|
||||||
QBT_CONFIG = {
|
QBT_CONFIG = {
|
||||||
"host": "192.168.1.76",
|
"host": "192.168.1.76",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
# pokud máš whitelist a bypass auth, username/password netřeba
|
|
||||||
"username": "admin",
|
"username": "admin",
|
||||||
"password": "adminadmin",
|
"password": "adminadmin",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Max. počet *aktivních* downloadů v qBittorrentu
|
|
||||||
MAX_ACTIVE_DOWNLOADS = 10
|
MAX_ACTIVE_DOWNLOADS = 10
|
||||||
|
|
||||||
# Jak často běží hlavní smyčka (sekundy)
|
|
||||||
LOOP_SLEEP_SECONDS = 60
|
LOOP_SLEEP_SECONDS = 60
|
||||||
|
|
||||||
# Jak dlouho může být torrent "stalled", než ho smažeme (sekundy)
|
# Torrent označíme jako "dead" pokud nebyl nikdy "seen_complete"
|
||||||
STALLED_MAX_SECONDS = 60 * 60 # 60 minut
|
# více než X minut od přidání
|
||||||
|
DEAD_TORRENT_MINUTES = 5
|
||||||
|
|
||||||
|
DEFAULT_SAVE_PATH = None
|
||||||
|
|
||||||
# Volitelně – kam ukládat data (jinak použije default z qB)
|
|
||||||
DEFAULT_SAVE_PATH = None # např. r"/mnt/torrents/movies"
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 🔗 CONNECTIONS
|
# 🔧 CONNECT
|
||||||
# ==============================
|
# ==============================
|
||||||
|
|
||||||
db = pymysql.connect(**DB_CONFIG)
|
db = pymysql.connect(**DB_CONFIG)
|
||||||
cursor = db.cursor(pymysql.cursors.DictCursor)
|
cursor = db.cursor(pymysql.cursors.DictCursor)
|
||||||
|
|
||||||
if QBT_CONFIG["username"] and QBT_CONFIG["password"]:
|
qb = qbittorrentapi.Client(
|
||||||
qb = qbittorrentapi.Client(
|
host=QBT_CONFIG["host"],
|
||||||
host=QBT_CONFIG["host"],
|
port=QBT_CONFIG["port"],
|
||||||
port=QBT_CONFIG["port"],
|
username=QBT_CONFIG["username"],
|
||||||
username=QBT_CONFIG["username"],
|
password=QBT_CONFIG["password"],
|
||||||
password=QBT_CONFIG["password"],
|
)
|
||||||
)
|
|
||||||
else:
|
|
||||||
# bez auth (whitelist / bypass)
|
|
||||||
qb = qbittorrentapi.Client(
|
|
||||||
host=QBT_CONFIG["host"],
|
|
||||||
port=QBT_CONFIG["port"],
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
qb.auth_log_in()
|
qb.auth_log_in()
|
||||||
print("✅ Connected to qBittorrent.")
|
print("✅ Connected to qBittorrent.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("❌ Could not connect to qBittorrent:", e)
|
print("❌ Could not connect:", e)
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 🧠 HELPER FUNCTIONS
|
# 🧪 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():
|
def sync_qb_to_db():
|
||||||
"""Synchronize state of torrents from qBittorrent into MySQL."""
|
|
||||||
torrents = qb.torrents_info()
|
torrents = qb.torrents_info()
|
||||||
|
|
||||||
for t in torrents:
|
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
|
completion_dt = None
|
||||||
if getattr(t, "completion_on", 0):
|
if getattr(t, "completion_on", 0):
|
||||||
try:
|
try:
|
||||||
completion_dt = datetime.fromtimestamp(t.completion_on)
|
completion_dt = datetime.fromtimestamp(t.completion_on)
|
||||||
except Exception:
|
except:
|
||||||
completion_dt = None
|
pass
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
UPDATE torrents
|
UPDATE torrents
|
||||||
SET
|
SET qb_added = 1,
|
||||||
qb_added = 1,
|
|
||||||
qb_hash = COALESCE(qb_hash, %s),
|
qb_hash = COALESCE(qb_hash, %s),
|
||||||
qb_state = %s,
|
qb_state = %s,
|
||||||
qb_progress = %s,
|
qb_progress = %s,
|
||||||
qb_savepath = %s,
|
qb_savepath = %s,
|
||||||
qb_completed_datetime = IF(%s IS NOT NULL AND qb_completed_datetime IS NULL, %s, qb_completed_datetime),
|
qb_completed_datetime =
|
||||||
|
IF(%s IS NOT NULL AND qb_completed_datetime IS NULL, %s, qb_completed_datetime),
|
||||||
qb_last_update = NOW()
|
qb_last_update = NOW()
|
||||||
WHERE qb_hash = %s OR torrent_hash = %s
|
WHERE qb_hash = %s OR torrent_hash = %s
|
||||||
"""
|
"""
|
||||||
progress_pct = float(t.progress) * 100.0
|
|
||||||
cursor.execute(
|
cursor.execute(sql, (
|
||||||
sql,
|
t.hash,
|
||||||
(
|
t.state,
|
||||||
t.hash, # COALESCE(qb_hash, %s)
|
float(t.progress) * 100.0,
|
||||||
t.state,
|
getattr(t, "save_path", None),
|
||||||
progress_pct,
|
completion_dt,
|
||||||
getattr(t, "save_path", None),
|
completion_dt,
|
||||||
completion_dt,
|
t.hash,
|
||||||
completion_dt,
|
t.hash
|
||||||
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.
|
# 🧹 HANDLE COMPLETED + DEAD TORRENTS
|
||||||
- Dlouho stalled torrenty: odstraní z qB (včetně nedokončených dat), v DB označí jako stalled_removed.
|
# ==============================
|
||||||
"""
|
|
||||||
|
def handle_completed_and_dead():
|
||||||
torrents = qb.torrents_info()
|
torrents = qb.torrents_info()
|
||||||
|
|
||||||
completed_states = {"uploading", "stalledUP", "queuedUP", "pausedUP", "completed"}
|
|
||||||
stalled_states = {"stalledDL", "error", "missingFiles"}
|
|
||||||
|
|
||||||
for t in torrents:
|
for t in torrents:
|
||||||
t_hash = t.hash
|
t_hash = t.hash
|
||||||
state = t.state
|
state = t.state
|
||||||
progress = float(t.progress)
|
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:
|
# ✔ COMPLETED
|
||||||
print(f"✅ Completed torrent, removing from qB (keeping data): {t.name}")
|
# ==========================
|
||||||
|
if progress >= 1.0 or state in {"completed", "uploading", "stalledUP", "queuedUP"}:
|
||||||
|
print(f"✅ Completed torrent → remove (keep data): {t.name}")
|
||||||
try:
|
try:
|
||||||
qb.torrents_delete(
|
qb.torrents_delete(torrent_hashes=t_hash, delete_files=False)
|
||||||
torrent_hashes=t_hash,
|
|
||||||
delete_files=False, # data necháme
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Could not remove completed torrent {t_hash}: {e}")
|
print("⚠️ delete failed:", e)
|
||||||
|
|
||||||
sql = """
|
cursor.execute("""
|
||||||
UPDATE torrents
|
UPDATE torrents
|
||||||
SET
|
SET qb_state='completed',
|
||||||
qb_state = 'completed',
|
qb_progress=100,
|
||||||
qb_progress = 100,
|
qb_completed_datetime=NOW(),
|
||||||
qb_completed_datetime = IF(qb_completed_datetime IS NULL, NOW(), qb_completed_datetime),
|
qb_last_update=NOW()
|
||||||
qb_last_update = NOW()
|
WHERE qb_hash=%s OR torrent_hash=%s
|
||||||
WHERE qb_hash = %s OR torrent_hash = %s
|
""", (t_hash, t_hash))
|
||||||
"""
|
|
||||||
cursor.execute(sql, (t_hash, t_hash))
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 2) STALLED dlouho
|
# ==========================
|
||||||
if state in stalled_states and time_active and time_active > STALLED_MAX_SECONDS:
|
# ❌ DEAD TORRENT (never seen_complete)
|
||||||
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 = """
|
props = qb.torrents_properties(t_hash)
|
||||||
UPDATE torrents
|
seen = getattr(props, "last_seen", 0)
|
||||||
SET
|
|
||||||
qb_state = 'stalled_removed',
|
|
||||||
qb_last_update = NOW()
|
|
||||||
WHERE qb_hash = %s OR torrent_hash = %s
|
|
||||||
"""
|
|
||||||
cursor.execute(sql, (t_hash, t_hash))
|
|
||||||
|
|
||||||
|
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():
|
def count_active_downloads():
|
||||||
"""Return number of torrents that are currently downloading/active."""
|
torrents = qb.torrents_info(filter="all")
|
||||||
torrents = qb.torrents_info()
|
return sum(1 for t in torrents if float(t.progress) < 1.0)
|
||||||
active_states = {
|
|
||||||
"downloading",
|
|
||||||
"stalledDL",
|
|
||||||
"queuedDL",
|
|
||||||
"checkingDL",
|
|
||||||
"allocating",
|
|
||||||
"metaDL",
|
|
||||||
"forcedDL",
|
|
||||||
}
|
|
||||||
count = sum(1 for t in torrents if t.state in active_states)
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# ➕ ENQUEUE NEW TORRENTS
|
||||||
|
# ==============================
|
||||||
|
|
||||||
def enqueue_new_torrents():
|
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()
|
active = count_active_downloads()
|
||||||
|
|
||||||
|
print("DEBUG active =", active)
|
||||||
|
|
||||||
if active >= MAX_ACTIVE_DOWNLOADS:
|
if active >= MAX_ACTIVE_DOWNLOADS:
|
||||||
print(f"📦 Active downloads: {active} (max {MAX_ACTIVE_DOWNLOADS}) → no new torrents enqueued.")
|
print(f"📦 {active}/{MAX_ACTIVE_DOWNLOADS} active → no enqueue")
|
||||||
return
|
return
|
||||||
|
|
||||||
slots = MAX_ACTIVE_DOWNLOADS - active
|
slots = MAX_ACTIVE_DOWNLOADS - active
|
||||||
print(f"🪣 Active downloads: {active}, free slots: {slots}")
|
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
SELECT id, torrent_hash, torrent_content, torrent_filename
|
SELECT id, torrent_hash, torrent_content, torrent_filename, added_datetime
|
||||||
FROM torrents
|
FROM torrents
|
||||||
WHERE (qb_added IS NULL OR qb_added = 0)
|
WHERE (qb_added IS NULL OR qb_added = 0)
|
||||||
AND torrent_content IS NOT NULL
|
AND torrent_content IS NOT NULL
|
||||||
ORDER BY added_datetime ASC
|
ORDER BY added_datetime DESC -- <── take NEWEST FIRST
|
||||||
LIMIT %s
|
LIMIT %s
|
||||||
"""
|
"""
|
||||||
cursor.execute(sql, (slots,))
|
cursor.execute(sql, (slots,))
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
print("ℹ️ No new torrents in DB to enqueue.")
|
print("ℹ️ No new torrents")
|
||||||
return
|
return
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
t_id = row["id"]
|
t_id = row["id"]
|
||||||
t_hash = row["torrent_hash"]
|
t_hash = row["torrent_hash"]
|
||||||
blob = row["torrent_content"]
|
blob = row["torrent_content"]
|
||||||
filename = row.get("torrent_filename") or "unknown.torrent"
|
filename = row.get("torrent_filename", "unknown.torrent")
|
||||||
|
|
||||||
if not blob:
|
if not blob:
|
||||||
print(f"⚠️ Torrent id={t_id} hash={t_hash} has no content, skipping.")
|
print("⚠️ empty blob, skip")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"➕ Enqueuing torrent to qB: {filename} ({t_hash})")
|
# ==========================
|
||||||
|
# 🧪 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:
|
try:
|
||||||
qb.torrents_add(
|
qb.torrents_add(torrent_files=blob, savepath=DEFAULT_SAVE_PATH)
|
||||||
torrent_files=blob,
|
|
||||||
savepath=DEFAULT_SAVE_PATH,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Failed to add torrent {t_hash} to qBittorrent:", e)
|
print(f"❌ Failed to add {t_hash}: {e}")
|
||||||
# můžeš si zde označit v DB jako error, pokud chceš
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Označíme v DB jako přidaný (qb_added=1), qb_hash=t_hash
|
cursor.execute("""
|
||||||
sql_update = """
|
|
||||||
UPDATE torrents
|
UPDATE torrents
|
||||||
SET
|
SET qb_added=1,
|
||||||
qb_added = 1,
|
qb_hash=COALESCE(qb_hash, %s),
|
||||||
qb_hash = COALESCE(qb_hash, %s),
|
qb_state='added',
|
||||||
qb_state = 'added',
|
qb_last_update=NOW()
|
||||||
qb_last_update = NOW()
|
WHERE id=%s
|
||||||
WHERE id = %s
|
""", (t_hash, t_id))
|
||||||
"""
|
|
||||||
cursor.execute(sql_update, (t_hash, t_id))
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 🏁 MAIN LOOP
|
# 🏁 MAIN LOOP
|
||||||
# ==============================
|
# ==============================
|
||||||
|
|
||||||
print("🚀 Torrent worker started. Press Ctrl+C to stop.\n")
|
print("🚀 Worker started")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
loop_start = datetime.now()
|
print(f"\n⏱ Loop {datetime.now():%Y-%m-%d %H:%M:%S}")
|
||||||
print(f"⏱️ Loop start: {loop_start.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
||||||
|
|
||||||
try:
|
sync_qb_to_db()
|
||||||
# 1) Sync from qB → DB
|
handle_completed_and_dead()
|
||||||
sync_qb_to_db()
|
enqueue_new_torrents()
|
||||||
|
|
||||||
# 2) Handle completed & stalled torrents (remove from qB, mark in DB)
|
print(f"🛌 Sleep {LOOP_SLEEP_SECONDS}s\n")
|
||||||
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)
|
time.sleep(LOOP_SLEEP_SECONDS)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("🛑 Stopping worker (Ctrl+C).")
|
print("🛑 Stopping worker...")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
try:
|
db.close()
|
||||||
db.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
print("👋 Bye.")
|
print("👋 Bye.")
|
||||||
|
|||||||
72
60 Testcountoftorrents.py
Normal file
72
60 Testcountoftorrents.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import qbittorrentapi
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# CONFIG – přizpůsob si podle sebe
|
||||||
|
# ==============================
|
||||||
|
QBT_CONFIG = {
|
||||||
|
"host": "192.168.1.76",
|
||||||
|
"port": 8080,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "adminadmin",
|
||||||
|
}
|
||||||
|
|
||||||
|
def fmt_ts(ts: int) -> str:
|
||||||
|
"""
|
||||||
|
Převod unix timestampu na čitelný string.
|
||||||
|
qBittorrent vrací -1 pokud hodnota není známá.
|
||||||
|
"""
|
||||||
|
if ts is None or ts <= 0:
|
||||||
|
return "—"
|
||||||
|
try:
|
||||||
|
return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except Exception:
|
||||||
|
return f"invalid({ts})"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Připojení
|
||||||
|
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\n")
|
||||||
|
except Exception as e:
|
||||||
|
print("❌ Could not connect to qBittorrent:", e)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Všechno, žádný filter na downloading
|
||||||
|
torrents = qb.torrents_info(filter='all')
|
||||||
|
print(f"Found {len(torrents)} torrents (filter='all')\n")
|
||||||
|
|
||||||
|
for t in torrents:
|
||||||
|
# properties – obsahují last_seen
|
||||||
|
try:
|
||||||
|
props = qb.torrents_properties(t.hash)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Cannot get properties for {t.hash[:8]} {t.name}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen_complete = getattr(t, "seen_complete", None) # z /torrents/info
|
||||||
|
last_seen = getattr(props, "last_seen", None) # z /torrents/properties
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Name : {t.name}")
|
||||||
|
print(f"Hash : {t.hash}")
|
||||||
|
print(f"State : {t.state}")
|
||||||
|
print(f"Progress : {float(t.progress) * 100:.2f}%")
|
||||||
|
print(f"Seen complete: {fmt_ts(seen_complete)} (t.seen_complete)")
|
||||||
|
print(f"Last seen : {fmt_ts(last_seen)} (props.last_seen)")
|
||||||
|
|
||||||
|
print("\n✅ Done.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user