Compare commits

...

13 Commits

Author SHA1 Message Date
3ce8fa0080 Integrate dead torrent cleanup into Manager loop
Move dead torrent detection logic from 50 MrtveTorrenty.py into 70 Manager.py as step 1b, so the manager handles completed, dead, and new torrents in a single run cycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 13:54:35 +01:00
c4f2d8b13d git 2026-03-06 17:25:47 +01:00
a74ad8ff00 git 2026-03-06 17:22:02 +01:00
afbca5b348 z230 2026-03-06 14:24:32 +01:00
20c4a7d8b4 Merge remote-tracking branch 'origin/master' 2026-03-06 07:11:30 +01:00
489b236b9b git 2026-03-06 07:11:03 +01:00
197cb3f8db z230 2026-03-03 14:27:37 +01:00
b37db5397e Fix handle_completed — guard against invalid completion_on timestamp
qBittorrent returns completion_on = -1 for torrents that were never
completed. datetime.fromtimestamp(-1) throws OSError on Windows.
Added explicit check for negative values and try/except for safety.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 07:02:21 +01:00
15b498ca55 Refactor 70 Manager.py — multi-client support + scheduled task mode
- Add CLIENTS list: UltraCC Seedbox (max 20) + Local qBittorrent (max 20)
- Add 'added' to SELECT_NEXT exclusion list to prevent two clients
  claiming the same torrent from the shared DB queue
- Add qb_client column tracking — each torrent records which client
  downloaded it; per-client stats shown at startup
- Extract process_client() to encapsulate steps 1-3 per client
- Remove continuous loop and sleep — script runs once and exits,
  designed to be triggered by a scheduled task

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 20:32:57 +01:00
7646f6f68f Add Seedbox/70 Manager.py — continuous download manager for UltraCC 2026-03-01 12:15:49 +01:00
e0cb02c490 Move 95 IncrementalImport.py to Seedbox/ 2026-03-01 12:01:10 +01:00
6b8728360c Add 95 IncrementalImport.py — incremental torrent scraper without Selenium 2026-03-01 11:58:22 +01:00
d57f7d75ce Add Seedbox/60 AktualizaceSeeders.py — scrape seeders/leechers from sktorrent.eu 2026-03-01 11:45:43 +01:00
12 changed files with 2209 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import bencodepy
# ============================================================
# DB CONFIG UPRAV
# ============================================================
DB_CONFIG = {
"host": "192.168.1.50",
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.SSCursor # streaming
}
# ============================================================
# HELPERS
# ============================================================
def decode_if_bytes(value):
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
return value
def parse_torrent(blob):
data = bencodepy.decode(blob)
info = data[b"info"]
torrent_name = decode_if_bytes(info[b"name"])
files = []
# multi-file torrent
if b"files" in info:
for f in info[b"files"]:
path = "/".join(decode_if_bytes(p) for p in f[b"path"])
length = f[b"length"]
files.append((path, length))
# single file torrent
else:
length = info[b"length"]
files.append((torrent_name, length))
return torrent_name, files
# ============================================================
# MAIN
# ============================================================
def main():
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cursor:
cursor.execute("""
SELECT id, title_visible, torrent_content
FROM torrents
WHERE torrent_content IS NOT NULL
""")
for row in cursor:
torrent_id = row[0]
title_visible = row[1]
blob = row[2]
try:
name, files = parse_torrent(blob)
print("=" * 70)
print(f"DB ID : {torrent_id}")
print(f"Title visible : {title_visible}")
print(f"Torrent name : {name}")
print("Files:")
for f, size in files:
print(f" - {f} ({size} bytes)")
except Exception as e:
print(f"ERROR parsing torrent ID {torrent_id}: {e}")
conn.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import bencodepy
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
# ============================================================
# DB CONFIG
# ============================================================
DB_CONFIG = {
"host": "192.168.1.50",
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.SSCursor
}
OUTPUT_FILE = "torrent_report.xlsx"
# ============================================================
# HELPERS
# ============================================================
def decode_if_bytes(value):
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
return value
def get_root_name(info):
# prefer UTF-8 variant
if b"name.utf-8" in info:
return decode_if_bytes(info[b"name.utf-8"])
return decode_if_bytes(info[b"name"])
def get_file_parts(file_entry):
# prefer UTF-8 variant
if b"path.utf-8" in file_entry:
return [decode_if_bytes(p) for p in file_entry[b"path.utf-8"]]
return [decode_if_bytes(p) for p in file_entry[b"path"]]
def parse_torrent(blob):
data = bencodepy.decode(blob)
info = data[b"info"]
root_name = get_root_name(info)
files = []
# =====================
# MULTI FILE TORRENT
# =====================
if b"files" in info:
for f in info[b"files"]:
parts = get_file_parts(f)
# ochrana proti root/root duplicite
if parts and parts[0] == root_name:
full_path = "/".join(parts)
else:
full_path = root_name + "/" + "/".join(parts)
files.append(full_path)
# =====================
# SINGLE FILE TORRENT
# =====================
else:
files.append(root_name)
return files
# ============================================================
# MAIN
# ============================================================
def main():
conn = pymysql.connect(**DB_CONFIG)
wb = Workbook()
ws = wb.active
ws.title = "Torrent report"
ws.append(["QB Status", "Title", "Torrent Path"])
with conn.cursor() as cursor:
cursor.execute("""
SELECT qb_state, title_visible, torrent_content
FROM torrents
WHERE torrent_content IS NOT NULL
""")
for qb_state, title_visible, blob in cursor:
qb_state = qb_state or "UNKNOWN"
title_visible = title_visible or ""
try:
files = parse_torrent(blob)
for f in files:
ws.append([qb_state, title_visible, f])
except Exception as e:
ws.append([qb_state, title_visible, f"ERROR: {e}"])
# autosize
for col in ws.columns:
max_len = 0
col_letter = get_column_letter(col[0].column)
for cell in col:
if cell.value:
max_len = max(max_len, len(str(cell.value)))
ws.column_dimensions[col_letter].width = min(max_len + 2, 90)
wb.save(OUTPUT_FILE)
conn.close()
print("DONE ->", OUTPUT_FILE)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,48 @@
import pymysql
# =========================
# CONFIG
# =========================
DB_CONFIG = {
"host": "192.168.1.50",
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4"
}
SEARCH_NAME = "Balík audioknih"
# =========================
# MAIN
# =========================
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cursor:
cursor.execute("""
SELECT id, title_visible, torrent_content
FROM torrents
WHERE title_visible LIKE %s
LIMIT 1
""", ("%" + SEARCH_NAME + "%",))
row = cursor.fetchone()
if not row:
print("Torrent not found")
exit()
torrent_id, title, blob = row
filename = f"{title}.torrent".replace("/", "_")
with open(filename, "wb") as f:
f.write(blob)
print("Saved:", filename)
conn.close()

View File

@@ -0,0 +1,220 @@
import pymysql
import requests
import json
import time
import re
import sys
from bs4 import BeautifulSoup
from datetime import datetime
# ============================================================
# CONFIG
# ============================================================
COOKIE_FILE = "sktorrent_cookies.json"
BASE_URL = "https://sktorrent.eu/torrent/torrents.php?active=0&category=24&order=data&by=DESC"
SLEEP_BETWEEN_PAGES = 2.0 # sekundy mezi stránkami (web nás neblokuje)
MAX_PAGES = 300 # pojistka — skript se zastaví nejpozději zde
# Kolik stránek za sebou bez jediné shody v DB = konec (dorazili jsme k novým torrentům)
STOP_AFTER_EMPTY_PAGES = 5
# Kolik 403 chyb za sebou = přerušit (web nás blokuje)
STOP_AFTER_403 = 3
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
# ============================================================
# CONNECT
# ============================================================
def connect_db():
return pymysql.connect(**DB_CONFIG)
def build_session():
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies = json.load(f)
session = requests.Session()
session.headers["User-Agent"] = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)
for c in cookies:
session.cookies.set(c["name"], c["value"], domain=c.get("domain", ""))
return session
# ============================================================
# PARSE ONE PAGE
# ============================================================
def parse_page(html):
"""
Vrátí seznam dict: {hash, seeders, leechers}
"""
soup = BeautifulSoup(html, "html.parser")
results = []
for row in soup.select("table tr"):
cells = row.find_all("td")
if len(cells) != 7:
continue
# td[1] musí mít odkaz download.php?id=<hash>
dl_link = cells[1].find("a", href=re.compile(r"download\.php\?id="))
if not dl_link:
continue
match = re.search(r"id=([a-f0-9]+)", dl_link["href"])
if not match:
continue
torrent_hash = match.group(1).lower()
# seeders = td[4], leechers = td[5]
seeders_text = cells[4].get_text(strip=True)
leechers_text = cells[5].get_text(strip=True)
try:
seeders = int(seeders_text)
except ValueError:
seeders = 0
try:
leechers = int(leechers_text)
except ValueError:
leechers = 0
results.append({
"hash": torrent_hash,
"seeders": seeders,
"leechers": leechers,
})
return results
# ============================================================
# MAIN
# ============================================================
def main():
sys.stdout.reconfigure(encoding="utf-8")
print("=" * 60)
print("AKTUALIZACE SEEDERS / LEECHERS — sktorrent.eu")
print(f"Spuštěno: {datetime.now():%Y-%m-%d %H:%M:%S}")
print("=" * 60)
session = build_session()
db = connect_db()
cursor = db.cursor()
# Zjisti max stránku
r0 = session.get(f"{BASE_URL}&page=0", timeout=15)
all_page_nums = [int(m.group(1)) for m in re.finditer(r"page=(\d+)", r0.text)]
max_page = max(all_page_nums) if all_page_nums else MAX_PAGES
print(f"Max stránka na webu: {max_page}")
print(f"Prochází od stránky {max_page} směrem dolů...\n")
total_pages = 0
total_parsed = 0
total_updated = 0
total_skipped = 0
consecutive_empty = 0 # stránky za sebou bez jediné shody v DB
consecutive_403 = 0 # 403 chyby za sebou
# Procházíme od nejstarší stránky (konec) k nejnovější (začátek)
for page in range(max_page, -1, -1):
url = f"{BASE_URL}&page={page}"
try:
r = session.get(url, timeout=15)
r.raise_for_status()
consecutive_403 = 0 # reset po úspěchu
except requests.exceptions.HTTPError as e:
if e.response is not None and e.response.status_code == 403:
consecutive_403 += 1
print(f"⚠️ Stránka {page} — 403 Forbidden ({consecutive_403}/{STOP_AFTER_403})")
if consecutive_403 >= STOP_AFTER_403:
print(f"\n🛑 {STOP_AFTER_403}× 403 za sebou — web nás blokuje, přerušuji.")
break
time.sleep(5) # pauza po 403
else:
print(f"⚠️ Stránka {page} — chyba: {e}")
continue
except Exception as e:
print(f"⚠️ Stránka {page} — chyba: {e}")
continue
if "login.php" in r.url or "Prihlas sa" in r.text:
print("❌ Cookies expiraly — je potřeba se znovu přihlásit (spusť Selenium skript)")
break
rows = parse_page(r.text)
if not rows:
print(f" Stránka {page:3d} → prázdná, konec paginace.")
break
total_pages += 1
total_parsed += len(rows)
page_updated = 0
for item in rows:
cursor.execute("""
UPDATE torrents
SET
seeders = %s,
leechers = %s,
qb_last_update = NOW()
WHERE torrent_hash = %s
""", (item["seeders"], item["leechers"], item["hash"]))
if cursor.rowcount > 0:
total_updated += 1
page_updated += 1
else:
total_skipped += 1
print(f" Stránka {page:3d}{len(rows):2d} torrentů, "
f"updatováno: {page_updated:2d} (celkem: {total_updated})")
# Zastavit pokud jsme dorazili do oblasti novějších torrentů (mimo DB)
if page_updated == 0:
consecutive_empty += 1
if consecutive_empty >= STOP_AFTER_EMPTY_PAGES:
print(f"\n{STOP_AFTER_EMPTY_PAGES} stránek po sobě bez shody → "
f"dorazili jsme k novějším torrentům, které nejsou v DB. Konec.")
break
else:
consecutive_empty = 0
time.sleep(SLEEP_BETWEEN_PAGES)
# ============================================================
# SUMMARY
# ============================================================
print()
print("=" * 60)
print(f"Hotovo: {datetime.now():%Y-%m-%d %H:%M:%S}")
print(f"Stránek zpracováno : {total_pages}")
print(f"Záznamů parsováno : {total_parsed}")
print(f"DB řádků updatováno: {total_updated}")
print(f"Nebylo v DB : {total_skipped}")
print("=" * 60)
db.close()
if __name__ == "__main__":
main()

383
Seedbox/70 Manager.py Normal file
View File

@@ -0,0 +1,383 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Download Manager — Multi-client (UltraCC + lokální qBittorrent)
Smyčka každých N minut pro každý klient:
1. Dokončené torrenty → odeber z qBittorrentu (data zachovej), zapiš do DB
2. Spočítej volné sloty
3. Doplň nové torrenty dle priority: seeders + velikost
Oba klienti sdílí stejnou DB frontu. Torrent "nárokovaný" jedním klientem
(qb_state='added') nebude nabídnut druhému klientovi.
"""
import pymysql
import qbittorrentapi
import sys
from datetime import datetime, timedelta
# ============================================================
# CONFIG
# ============================================================
DEAD_AFTER_HOURS = 72 # progress < 95% po 72h → dead
DEAD_PROGRESS_THRESHOLD = 95.0
STUCK_AFTER_HOURS = 168 # progress >= 95% ale < 100% po 7 dnech → dead
CLIENTS = [
{
"name": "UltraCC Seedbox",
"max_concurrent": 30,
"qbt": {
"host": "https://vladob.zen.usbx.me/qbittorrent",
"username": "vladob",
"password": "jCni3U6d#y4bfcm",
"VERIFY_WEBUI_CERTIFICATE": False,
},
},
{
"name": "Local qBittorrent",
"max_concurrent": 30,
"qbt": {
"host": "192.168.1.76",
"port": 8080,
"username": "admin",
"password": "adminadmin",
},
},
]
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
# ============================================================
# PRIORITY SQL
# Pořadí: nejlepší seeders + nejmenší soubory první
#
# DŮLEŽITÉ: 'added' je v exclusion listu — torrent nárokovaný
# jedním klientem nebude nabídnut druhému.
# ============================================================
SELECT_NEXT = """
SELECT id, torrent_hash, torrent_content, title_visible, size_pretty, seeders
FROM torrents
WHERE
torrent_content IS NOT NULL
AND qb_completed_datetime IS NULL
AND (qb_state IS NULL OR qb_state NOT IN (
'added', 'incomplete', 'invalid', 'dead',
'completed', 'completed_removed'
))
ORDER BY
CASE
WHEN seeders >= 3 THEN 1
WHEN seeders >= 1 THEN 2
ELSE 3
END ASC,
CASE
WHEN size_pretty LIKE '%%MB%%' THEN 0
ELSE 1
END ASC,
seeders DESC,
added_datetime DESC
LIMIT %s
"""
# ============================================================
# CONNECT
# ============================================================
def connect_db():
return pymysql.connect(**DB_CONFIG)
def connect_qbt(cfg: dict):
qbt = qbittorrentapi.Client(**cfg)
qbt.auth_log_in()
return qbt
# ============================================================
# STEP 1: Handle completed torrents
# ============================================================
def handle_completed(qbt, cursor):
"""
Dokončené torrenty odebere z qBittorrentu (data zůstanou)
a zapíše qb_completed_datetime do DB.
"""
removed = 0
for t in qbt.torrents_info():
if not t.completion_on or t.completion_on < 0:
continue
try:
completed_dt = datetime.fromtimestamp(t.completion_on)
except (OSError, ValueError, OverflowError):
continue
thash = t.hash.lower()
try:
qbt.torrents_delete(torrent_hashes=thash, delete_files=False)
except Exception as e:
print(f" ⚠️ Nelze odebrat {t.name[:40]}: {e}")
continue
cursor.execute("""
UPDATE torrents
SET
qb_state = 'completed_removed',
qb_progress = 100,
qb_completed_datetime = COALESCE(qb_completed_datetime, %s),
qb_last_update = NOW()
WHERE torrent_hash = %s OR qb_hash = %s
""", (completed_dt, thash, thash))
print(f" ✅ Dokončen a odebrán: {t.name[:50]}")
removed += 1
return removed
# ============================================================
# STEP 1b: Handle dead torrents (z 50 MrtveTorrenty.py)
# ============================================================
def handle_dead_torrents(qbt, cursor):
"""
Mrtvé torrenty: nízký progress po 72h NEBO zaseknutý >= 95% po 7 dnech.
Smaže z qBittorrentu včetně souborů a označí v DB jako incomplete.
"""
now = datetime.now()
deadline_a = now - timedelta(hours=DEAD_AFTER_HOURS)
deadline_b = now - timedelta(hours=STUCK_AFTER_HOURS)
dead_count = 0
for t in qbt.torrents_info():
# Přeskočit dokončené
if t.completion_on and t.completion_on > 0:
continue
added_on = t.added_on
if not added_on:
continue
added_dt = datetime.fromtimestamp(added_on)
progress_pct = float(t.progress) * 100.0
# Kritérium A: nízký progress po 72h
is_dead_a = (added_dt <= deadline_a) and (progress_pct < DEAD_PROGRESS_THRESHOLD)
# Kritérium B: zaseknutý blízko 100% po 7 dnech
is_dead_b = (added_dt <= deadline_b) and (progress_pct >= DEAD_PROGRESS_THRESHOLD) and (progress_pct < 100.0)
if not is_dead_a and not is_dead_b:
continue
thash = t.hash.lower()
reason = "nízký progress po 72h" if is_dead_a else "zaseknutý blízko 100% po 7 dnech"
print(f" 💀 MRTVÝ ({reason}): {t.name[:50]}")
print(f" Progress: {progress_pct:.1f}% | Stav: {t.state} | Seeds: {t.num_seeds}")
try:
qbt.torrents_delete(torrent_hashes=thash, delete_files=True)
except Exception as e:
print(f" ❌ Smazání selhalo: {e}")
continue
cursor.execute("""
UPDATE torrents
SET
qb_state = 'incomplete',
qb_progress = %s,
qb_last_update = NOW()
WHERE torrent_hash = %s OR qb_hash = %s
""", (progress_pct, thash, thash))
dead_count += 1
return dead_count
# ============================================================
# STEP 2: Count active (non-completed) torrents in qBittorrent
# ============================================================
def get_active_hashes(qbt):
"""
Vrátí sadu hashů torrentů, které jsou aktuálně v qBittorrentu.
"""
return {t.hash.lower() for t in qbt.torrents_info()}
# ============================================================
# STEP 3: Add new torrents
# ============================================================
def add_torrents(qbt, cursor, active_hashes, slots, client_name: str):
"""
Vybere z DB torrenty dle priority a přidá je do qBittorrentu.
Přeskočí ty, které jsou tam již nahrané (dle active_hashes).
Zapíše qb_client = client_name, aby bylo vidět, kdo torrent stahuje.
"""
cursor.execute(SELECT_NEXT, (slots * 3,))
rows = cursor.fetchall()
added = 0
for row in rows:
if added >= slots:
break
t_id, t_hash, content, title, size, seeders = row
t_hash = t_hash.lower() if t_hash else ""
if t_hash in active_hashes:
# Torrent je v qB, ale DB ho ještě nemá označený
cursor.execute("""
UPDATE torrents SET qb_added=1, qb_last_update=NOW()
WHERE id=%s AND (qb_added IS NULL OR qb_added=0)
""", (t_id,))
continue
if not content:
continue
try:
qbt.torrents_add(
torrent_files={f"{t_hash}.torrent": content},
is_paused=False,
)
except Exception as e:
print(f" ❌ Nelze přidat {(title or '')[:40]}: {e}")
continue
cursor.execute("""
UPDATE torrents
SET qb_added=1, qb_state='added', qb_client=%s, qb_last_update=NOW()
WHERE id=%s
""", (client_name, t_id))
print(f" Přidán: {(title or '')[:45]} "
f"| {size or '?':>10} | seeds={seeders}")
added += 1
return added
# ============================================================
# PROCESS ONE CLIENT (steps 1-3)
# ============================================================
def process_client(client_cfg: dict, cursor):
name = client_cfg["name"]
max_concurrent = client_cfg["max_concurrent"]
print(f"\n ┌── {name} (max {max_concurrent}) ──")
try:
qbt = connect_qbt(client_cfg["qbt"])
except Exception as e:
print(f" │ ❌ Nelze se připojit: {e}")
print(f" └──")
return
# Krok 1: Dokončené
print(f" │ [1] Kontrola dokončených...")
removed = handle_completed(qbt, cursor)
if removed == 0:
print(f" │ Žádné dokončené.")
# Krok 1b: Mrtvé torrenty
print(f" │ [1b] Kontrola mrtvých torrentů...")
dead = handle_dead_torrents(qbt, cursor)
if dead == 0:
print(f" │ Žádné mrtvé.")
else:
print(f" │ Odstraněno mrtvých: {dead}")
# Krok 2: Stav slotů
active_hashes = get_active_hashes(qbt)
active = len(active_hashes)
slots = max(0, max_concurrent - active)
print(f" │ [2] Sloty: {active}/{max_concurrent} aktivních | volných: {slots}")
# Krok 3: Doplnění
if slots > 0:
print(f" │ [3] Doplňuji {slots} torrentů...")
added = add_torrents(qbt, cursor, active_hashes, slots, name)
if added == 0:
print(f" │ Žádné vhodné torrenty k přidání.")
else:
print(f" │ Přidáno: {added}")
else:
print(f" │ [3] Sloty plné ({active}/{max_concurrent}), přeskakuji.")
print(f" └──")
# ============================================================
# MAIN LOOP
# ============================================================
def main():
sys.stdout.reconfigure(encoding="utf-8")
now = datetime.now()
print("=" * 60)
print(f"DOWNLOAD MANAGER — Multi-client {now:%Y-%m-%d %H:%M:%S}")
for c in CLIENTS:
print(f"{c['name']} (max {c['max_concurrent']})")
print(f"Celkem slotů: {sum(c['max_concurrent'] for c in CLIENTS)}")
print("=" * 60)
db = connect_db()
cursor = db.cursor()
try:
# DB statistiky — celkové
cursor.execute("""
SELECT
SUM(CASE WHEN qb_completed_datetime IS NOT NULL THEN 1 ELSE 0 END),
SUM(CASE WHEN qb_state IS NULL AND torrent_content IS NOT NULL THEN 1 ELSE 0 END),
SUM(CASE WHEN qb_state IN ('incomplete','dead') THEN 1 ELSE 0 END)
FROM torrents
""")
db_completed, db_waiting, db_dead = cursor.fetchone()
print(f" DB — staženo: {db_completed or 0} "
f"| čeká: {db_waiting or 0} "
f"| dead/incomplete: {db_dead or 0}")
# DB statistiky — per-client
cursor.execute("""
SELECT qb_client, COUNT(*) AS cnt
FROM torrents
WHERE qb_client IS NOT NULL
GROUP BY qb_client
""")
per_client = cursor.fetchall()
if per_client:
parts = " | ".join(f"{name}: {cnt}" for name, cnt in per_client)
print(f" DB — per-client: {parts}")
# Zpracuj každý klient
# (druhý klient vidí stav DB aktualizovaný prvním)
for client_cfg in CLIENTS:
process_client(client_cfg, cursor)
finally:
db.close()
print("\n👋 Hotovo.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Projde /mnt/user/torrents/ultracc, pro každý soubor spočítá blake3
a porovná s tabulkou file_md5_index. Pokud je hash nalezen → soubor smaže.
Po smazání souborů odstraní prázdné adresáře.
"""
import os
import sys
import blake3
import pymysql
import paramiko
from pathlib import Path
# ============================================================
# CONFIG
# ============================================================
SCAN_DIR = "//tower/torrents/ultracc2"
SSH_CONFIG = {
"hostname": "192.168.1.76",
"port": 22,
"username": "root",
"password": "7309208104",
}
ULTRACC_DIRS = [
"/mnt/user/Torrents/UltraCC",
"/mnt/user/Torrents/UltraCC1",
"/mnt/user/Torrents/UltraCC2",
]
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
}
CHUNK_SIZE = 8 * 1024 * 1024 # 8 MB
DRY_RUN = False # True = pouze vypíše, nesmaže
# ============================================================
# HELPERS
# ============================================================
def compute_blake3(path: Path) -> bytes:
"""Vrátí blake3 digest jako 32 raw bytes."""
h = blake3.blake3()
with open(path, "rb") as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk:
break
h.update(chunk)
return h.digest()
def hash_in_db(cursor, digest: bytes):
"""Vrátí (host_name, full_path) prvního záznamu s daným hashem, nebo None."""
cursor.execute(
"SELECT host_name, full_path FROM file_md5_index WHERE blake3 = %s AND host_name = 'tower1' AND full_path LIKE '/mnt/user/#ColdData/Porno/%%' LIMIT 1",
(digest,)
)
return cursor.fetchone() # None nebo (host_name, full_path)
def remove_empty_dirs(root: str) -> int:
"""Rekurzivně smaže prázdné adresáře pod root. Vrátí počet smazaných."""
removed = 0
for dirpath, dirnames, filenames in os.walk(root, topdown=False):
if dirpath == root:
continue
try:
os.rmdir(dirpath)
print(f" [rmdir] {dirpath}")
removed += 1
except OSError:
pass
return removed
# ============================================================
# MAIN
# ============================================================
def set_ultracc_permissions():
"""Přes SSH nastaví na Tower chown nobody:users + chmod 777 pro všechny UltraCC adresáře."""
print("Nastavuji práva na Tower (UltraCC*)...")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(**SSH_CONFIG)
for d in ULTRACC_DIRS:
_, out, err = ssh.exec_command(
'chown -R nobody:users "%s" && chmod -R 777 "%s" && echo OK' % (d, d)
)
result = out.read().decode().strip()
error = err.read().decode().strip()
if result == "OK":
print(f" [OK] {d}")
else:
print(f" [CHYBA] {d}: {error}")
ssh.close()
print()
def main():
dry_run = DRY_RUN
set_ultracc_permissions()
if dry_run:
print("=== DRY RUN — nic se nesmaže ===\n")
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor()
scan_root = Path(SCAN_DIR)
if not scan_root.exists():
print(f"CHYBA: Adresář neexistuje: {SCAN_DIR}")
sys.exit(1)
files_checked = 0
files_deleted = 0
files_kept = 0
bytes_deleted = 0
for file_path in scan_root.rglob("*"):
if not file_path.is_file():
continue
files_checked += 1
size = file_path.stat().st_size
try:
digest = compute_blake3(file_path)
except OSError as e:
print(f" [CHYBA čtení] {file_path}: {e}")
continue
db_match = hash_in_db(cursor, digest)
if db_match:
db_host, db_path = db_match
print(f" [SMAZAT] {file_path} ({size:,} B)")
print(f" ↳ originál v DB: [{db_host}] {db_path}")
if not dry_run:
try:
file_path.unlink()
files_deleted += 1
bytes_deleted += size
except OSError as e:
print(f" [CHYBA smazání] {file_path}: {e}")
else:
files_deleted += 1
bytes_deleted += size
else:
print(f" [zachovat] {file_path} ({size:,} B)")
files_kept += 1
cursor.close()
conn.close()
print()
print(f"Zkontrolováno: {files_checked} souborů")
print(f"Ke smazání: {files_deleted} souborů ({bytes_deleted / 1024**3:.2f} GB)")
print(f"Zachováno: {files_kept} souborů")
if not dry_run and files_deleted > 0:
print("\nOdstraňuji prázdné adresáře...")
removed = remove_empty_dirs(SCAN_DIR)
print(f"Odstraněno prázdných adresářů: {removed}")
if dry_run:
print("\n(Dry run — žádné změny nebyly provedeny)")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Projde /mnt/user/torrents/ultracc, pro každý soubor spočítá blake3
a porovná s tabulkou file_md5_index. Pokud je hash nalezen → soubor smaže.
Po smazání souborů odstraní prázdné adresáře.
"""
import os
import sys
import blake3
import pymysql
import paramiko
from pathlib import Path
# ============================================================
# CONFIG
# ============================================================
SCAN_DIR = "//tower/torrents/ultracc"
SSH_CONFIG = {
"hostname": "192.168.1.76",
"port": 22,
"username": "root",
"password": "7309208104",
}
ULTRACC_DIRS = [
"/mnt/user/Torrents/UltraCC",
"/mnt/user/Torrents/UltraCC1",
"/mnt/user/Torrents/UltraCC2",
]
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
}
CHUNK_SIZE = 8 * 1024 * 1024 # 8 MB
DRY_RUN = True # True = pouze vypíše, nesmaže
# ============================================================
# HELPERS
# ============================================================
def compute_blake3(path: Path) -> bytes:
"""Vrátí blake3 digest jako 32 raw bytes."""
h = blake3.blake3()
with open(path, "rb") as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk:
break
h.update(chunk)
return h.digest()
def hash_in_db(cursor, digest: bytes):
"""Vrátí (host_name, full_path) prvního záznamu s daným hashem, nebo None."""
cursor.execute(
"SELECT host_name, full_path FROM file_md5_index WHERE blake3 = %s AND host_name = 'tower1' AND full_path LIKE '/mnt/user/#ColdData/Porno/%%' LIMIT 1",
(digest,)
)
return cursor.fetchone() # None nebo (host_name, full_path)
def remove_empty_dirs(root: str) -> int:
"""Rekurzivně smaže prázdné adresáře pod root. Vrátí počet smazaných."""
removed = 0
for dirpath, dirnames, filenames in os.walk(root, topdown=False):
if dirpath == root:
continue
try:
os.rmdir(dirpath)
print(f" [rmdir] {dirpath}")
removed += 1
except OSError:
pass
return removed
# ============================================================
# MAIN
# ============================================================
def set_ultracc_permissions():
"""Přes SSH nastaví na Tower chown nobody:users + chmod 777 pro všechny UltraCC adresáře."""
print("Nastavuji práva na Tower (UltraCC*)...")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(**SSH_CONFIG)
for d in ULTRACC_DIRS:
_, out, err = ssh.exec_command(
'chown -R nobody:users "%s" && chmod -R 777 "%s" && echo OK' % (d, d)
)
result = out.read().decode().strip()
error = err.read().decode().strip()
if result == "OK":
print(f" [OK] {d}")
else:
print(f" [CHYBA] {d}: {error}")
ssh.close()
print()
def main():
dry_run = DRY_RUN
set_ultracc_permissions()
if dry_run:
print("=== DRY RUN — nic se nesmaže ===\n")
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor()
scan_root = Path(SCAN_DIR)
if not scan_root.exists():
print(f"CHYBA: Adresář neexistuje: {SCAN_DIR}")
sys.exit(1)
files_checked = 0
files_deleted = 0
files_kept = 0
bytes_deleted = 0
for file_path in scan_root.rglob("*"):
if not file_path.is_file():
continue
files_checked += 1
size = file_path.stat().st_size
try:
digest = compute_blake3(file_path)
except OSError as e:
print(f" [CHYBA čtení] {file_path}: {e}")
continue
db_match = hash_in_db(cursor, digest)
if db_match:
db_host, db_path = db_match
print(f" [SMAZAT] {file_path} ({size:,} B)")
print(f" ↳ originál v DB: [{db_host}] {db_path}")
if not dry_run:
try:
file_path.unlink()
files_deleted += 1
bytes_deleted += size
except OSError as e:
print(f" [CHYBA smazání] {file_path}: {e}")
else:
files_deleted += 1
bytes_deleted += size
else:
print(f" [zachovat] {file_path} ({size:,} B)")
files_kept += 1
cursor.close()
conn.close()
print()
print(f"Zkontrolováno: {files_checked} souborů")
print(f"Ke smazání: {files_deleted} souborů ({bytes_deleted / 1024**3:.2f} GB)")
print(f"Zachováno: {files_kept} souborů")
if not dry_run and files_deleted > 0:
print("\nOdstraňuji prázdné adresáře...")
removed = remove_empty_dirs(SCAN_DIR)
print(f"Odstraněno prázdných adresářů: {removed}")
if dry_run:
print("\n(Dry run — žádné změny nebyly provedeny)")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Projde /mnt/user/torrents/ultracc, pro každý soubor spočítá blake3
a porovná s tabulkou file_md5_index. Pokud je hash nalezen → soubor smaže.
Po smazání souborů odstraní prázdné adresáře.
"""
import os
import sys
import blake3
import pymysql
import paramiko
from pathlib import Path
# ============================================================
# CONFIG
# ============================================================
SCAN_DIR = "//tower/torrents/ultracc1"
SSH_CONFIG = {
"hostname": "192.168.1.76",
"port": 22,
"username": "root",
"password": "7309208104",
}
ULTRACC_DIRS = [
"/mnt/user/Torrents/UltraCC",
"/mnt/user/Torrents/UltraCC1",
"/mnt/user/Torrents/UltraCC2",
]
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
}
CHUNK_SIZE = 8 * 1024 * 1024 # 8 MB
DRY_RUN = False # True = pouze vypíše, nesmaže
# ============================================================
# HELPERS
# ============================================================
def compute_blake3(path: Path) -> bytes:
"""Vrátí blake3 digest jako 32 raw bytes."""
h = blake3.blake3()
with open(path, "rb") as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk:
break
h.update(chunk)
return h.digest()
def hash_in_db(cursor, digest: bytes):
"""Vrátí (host_name, full_path) prvního záznamu s daným hashem, nebo None."""
cursor.execute(
"SELECT host_name, full_path FROM file_md5_index WHERE blake3 = %s AND host_name = 'tower1' AND full_path LIKE '/mnt/user/#ColdData/Porno/%%' LIMIT 1",
(digest,)
)
return cursor.fetchone() # None nebo (host_name, full_path)
def remove_empty_dirs(root: str) -> int:
"""Rekurzivně smaže prázdné adresáře pod root. Vrátí počet smazaných."""
removed = 0
for dirpath, dirnames, filenames in os.walk(root, topdown=False):
if dirpath == root:
continue
try:
os.rmdir(dirpath)
print(f" [rmdir] {dirpath}")
removed += 1
except OSError:
pass
return removed
# ============================================================
# MAIN
# ============================================================
def set_ultracc_permissions():
"""Přes SSH nastaví na Tower chown nobody:users + chmod 777 pro všechny UltraCC adresáře."""
print("Nastavuji práva na Tower (UltraCC*)...")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(**SSH_CONFIG)
for d in ULTRACC_DIRS:
_, out, err = ssh.exec_command(
'chown -R nobody:users "%s" && chmod -R 777 "%s" && echo OK' % (d, d)
)
result = out.read().decode().strip()
error = err.read().decode().strip()
if result == "OK":
print(f" [OK] {d}")
else:
print(f" [CHYBA] {d}: {error}")
ssh.close()
print()
def main():
dry_run = DRY_RUN
set_ultracc_permissions()
if dry_run:
print("=== DRY RUN — nic se nesmaže ===\n")
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor()
scan_root = Path(SCAN_DIR)
if not scan_root.exists():
print(f"CHYBA: Adresář neexistuje: {SCAN_DIR}")
sys.exit(1)
files_checked = 0
files_deleted = 0
files_kept = 0
bytes_deleted = 0
for file_path in scan_root.rglob("*"):
if not file_path.is_file():
continue
files_checked += 1
size = file_path.stat().st_size
try:
digest = compute_blake3(file_path)
except OSError as e:
print(f" [CHYBA čtení] {file_path}: {e}")
continue
db_match = hash_in_db(cursor, digest)
if db_match:
db_host, db_path = db_match
print(f" [SMAZAT] {file_path} ({size:,} B)")
print(f" ↳ originál v DB: [{db_host}] {db_path}")
if not dry_run:
try:
file_path.unlink()
files_deleted += 1
bytes_deleted += size
except OSError as e:
print(f" [CHYBA smazání] {file_path}: {e}")
else:
files_deleted += 1
bytes_deleted += size
else:
print(f" [zachovat] {file_path} ({size:,} B)")
files_kept += 1
cursor.close()
conn.close()
print()
print(f"Zkontrolováno: {files_checked} souborů")
print(f"Ke smazání: {files_deleted} souborů ({bytes_deleted / 1024**3:.2f} GB)")
print(f"Zachováno: {files_kept} souborů")
if not dry_run and files_deleted > 0:
print("\nOdstraňuji prázdné adresáře...")
removed = remove_empty_dirs(SCAN_DIR)
print(f"Odstraněno prázdných adresářů: {removed}")
if dry_run:
print("\n(Dry run — žádné změny nebyly provedeny)")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,286 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Incremental import — sktorrent.eu
- Prochází od nejnovějších torrentů
- Stahuje a ukládá .torrent soubory pro nové záznamy
- Zastaví se, jakmile narazí na torrent, který už v DB máme
- Nevyžaduje Selenium — stačí requests + BeautifulSoup + cookies
"""
import pymysql
import requests
import json
import time
import re
import sys
from bs4 import BeautifulSoup
from pathlib import Path
from datetime import datetime
import urllib.parse as urlparse
# ============================================================
# CONFIG
# ============================================================
COOKIE_FILE = Path("sktorrent_cookies.json")
BASE_URL = (
"https://sktorrent.eu/torrent/torrents.php"
"?active=0&category=24&order=data&by=DESC"
)
SLEEP_BETWEEN_PAGES = 2.0 # pauza mezi stránkami
SLEEP_BEFORE_DOWNLOAD = 1.5 # pauza před stažením každého .torrent souboru
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
# ============================================================
# CONNECT
# ============================================================
def connect_db():
return pymysql.connect(**DB_CONFIG)
def build_session():
if not COOKIE_FILE.exists():
raise FileNotFoundError(f"Cookie soubor nenalezen: {COOKIE_FILE}")
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies = json.load(f)
session = requests.Session()
session.headers["User-Agent"] = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)
for c in cookies:
session.cookies.set(c["name"], c["value"], domain=c.get("domain", ""))
return session
# ============================================================
# PARSE ONE LISTING PAGE
# ============================================================
def parse_page(html):
"""
Vrátí seznam dict pro každý torrent řádek na stránce.
Prázdný seznam = konec paginace nebo chyba.
"""
soup = BeautifulSoup(html, "html.parser")
results = []
for row in soup.select("table tr"):
cells = row.find_all("td")
if len(cells) != 7:
continue
# td[1] — odkaz na stažení: download.php?id=<hash>&f=<filename>
dl_a = cells[1].find("a", href=re.compile(r"download\.php\?id="))
if not dl_a:
continue
download_url = dl_a["href"]
if not download_url.startswith("http"):
download_url = "https://sktorrent.eu/torrent/" + download_url
m_hash = re.search(r"id=([a-f0-9A-F]+)", download_url)
if not m_hash:
continue
torrent_hash = m_hash.group(1).lower()
parsed_dl = urlparse.urlparse(download_url)
dl_query = urlparse.parse_qs(parsed_dl.query)
torrent_filename = dl_query.get("f", ["unknown.torrent"])[0]
# td[2] — název, details link, velikost, datum
title_a = cells[2].find("a", href=re.compile(r"details\.php\?id="))
if not title_a:
continue
title_visible = title_a.get_text(strip=True)
title_full = title_a.get("title", title_visible)
details_link = title_a["href"]
if not details_link.startswith("http"):
details_link = "https://sktorrent.eu/torrent/" + details_link
cell2_text = cells[2].get_text(" ", strip=True)
size_match = re.search(r"Velkost\s+([\d\.,]+\s*[KMG]B)", cell2_text, re.IGNORECASE)
added_match = re.search(r"Pridany\s+(\d+/\d+/\d+)\s+(?:o\s+)?(\d+:\d+)", cell2_text, re.IGNORECASE)
size_pretty = size_match.group(1).strip() if size_match else None
added_mysql = None
if added_match:
try:
d, mo, y = added_match.group(1).split("/")
t = added_match.group(2) + ":00"
added_mysql = f"{y}-{mo}-{d} {t}"
except Exception:
pass
# td[0] — kategorie
category = cells[0].get_text(strip=True)
# td[4] seeders, td[5] leechers
try:
seeders = int(cells[4].get_text(strip=True))
except ValueError:
seeders = 0
try:
leechers = int(cells[5].get_text(strip=True))
except ValueError:
leechers = 0
results.append({
"torrent_hash": torrent_hash,
"download_url": download_url,
"details_link": details_link,
"torrent_filename": torrent_filename,
"category": category,
"title_visible": title_visible,
"title_full": title_full,
"size_pretty": size_pretty,
"added_datetime": added_mysql,
"seeders": seeders,
"leechers": leechers,
})
return results
# ============================================================
# DOWNLOAD .TORRENT FILE
# ============================================================
def download_torrent(session, url):
try:
r = session.get(url, timeout=15)
r.raise_for_status()
if len(r.content) < 20:
return None
return r.content
except Exception as e:
print(f" ⚠️ Stažení selhalo: {e}")
return None
# ============================================================
# DB INSERT
# ============================================================
INSERT_SQL = """
INSERT INTO torrents (
torrent_hash, details_link, download_url, category,
title_visible, title_full, size_pretty, added_datetime,
seeders, leechers, torrent_filename, torrent_content
) VALUES (
%(torrent_hash)s, %(details_link)s, %(download_url)s, %(category)s,
%(title_visible)s, %(title_full)s, %(size_pretty)s, %(added_datetime)s,
%(seeders)s, %(leechers)s, %(torrent_filename)s, %(torrent_content)s
)
ON DUPLICATE KEY UPDATE
seeders = VALUES(seeders),
leechers = VALUES(leechers),
download_url = VALUES(download_url),
torrent_content = COALESCE(VALUES(torrent_content), torrent_content)
"""
# ============================================================
# MAIN
# ============================================================
def main():
sys.stdout.reconfigure(encoding="utf-8")
print("=" * 60)
print("INCREMENTAL IMPORT — sktorrent.eu")
print(f"Spuštěno: {datetime.now():%Y-%m-%d %H:%M:%S}")
print("Pořadí: nejnovější → nejstarší | stop při první shodě")
print("=" * 60)
session = build_session()
db = connect_db()
cursor = db.cursor()
new_count = 0
page = 0
stop = False
while not stop:
url = f"{BASE_URL}&page={page}"
try:
r = session.get(url, timeout=15)
r.raise_for_status()
except Exception as e:
print(f"⚠️ Stránka {page} — chyba: {e}")
break
if "login.php" in r.url or "Prihlas sa" in r.text:
print("❌ Cookies expiraly — spusť přihlašovací Selenium skript a obnov cookies.")
break
rows = parse_page(r.text)
if not rows:
print(f" Stránka {page} — žádné záznamy, konec.")
break
print(f"\n📄 Stránka {page} ({len(rows)} torrentů)")
for item in rows:
# Zkontroluj DB
cursor.execute(
"SELECT 1 FROM torrents WHERE torrent_hash = %s",
(item["torrent_hash"],)
)
exists = cursor.fetchone()
if exists:
print(f" ⏹ Již v DB: {item['title_visible']} → zastavuji import.")
stop = True
break
# Nový torrent — stáhni .torrent soubor
print(f" ⬇️ Nový: {item['title_visible']}")
time.sleep(SLEEP_BEFORE_DOWNLOAD)
content = download_torrent(session, item["download_url"])
if content:
print(f" ✔ Staženo ({len(content):,} B)")
else:
print(f" ✖ Nepodařilo se stáhnout, ukládám bez obsahu")
item["torrent_content"] = content
cursor.execute(INSERT_SQL, item)
new_count += 1
if not stop:
page += 1
time.sleep(SLEEP_BETWEEN_PAGES)
# ============================================================
# SUMMARY
# ============================================================
print()
print("=" * 60)
print(f"Hotovo: {datetime.now():%Y-%m-%d %H:%M:%S}")
print(f"Nových torrentů uloženo : {new_count}")
print(f"Stránek prošlo : {page}")
print("=" * 60)
db.close()
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
[
{
"name": "uid",
"value": "646071",
"domain": "sktorrent.eu",
"path": "/",
"expires": 1798003565.462807,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
},
{
"name": "pass",
"value": "91df6b497860582e09a7b333569d0187",
"domain": "sktorrent.eu",
"path": "/",
"expires": 1798003565.463191,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
}
]

BIN
Seedbox/torrent_report.xlsx Normal file

Binary file not shown.