@@ -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 " )