Compare commits
13 Commits
0710af0f82
...
claude/bea
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ce8fa0080 | |||
| c4f2d8b13d | |||
| a74ad8ff00 | |||
| afbca5b348 | |||
| 20c4a7d8b4 | |||
| 489b236b9b | |||
| 197cb3f8db | |||
| b37db5397e | |||
| 15b498ca55 | |||
| 7646f6f68f | |||
| e0cb02c490 | |||
| 6b8728360c | |||
| d57f7d75ce |
94
Seedbox/50 PrintFilenamesFromTorrentFiles.py
Normal file
94
Seedbox/50 PrintFilenamesFromTorrentFiles.py
Normal 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()
|
||||
137
Seedbox/51 ExportTorrentObsahuDoExcel.py
Normal file
137
Seedbox/51 ExportTorrentObsahuDoExcel.py
Normal 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()
|
||||
48
Seedbox/52 UložBalíkAudioknih.py
Normal file
48
Seedbox/52 UložBalíkAudioknih.py
Normal 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()
|
||||
220
Seedbox/60 AktualizaceSeeders.py
Normal file
220
Seedbox/60 AktualizaceSeeders.py
Normal 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
383
Seedbox/70 Manager.py
Normal 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()
|
||||
182
Seedbox/80 DeleteWhatWeHaveUltraCC2.py
Normal file
182
Seedbox/80 DeleteWhatWeHaveUltraCC2.py
Normal 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()
|
||||
182
Seedbox/81 DeleteWhatWeHaveUltraCC.py
Normal file
182
Seedbox/81 DeleteWhatWeHaveUltraCC.py
Normal 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()
|
||||
182
Seedbox/82 DeleteWhatWeHaveUltraCC1.py
Normal file
182
Seedbox/82 DeleteWhatWeHaveUltraCC1.py
Normal 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()
|
||||
286
Seedbox/95 IncrementalImport.py
Normal file
286
Seedbox/95 IncrementalImport.py
Normal 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()
|
||||
473
Seedbox/Balík audioknih.torrent
Normal file
473
Seedbox/Balík audioknih.torrent
Normal file
File diff suppressed because one or more lines are too long
22
Seedbox/sktorrent_cookies.json
Normal file
22
Seedbox/sktorrent_cookies.json
Normal 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
BIN
Seedbox/torrent_report.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user