Compare commits

...

35 Commits

Author SHA1 Message Date
aff7993093 git 2026-02-04 05:59:17 +01:00
3d11661997 git 2026-02-01 07:18:20 +01:00
7b0404bfe3 z230 2026-01-30 10:28:42 +01:00
0b7475c5c4 reporter 2026-01-20 06:18:42 +01:00
edee7cb8dd reporter 2026-01-20 06:18:29 +01:00
a990f7b394 git 2026-01-19 21:05:28 +01:00
52ae7cf60d git 2026-01-19 07:10:41 +01:00
5aac1b29c6 z230 2026-01-16 13:31:02 +01:00
c3c723e2e8 z230 2026-01-16 13:19:26 +01:00
f451317b6f z230 2026-01-16 13:10:07 +01:00
a4ede43153 z230 2026-01-13 14:43:44 +01:00
74083614e5 Remove PyCharm IDE files from repository 2026-01-11 08:18:35 +01:00
6d8ea05edb git 2026-01-11 08:15:46 +01:00
387d09b59c vbnotebook 2026-01-10 15:23:49 +01:00
a64f4b663f vbnotebook 2026-01-10 08:56:58 +01:00
84e38b01f1 vbnotebook 2026-01-09 06:38:19 +01:00
44162413e1 vbnotebook 2026-01-08 07:23:30 +01:00
1fc3323afd vbnotebook 2026-01-08 07:23:09 +01:00
8ea687b724 vbnotebook 2026-01-08 07:22:05 +01:00
b08c11b815 z230 2025-12-30 08:51:10 +01:00
b4e5c2f46e Save local IDE settings 2025-12-30 08:42:44 +01:00
a113f68e97 Save local settings before merge 2025-12-30 08:40:56 +01:00
5f01729bce WIP: torrent manipulation logic 2025-12-18 11:25:10 +01:00
bf75bffb02 z230 2025-12-18 11:22:00 +01:00
0769bd2670 z230 2025-12-18 07:53:48 +01:00
5f1c55243e reporter 2025-12-15 07:03:22 +01:00
314eb20e6b reporter 2025-12-15 06:28:20 +01:00
9c95999a26 Remove PyCharm IDE files and add .gitignore 2025-12-15 06:15:28 +01:00
1ed48cd640 reporter 2025-12-15 06:13:07 +01:00
dae0e763f4 Merge remote-tracking branch 'gitea/master' 2025-12-15 06:12:04 +01:00
3631f6cdf5 reporter 2025-12-15 06:11:53 +01:00
1fc6f9f3f5 revert 6d4b3bf6d7
revert Merge branch 'master' of ssh://192.168.1.76:2222/administrator/insurance
2025-12-15 06:11:21 +01:00
6e8395890d z230 2025-12-08 19:26:13 +01:00
bc35cbdfac vbnotebook 2025-12-08 06:53:29 +01:00
af295ff63c vbnotebook 2025-12-08 06:08:04 +01:00
53 changed files with 5233 additions and 1852 deletions

3
.gitignore vendored
View File

@@ -6,8 +6,9 @@ __pycache__/
*.pyc *.pyc
*.log *.log
# IDE # IDE (PyCharm)
.idea/ .idea/
*.iml
# OS # OS
.DS_Store .DS_Store

3
.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

10
.idea/Torrents.iml generated
View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (Torrents)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Torrents.iml" filepath="$PROJECT_DIR$/.idea/Torrents.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,132 +0,0 @@
# pip install requests_pkcs12 pymysql
from requests_pkcs12 import Pkcs12Adapter
import requests
import xml.etree.ElementTree as ET
from datetime import date
import pymysql
from pymysql.cursors import DictCursor
from pprint import pprint
# ========== CONFIG ==========
PFX_PATH = r"mbcert.pfx"
PFX_PASS = "Vlado7309208104++"
VERIFY = True
EP_STAV = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/stavPojisteniB2B"
SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/"
NS_STAV = "http://xmlns.gemsystem.cz/stavPojisteniB2B"
# RC provided by you (can contain slash; we normalize before sending/saving)
RC = "7309208104"
# RC = "280616/091"
K_DATU = date.today().isoformat()
# MySQL (table already created as per previous DDL)
MYSQL_CFG = dict(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
cursorclass=DictCursor,
autocommit=False,
)
# ========== HELPERS ==========
def to_int_or_none(val):
if val is None:
return None
s = str(val).strip()
return int(s) if s.isdigit() else None
def normalize_rc(rc: str) -> str:
return rc.replace("/", "").strip()
def post_soap(endpoint: str, body_xml: str) -> str:
env = f'''<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="{SOAP_NS}">
<soap:Body>{body_xml}</soap:Body>
</soap:Envelope>'''
s = requests.Session()
s.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_PATH, pkcs12_password=PFX_PASS))
r = s.post(
endpoint,
data=env.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": "process"},
timeout=30,
verify=VERIFY,
)
print("HTTP:", r.status_code)
return r.text
def parse_stav(xml_text: str):
NS = {"soap": SOAP_NS, "s": NS_STAV}
root = ET.fromstring(xml_text)
out = {
"stav": None,
"kodPojistovny": None,
"nazevPojistovny": None,
"pojisteniKod": None,
"stavVyrizeniPozadavku": None,
}
node = root.find(".//s:stavPojisteni", NS)
if node is not None:
for tag in ("stav", "kodPojistovny", "nazevPojistovny", "pojisteniKod"):
el = node.find(f"s:{tag}", NS)
out[tag] = el.text.strip() if el is not None and el.text else None
st = root.find(".//s:stavVyrizeniPozadavku", NS)
out["stavVyrizeniPozadavku"] = st.text.strip() if st is not None and st.text else None
return out
def save_stav_pojisteni(rc: str, k_datu: str, parsed: dict, response_xml: str) -> int:
"""
Insert one log row into medevio.vzp_stav_pojisteni and print what is being saved.
Assumes the table already exists (no DDL/verification here).
"""
payload = {
"rc": normalize_rc(rc),
"k_datu": k_datu,
"stav": to_int_or_none(parsed.get("stav")), # handles 'X' -> None
"kod_pojistovny": parsed.get("kodPojistovny"),
"nazev_pojistovny": parsed.get("nazevPojistovny"),
"pojisteni_kod": parsed.get("pojisteniKod"),
"stav_vyrizeni": to_int_or_none(parsed.get("stavVyrizeniPozadavku")),
"response_xml": response_xml,
}
print("\n=== vzp_stav_pojisteni: will insert ===")
pprint(payload)
sql = """
INSERT INTO vzp_stav_pojisteni
(rc, k_datu, stav, kod_pojistovny, nazev_pojistovny, pojisteni_kod, stav_vyrizeni, response_xml)
VALUES
(%(rc)s, %(k_datu)s, %(stav)s, %(kod_pojistovny)s, %(nazev_pojistovny)s, %(pojisteni_kod)s, %(stav_vyrizeni)s, %(response_xml)s)
"""
conn = pymysql.connect(**MYSQL_CFG)
try:
with conn.cursor() as cur:
cur.execute(sql, payload)
conn.commit()
finally:
conn.close()
print("Inserted 1 row into vzp_stav_pojisteni.")
return 1
# ========== MAIN ==========
if __name__ == "__main__":
rc_norm = normalize_rc(RC)
body = f"""
<ns1:stavPojisteniB2B xmlns:ns1="{NS_STAV}">
<ns1:cisloPojistence>{rc_norm}</ns1:cisloPojistence>
<ns1:kDatu>{K_DATU}</ns1:kDatu>
</ns1:stavPojisteniB2B>""".strip()
xml = post_soap(EP_STAV, body)
parsed = parse_stav(xml)
print(parsed) # keep your quick print
# Save to MySQL and print exactly what is saved
save_stav_pojisteni(RC, K_DATU, parsed, xml)

View File

@@ -1,271 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Call VZP RegistracePojistencePZSB2B for one patient (001 = VPL),
parse the response, upsert rows into medevio.vzp_registrace,
and print what is being saved.
"""
# pip install requests_pkcs12 pymysql
from requests_pkcs12 import Pkcs12Adapter
import requests
import xml.etree.ElementTree as ET
from datetime import date
import pymysql
from pymysql.cursors import DictCursor
from pprint import pprint
from functions import get_medicus_connection
from functions import get_mysql_connection
import time, random,socket
# ------------------- CONFIG -------------------
ENDPOINT = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/RegistracePojistencePZSB2B" # case-sensitive
PFX_PATH = r"mbcert.pfx" # <-- your .pfx path
PFX_PASS = "Vlado7309208104++" # <-- your export password
VERIFY = True # or path to CA PEM, e.g. r"C:\certs\vzp_ca.pem"
# Patient + query
RC = "7309208104" # rodné číslo without slash
# RC = "280616/091" # rodné číslo without slash
K_DATU = date.today().isoformat() # YYYY-MM-DD
ODBORNOSTI = ["001"] # VPL (adult GP)
# MySQL
if socket.gethostname().strip() == "NTBVBHP470G10":
MYSQL_CFG = dict(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
cursorclass=DictCursor,
autocommit=True, # or False if you prefer manual commit
)
elif socket.gethostname().strip() == "SESTRA":
MYSQL_CFG = dict(
host="127.0.0.1",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
cursorclass=DictCursor,
autocommit=True, # or False if you prefer manual commit
)
# Namespaces (from your response/WSDL)
NS = {
"soap": "http://schemas.xmlsoap.org/soap/envelope/",
"rp": "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1",
}
# ------------------- HELPERS -------------------
def normalize_rc(rc: str) -> str:
return rc.replace("/", "").strip()
def build_envelope(rc: str, k_datu: str, odb_list: list[str]) -> str:
odb_xml = "".join([f"<ns1:kodOdbornosti>{kod}</ns1:kodOdbornosti>" for kod in odb_list])
inner = f"""
<ns1:registracePojistencePZSB2B xmlns:ns1="{NS['rp']}">
<ns1:cisloPojistence>{rc}</ns1:cisloPojistence>
<ns1:kDatu>{k_datu}</ns1:kDatu>
<ns1:seznamOdbornosti>
{odb_xml}
</ns1:seznamOdbornosti>
</ns1:registracePojistencePZSB2B>""".strip()
return f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="{NS['soap']}">
<soap:Body>
{inner}
</soap:Body>
</soap:Envelope>"""
def parse_registrace(xml_text: str):
"""
Return (rows, stav_vyrizeni) where rows is a list of dicts
for each <odbornost> item in the response.
"""
root = ET.fromstring(xml_text)
items = root.findall(".//rp:seznamOdbornosti/rp:odbornost", NS)
out = []
for it in items:
def g(tag, ctx=it):
el = ctx.find(f"rp:{tag}", NS)
return el.text.strip() if el is not None and el.text else None
poj = it.find("rp:zdravotniPojistovna", NS)
poj_kod = poj.find("rp:kod", NS).text.strip() if (poj is not None and poj.find("rp:kod", NS) is not None) else None
poj_zkr = poj.find("rp:zkratka", NS).text.strip() if (poj is not None and poj.find("rp:zkratka", NS) is not None) else None
spec = it.find("rp:odbornost", NS)
odb_kod = spec.find("rp:kod", NS).text.strip() if (spec is not None and spec.find("rp:kod", NS) is not None) else None
odb_naz = spec.find("rp:nazev", NS).text.strip() if (spec is not None and spec.find("rp:nazev", NS) is not None) else None
out.append(dict(
icz=g("ICZ"),
icp=g("ICP"),
nazev_icp=g("nazevICP"),
nazev_szz=g("nazevSZZ"),
poj_kod=poj_kod,
poj_zkratka=poj_zkr,
odbornost_kod=odb_kod,
odbornost_nazev=odb_naz,
datum_registrace=g("datumRegistrace"),
datum_zahajeni=g("datumZahajeni"),
datum_ukonceni=g("datumUkonceni"),
))
st = root.find(".//rp:stavVyrizeniPozadavku", NS)
stav_vyrizeni = st.text.strip() if (st is not None and st.text) else None
return out, stav_vyrizeni
def upsert_rows(rc: str, query_date: str, rows: list[dict], stav_vyrizeni: str, xml_text: str) -> int:
"""
Insert or update rows into medevio.vzp_registrace and print each payload.
Assumes the table already exists (as per your DDL).
"""
if not rows:
print("No <odbornost> rows found; nothing to save.")
return 0
sql = """
INSERT INTO vzp_registrace
(rc, query_date, odbornost_kod, odbornost_nazev,
icz, icp, nazev_icp, nazev_szz,
poj_kod, poj_zkratka,
datum_registrace, datum_zahajeni, datum_ukonceni,
stav_vyrizeni, response_xml)
VALUES
(%(rc)s, %(query_date)s, %(odbornost_kod)s, %(odbornost_nazev)s,
%(icz)s, %(icp)s, %(nazev_icp)s, %(nazev_szz)s,
%(poj_kod)s, %(poj_zkratka)s,
%(datum_registrace)s, %(datum_zahajeni)s, %(datum_ukonceni)s,
%(stav_vyrizeni)s, %(response_xml)s)
ON DUPLICATE KEY UPDATE
odbornost_nazev = VALUES(odbornost_nazev),
nazev_icp = VALUES(nazev_icp),
nazev_szz = VALUES(nazev_szz),
poj_kod = VALUES(poj_kod),
poj_zkratka = VALUES(poj_zkratka),
datum_registrace= VALUES(datum_registrace),
datum_zahajeni = VALUES(datum_zahajeni),
datum_ukonceni = VALUES(datum_ukonceni),
stav_vyrizeni = VALUES(stav_vyrizeni),
response_xml = VALUES(response_xml),
updated_at = CURRENT_TIMESTAMP
"""
rc_norm = normalize_rc(rc)
qd = query_date or date.today().isoformat()
payloads = []
for r in rows:
payload = {
"rc": rc_norm,
"query_date": qd,
**r,
"stav_vyrizeni": stav_vyrizeni,
"response_xml": xml_text,
}
payloads.append(payload)
# Print what we're going to save
print("\n=== Will save the following payload(s) to medevio.vzp_registrace ===")
for p in payloads:
pprint(p)
conn = pymysql.connect(**MYSQL_CFG)
try:
with conn.cursor() as cur:
cur.executemany(sql, payloads)
conn.commit()
finally:
conn.close()
print(f"\nUpserted rows: {len(payloads)}")
return len(payloads)
def prepare_processed_rcs():
consql=get_mysql_connection()
cursql=consql.cursor()
sql="""
WITH ranked AS (
SELECT
vreg.*,
ROW_NUMBER() OVER (
PARTITION BY rc
ORDER BY query_date DESC
) AS rn
FROM vzp_registrace AS vreg
)
SELECT rc
FROM ranked
WHERE rn = 1
"""
cursql.execute(sql)
rows=cursql.fetchall()
print(f"Pocet jiz zpracovanych rodnych cisel v MYSQL MEDEVIO je {len(rows)}")
rc_set_vzp = {row["rc"] for row in rows}
return (rc_set_vzp)
# ------------------- MAIN FLOW -------------------
def main():
con = get_medicus_connection()
cur = con.cursor()
cur.execute("select rodcis, prijmeni, jmeno from kar where rodcis starting with '9'")
# cur.execute("select first 2 rodcis, prijmeni, jmeno from kar where rodcis starting with '0'")
# Vytvor seznam rodnych cisel, která už máme
rc_set_vzp = prepare_processed_rcs()
rows = cur.fetchall()
print(f"Pocet vybranych radku z tabulky KAR je: {len(rows)}")
for row in rows:
if row[0] in rc_set_vzp:
continue
else:
print(row[0], row[1], row[2])
K_DATU = date.today().isoformat() # YYYY-MM-DD
ODBORNOSTI = ["001"]
RC=row[0]
# Build SOAP envelope
envelope = build_envelope(RC, K_DATU, ODBORNOSTI)
# mTLS session
session = requests.Session()
session.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_PATH, pkcs12_password=PFX_PASS))
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "process", # Oracle composite usually expects this
}
# Call service
resp = session.post(ENDPOINT, data=envelope.encode("utf-8"),
headers=headers, timeout=30, verify=VERIFY)
print("HTTP:", resp.status_code)
# (Optional) Uncomment to see raw XML
# print(resp.text)
# Parse and save
rows, stav = parse_registrace(resp.text)
upsert_rows(RC, K_DATU, rows, stav, resp.text)
time.sleep(random.uniform(1, 5))
if __name__ == "__main__":
main()

View File

@@ -1,262 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Call VZP RegistracePojistencePZSB2B for one patient (001 = VPL),
parse the response, upsert rows into medevio.vzp_registrace,
and print what is being saved.
"""
# pip install requests_pkcs12 pymysql
from requests_pkcs12 import Pkcs12Adapter
import requests
import xml.etree.ElementTree as ET
from datetime import date
import pymysql
from pymysql.cursors import DictCursor
from pprint import pprint
from functions import get_medicus_connection
from functions import get_mysql_connection
import time, random,socket
# ------------------- CONFIG -------------------
ENDPOINT = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/RegistracePojistencePZSB2B" # case-sensitive
PFX_PATH = r"mbcert.pfx" # <-- your .pfx path
PFX_PASS = "Vlado7309208104++" # <-- your export password
VERIFY = True # or path to CA PEM, e.g. r"C:\certs\vzp_ca.pem"
# Patient + query
RC = "7309208104" # rodné číslo without slash
# RC = "280616/091" # rodné číslo without slash
K_DATU = date.today().isoformat() # YYYY-MM-DD
ODBORNOSTI = ["001"] # VPL (adult GP)
# MySQL
if socket.gethostname().strip() == "NTBVBHP470G10":
MYSQL_CFG = dict(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
cursorclass=DictCursor,
autocommit=True, # or False if you prefer manual commit
)
elif socket.gethostname().strip() == "SESTRA":
MYSQL_CFG = dict(
host="127.0.0.1",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
cursorclass=DictCursor,
autocommit=True, # or False if you prefer manual commit
)
# Namespaces (from your response/WSDL)
NS = {
"soap": "http://schemas.xmlsoap.org/soap/envelope/",
"rp": "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1",
}
# ------------------- HELPERS -------------------
def normalize_rc(rc: str) -> str:
return rc.replace("/", "").strip()
# Ukázka XML Požadavek:
# <registracePojistencePZSB2B xmlns="http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1">
# <cisloPojistence>801212855</cisloPojistence>
# <seznamOdbornosti>
# <kodOdbornosti>002</kodOdbornosti>
# </seznamOdbornosti>
# </registracePojistencePZSB2B>
def build_envelope(rc: str, k_datu: str, odb_list: list[str]) -> str:
odb_xml = "".join([f"<ns1:kodOdbornosti>{kod}</ns1:kodOdbornosti>" for kod in odb_list])
inner = f"""
<ns1:registracePojistencePZSB2B xmlns:ns1="{NS['rp']}">
<ns1:cisloPojistence>{rc}</ns1:cisloPojistence>
<ns1:kDatu>{k_datu}</ns1:kDatu>
<ns1:seznamOdbornosti>
{odb_xml}
</ns1:seznamOdbornosti>
</ns1:registracePojistencePZSB2B>""".strip()
return f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="{NS['soap']}">
<soap:Body>
{inner}
</soap:Body>
</soap:Envelope>"""
def parse_registrace(xml_text: str):
"""
Return (rows, stav_vyrizeni) where rows is a list of dicts
for each <odbornost> item in the response.
"""
root = ET.fromstring(xml_text)
items = root.findall(".//rp:seznamOdbornosti/rp:odbornost", NS)
out = []
for it in items:
def g(tag, ctx=it):
el = ctx.find(f"rp:{tag}", NS)
return el.text.strip() if el is not None and el.text else None
poj = it.find("rp:zdravotniPojistovna", NS)
poj_kod = poj.find("rp:kod", NS).text.strip() if (poj is not None and poj.find("rp:kod", NS) is not None) else None
poj_zkr = poj.find("rp:zkratka", NS).text.strip() if (poj is not None and poj.find("rp:zkratka", NS) is not None) else None
spec = it.find("rp:odbornost", NS)
odb_kod = spec.find("rp:kod", NS).text.strip() if (spec is not None and spec.find("rp:kod", NS) is not None) else None
odb_naz = spec.find("rp:nazev", NS).text.strip() if (spec is not None and spec.find("rp:nazev", NS) is not None) else None
out.append(dict(
icz=g("ICZ"),
icp=g("ICP"),
nazev_icp=g("nazevICP"),
nazev_szz=g("nazevSZZ"),
poj_kod=poj_kod,
poj_zkratka=poj_zkr,
odbornost_kod=odb_kod,
odbornost_nazev=odb_naz,
datum_registrace=g("datumRegistrace"),
datum_zahajeni=g("datumZahajeni"),
datum_ukonceni=g("datumUkonceni"),
))
st = root.find(".//rp:stavVyrizeniPozadavku", NS)
stav_vyrizeni = st.text.strip() if (st is not None and st.text) else None
return out, stav_vyrizeni
def upsert_rows(rc: str, query_date: str, rows: list[dict], stav_vyrizeni: str, xml_text: str) -> int:
"""
Insert or update rows into medevio.vzp_registrace and print each payload.
Assumes the table already exists (as per your DDL).
"""
if not rows:
print("No <odbornost> rows found; nothing to save.")
return 0
sql = """
INSERT INTO vzp_registrace
(rc, query_date, odbornost_kod, odbornost_nazev,
icz, icp, nazev_icp, nazev_szz,
poj_kod, poj_zkratka,
datum_registrace, datum_zahajeni, datum_ukonceni,
stav_vyrizeni, response_xml)
VALUES
(%(rc)s, %(query_date)s, %(odbornost_kod)s, %(odbornost_nazev)s,
%(icz)s, %(icp)s, %(nazev_icp)s, %(nazev_szz)s,
%(poj_kod)s, %(poj_zkratka)s,
%(datum_registrace)s, %(datum_zahajeni)s, %(datum_ukonceni)s,
%(stav_vyrizeni)s, %(response_xml)s)
ON DUPLICATE KEY UPDATE
odbornost_nazev = VALUES(odbornost_nazev),
nazev_icp = VALUES(nazev_icp),
nazev_szz = VALUES(nazev_szz),
poj_kod = VALUES(poj_kod),
poj_zkratka = VALUES(poj_zkratka),
datum_registrace= VALUES(datum_registrace),
datum_zahajeni = VALUES(datum_zahajeni),
datum_ukonceni = VALUES(datum_ukonceni),
stav_vyrizeni = VALUES(stav_vyrizeni),
response_xml = VALUES(response_xml),
updated_at = CURRENT_TIMESTAMP
"""
rc_norm = normalize_rc(rc)
qd = query_date or date.today().isoformat()
payloads = []
for r in rows:
payload = {
"rc": rc_norm,
"query_date": qd,
**r,
"stav_vyrizeni": stav_vyrizeni,
"response_xml": xml_text,
}
payloads.append(payload)
# Print what we're going to save
print("\n=== Will save the following payload(s) to medevio.vzp_registrace ===")
for p in payloads:
pprint(p)
conn = pymysql.connect(**MYSQL_CFG)
try:
with conn.cursor() as cur:
cur.executemany(sql, payloads)
conn.commit()
finally:
conn.close()
print(f"\nUpserted rows: {len(payloads)}")
return len(payloads)
def prepare_processed_rcs():
consql=get_mysql_connection()
cursql=consql.cursor()
sql="""
WITH ranked AS (
SELECT
vreg.*,
ROW_NUMBER() OVER (
PARTITION BY rc
ORDER BY query_date DESC
) AS rn
FROM vzp_registrace AS vreg
)
SELECT rc
FROM ranked
WHERE rn = 1
"""
cursql.execute(sql)
rows=cursql.fetchall()
print(f"Pocet jiz zpracovanych rodnych cisel v MYSQL MEDEVIO je {len(rows)}")
rc_set_vzp = {row["rc"] for row in rows}
return (rc_set_vzp)
# ------------------- MAIN FLOW -------------------
def main():
K_DATU = date.today().isoformat() # YYYY-MM-DD
ODBORNOSTI = ["001"]
RC='7309208104'
# Build SOAP envelope
envelope = build_envelope(RC, K_DATU, ODBORNOSTI)
# mTLS session
session = requests.Session()
session.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_PATH, pkcs12_password=PFX_PASS))
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "process", # Oracle composite usually expects this
}
# Call service
resp = session.post(ENDPOINT, data=envelope.encode("utf-8"),
headers=headers, timeout=30, verify=VERIFY)
print("HTTP:", resp.status_code)
# (Optional) Uncomment to see raw XML
print(resp.text)
# Parse and save
rows, stav = parse_registrace(resp.text)
print(rows,stav)
time.sleep(random.uniform(1, 5))
if __name__ == "__main__":
main()

View File

@@ -1,308 +0,0 @@
#kód bude vkládat i řádky pro pacienty bez registrovaného lékař v oboru 001#!/usr/bin/env python3 -*- coding: utf-8 -*- """ Call VZP RegistracePojistencePZSB2B for one patient (001 = VPL), parse the response, upsert rows into medevio.vzp_registrace, and print what is being saved. """ # pip install requests_pkcs12 pymysql from requests_pkcs12 import Pkcs12Adapter import requests import xml.etree.ElementTree as ET from datetime import date import pymysql from pymysql.cursors import DictCursor from pprint import pprint from functions import get_medicus_connection from functions import get_mysql_connection import time, random,socket # ------------------- CONFIG ------------------- ENDPOINT = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/RegistracePojistencePZSB2B" # case-sensitive PFX_PATH = r"mbcert.pfx" # <-- your .pfx path PFX_PASS = "Vlado7309208104++" # <-- your export password VERIFY = True # or path to CA PEM, e.g. r"C:\certs\vzp_ca.pem" Patient + query RC = "7309208104" # rodné číslo without slash RC = "280616/091" # rodné
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Call VZP RegistracePojistencePZSB2B for one patient (001 = VPL),
parse the response, upsert rows into medevio.vzp_registrace,
and print what is being saved.
"""
# pip install requests_pkcs12 pymysql
from requests_pkcs12 import Pkcs12Adapter
import requests
import xml.etree.ElementTree as ET
from datetime import date
import pymysql
from pymysql.cursors import DictCursor
from pprint import pprint
from functions import get_medicus_connection
from functions import get_mysql_connection
import time, random,socket
# ------------------- CONFIG -------------------
ENDPOINT = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/RegistracePojistencePZSB2B" # case-sensitive
PFX_PATH = r"mbcert.pfx" # <-- your .pfx path
PFX_PASS = "Vlado7309208104++" # <-- your export password
VERIFY = True # or path to CA PEM, e.g. r"C:\certs\vzp_ca.pem"
# Patient + query
RC = "7309208104" # rodné číslo without slash
# RC = "280616/091" # rodné číslo without slash
K_DATU = date.today().isoformat() # YYYY-MM-DD
ODBORNOSTI = ["001"] # VPL (adult GP)
# MySQL
if socket.gethostname().strip() in ("NTBVBHP470G10","Z230"):
MYSQL_CFG = dict(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
cursorclass=DictCursor,
autocommit=True, # or False if you prefer manual commit
)
elif socket.gethostname().strip() == "SESTRA":
MYSQL_CFG = dict(
host="127.0.0.1",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
cursorclass=DictCursor,
autocommit=True, # or False if you prefer manual commit
)
# Namespaces (from your response/WSDL)
NS = {
"soap": "http://schemas.xmlsoap.org/soap/envelope/",
"rp": "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1",
}
# ------------------- HELPERS -------------------
def normalize_rc(rc: str) -> str:
return rc.replace("/", "").strip()
def build_envelope(rc: str, k_datu: str, odb_list: list[str]) -> str:
odb_xml = "".join([f"<ns1:kodOdbornosti>{kod}</ns1:kodOdbornosti>" for kod in odb_list])
inner = f"""
<ns1:registracePojistencePZSB2B xmlns:ns1="{NS['rp']}">
<ns1:cisloPojistence>{rc}</ns1:cisloPojistence>
<ns1:kDatu>{k_datu}</ns1:kDatu>
<ns1:seznamOdbornosti>
{odb_xml}
</ns1:seznamOdbornosti>
</ns1:registracePojistencePZSB2B>""".strip()
return f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="{NS['soap']}">
<soap:Body>
{inner}
</soap:Body>
</soap:Envelope>"""
def parse_registrace(xml_text: str):
"""
Return (rows, stav_vyrizeni) where rows is a list of dicts
for each <odbornost> item in the response.
"""
root = ET.fromstring(xml_text)
items = root.findall(".//rp:seznamOdbornosti/rp:odbornost", NS)
out = []
for it in items:
def g(tag, ctx=it):
el = ctx.find(f"rp:{tag}", NS)
return el.text.strip() if el is not None and el.text else None
poj = it.find("rp:zdravotniPojistovna", NS)
poj_kod = poj.find("rp:kod", NS).text.strip() if (poj is not None and poj.find("rp:kod", NS) is not None) else None
poj_zkr = poj.find("rp:zkratka", NS).text.strip() if (poj is not None and poj.find("rp:zkratka", NS) is not None) else None
spec = it.find("rp:odbornost", NS)
odb_kod = spec.find("rp:kod", NS).text.strip() if (spec is not None and spec.find("rp:kod", NS) is not None) else None
odb_naz = spec.find("rp:nazev", NS).text.strip() if (spec is not None and spec.find("rp:nazev", NS) is not None) else None
out.append(dict(
icz=g("ICZ"),
icp=g("ICP"),
nazev_icp=g("nazevICP"),
nazev_szz=g("nazevSZZ"),
poj_kod=poj_kod,
poj_zkratka=poj_zkr,
odbornost_kod=odb_kod,
odbornost_nazev=odb_naz,
datum_registrace=g("datumRegistrace"),
datum_zahajeni=g("datumZahajeni"),
datum_ukonceni=g("datumUkonceni"),
))
st = root.find(".//rp:stavVyrizeniPozadavku", NS)
stav_vyrizeni = st.text.strip() if (st is not None and st.text) else None
return out, stav_vyrizeni
def upsert_rows(
rc: str,
query_date: str,
rows: list[dict],
stav_vyrizeni: str,
xml_text: str,
requested_odb_list: list[str] | None = None,
) -> int:
"""
Insert/update medevio.vzp_registrace.
If no <odbornost> items are returned, insert placeholder row(s)
for each requested specialty (e.g., "001") so the negative result is visible.
"""
sql = """
INSERT INTO vzp_registrace
(rc, query_date, odbornost_kod, odbornost_nazev,
icz, icp, nazev_icp, nazev_szz,
poj_kod, poj_zkratka,
datum_registrace, datum_zahajeni, datum_ukonceni,
stav_vyrizeni, response_xml)
VALUES
(%(rc)s, %(query_date)s, %(odbornost_kod)s, %(odbornost_nazev)s,
%(icz)s, %(icp)s, %(nazev_icp)s, %(nazev_szz)s,
%(poj_kod)s, %(poj_zkratka)s,
%(datum_registrace)s, %(datum_zahajeni)s, %(datum_ukonceni)s,
%(stav_vyrizeni)s, %(response_xml)s)
ON DUPLICATE KEY UPDATE
odbornost_nazev = VALUES(odbornost_nazev),
nazev_icp = VALUES(nazev_icp),
nazev_szz = VALUES(nazev_szz),
poj_kod = VALUES(poj_kod),
poj_zkratka = VALUES(poj_zkratka),
datum_registrace= VALUES(datum_registrace),
datum_zahajeni = VALUES(datum_zahajeni),
datum_ukonceni = VALUES(datum_ukonceni),
stav_vyrizeni = VALUES(stav_vyrizeni),
response_xml = VALUES(response_xml),
updated_at = CURRENT_TIMESTAMP
"""
rc_norm = normalize_rc(rc)
qd = query_date or date.today().isoformat()
payloads: list[dict] = []
if rows:
# Positive path: save what came from the API
for r in rows:
payloads.append({
"rc": rc_norm,
"query_date": qd,
**r,
"stav_vyrizeni": stav_vyrizeni,
"response_xml": xml_text,
})
else:
# Negative path: no registration items -> create placeholders
if not requested_odb_list:
requested_odb_list = ["001"]
for kod in requested_odb_list:
payloads.append({
"rc": rc_norm,
"query_date": qd,
"odbornost_kod": kod,
"odbornost_nazev": None,
"icz": None,
"icp": None,
"nazev_icp": None,
"nazev_szz": None,
"poj_kod": None,
"poj_zkratka": None,
"datum_registrace": None,
"datum_zahajeni": None,
"datum_ukonceni": None,
# Keep what VZP said (e.g., 'X'), and raw XML for audit
"stav_vyrizeni": stav_vyrizeni,
"response_xml": xml_text,
})
# Print what we're going to save
print("\n=== Will save the following payload(s) to medevio.vzp_registrace ===")
for p in payloads:
pprint(p)
if not payloads:
print("No payloads prepared (this should not happen).")
return 0
connsql = get_mysql_connection()
try:
with connsql.cursor() as cur:
cur.executemany(sql, payloads)
connsql.commit()
finally:
connsql.close()
print(f"\nUpserted rows: {len(payloads)}")
return len(payloads)
def prepare_processed_rcs():
consql=get_mysql_connection()
cursql=consql.cursor()
sql="""
WITH ranked AS (
SELECT
vreg.*,
ROW_NUMBER() OVER (
PARTITION BY rc
ORDER BY query_date DESC
) AS rn
FROM vzp_registrace AS vreg
)
SELECT rc
FROM ranked
WHERE rn = 1
"""
cursql.execute(sql)
rows=cursql.fetchall()
print(f"Pocet jiz zpracovanych rodnych cisel v MYSQL MEDEVIO je {len(rows)}")
rc_set_vzp = {row["rc"] for row in rows}
return (rc_set_vzp)
# ------------------- MAIN FLOW -------------------
def main():
con = get_medicus_connection()
cur = con.cursor()
cur.execute("select rodcis, prijmeni, jmeno from kar where rodcis starting with '1'")
# cur.execute("select first 2 rodcis, prijmeni, jmeno from kar where rodcis starting with '0'")
# Vytvor seznam rodnych cisel, která už máme
rc_set_vzp = prepare_processed_rcs()
rows = cur.fetchall()
print(f"Pocet vybranych radku z tabulky KAR je: {len(rows)}")
for row in rows:
if row[0] in rc_set_vzp:
continue
else:
print(row[0], row[1], row[2])
K_DATU = date.today().isoformat() # YYYY-MM-DD
ODBORNOSTI = ["001"]
RC=row[0]
# Build SOAP envelope
envelope = build_envelope(RC, K_DATU, ODBORNOSTI)
# mTLS session
session = requests.Session()
session.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_PATH, pkcs12_password=PFX_PASS))
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "process", # Oracle composite usually expects this
}
print(envelope)
# Call service
resp = session.post(ENDPOINT, data=envelope.encode("utf-8"),
headers=headers, timeout=30, verify=VERIFY)
print("HTTP:", resp.status_code)
# (Optional) Uncomment to see raw XML
print(resp.text)
# Parse and save
rows, stav = parse_registrace(resp.text)
print(rows,stav)
upsert_rows(RC, K_DATU, rows, stav, resp.text, requested_odb_list=ODBORNOSTI)
time.sleep(random.uniform(1, 5))
if __name__ == "__main__":
main()

View File

@@ -1,46 +0,0 @@
from functions import get_medicus_connection
from functions import get_mysql_connection
from functions import check_insurance
import time, random
def prepare_processed_rcs():
consql=get_mysql_connection()
cursql=consql.cursor()
sql="""
WITH ranked AS (
SELECT
vr.*,
ROW_NUMBER() OVER (
PARTITION BY rc
ORDER BY k_datu DESC, queried_at DESC
) AS rn
FROM vzp_stav_pojisteni AS vr
)
SELECT rc
FROM ranked
WHERE rn = 1
"""
cursql.execute(sql)
rows=cursql.fetchall()
print(f"Pocet jiz zpracovanych rodnych cisel v MYSQL MEDEVIO je {len(rows)}")
rc_set_vzp = {row["rc"] for row in rows}
return (rc_set_vzp)
con=get_medicus_connection()
cur=con.cursor()
cur.execute("select rodcis, prijmeni, jmeno from kar where rodcis starting with '3'")
rc_set_vzp=prepare_processed_rcs()
rows=cur.fetchall()
print(f"Pocet vybranych radku z tabulky KAR je: {len(rows)}")
for row in rows:
if row[0] in rc_set_vzp:
continue
else:
print(row[0], row[1],row[2])
print(check_insurance(row[0]))
time.sleep(random.uniform(1, 5))

View File

@@ -1,7 +0,0 @@
from functions import check_insurance
if __name__ == "__main__":
rc = "7309208104"
res = check_insurance(rc)
print("=== RESULT ===")
print(res)

View File

@@ -1,4 +0,0 @@
import socket
computer_name = socket.gethostname()
print(computer_name)

Binary file not shown.

View File

@@ -1,67 +0,0 @@
import pandas as pd
import fdb
import pymysql
# ---------------------------------
# FIREBIRD CONNECTION
# ---------------------------------
fb = fdb.connect(
host="192.168.1.4",
database=r"z:\Medicus 3\data\MEDICUS.FDB",
user="SYSDBA",
password="masterkey",
charset="WIN1250"
)
cur = fb.cursor()
sql_fb = """
SELECT kar.rodcis
FROM registr
JOIN kar ON registr.idpac = kar.idpac
WHERE registr.datum_zruseni IS NULL
AND registr.priznak IN ('A','D','V')
"""
cur.execute(sql_fb)
rows_fb = cur.fetchall()
df_fb = pd.DataFrame(rows_fb, columns=["rc"])
print("FB count:", len(df_fb))
# ---------------------------------
# MYSQL CONNECTION
# ---------------------------------
mysql = pymysql.connect(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
charset="utf8mb4"
)
sql_mysql = """
SELECT rc
FROM vzp_stav_pojisteni AS v
WHERE v.k_datu = CURDATE()
AND v.id = (
SELECT MAX(id)
FROM vzp_stav_pojisteni
WHERE rc = v.rc
AND k_datu = CURDATE()
);
"""
df_mysql = pd.read_sql(sql_mysql, mysql)
print("MySQL count:", len(df_mysql))
# ---------------------------------
# FIND MISSING RC
# ---------------------------------
df_missing = df_fb[~df_fb["rc"].isin(df_mysql["rc"])]
print("\nMissing patients:")
print(df_missing)
fb.close()
mysql.close()

View File

@@ -1,42 +0,0 @@
import fdb
class MedicusDB:
def __init__(self, host, db_path, user="SYSDBA", password="masterkey", charset="WIN1250"):
self.conn = fdb.connect(
host=host,
database=db_path,
user=user,
password=password,
charset=charset
)
self.cur = self.conn.cursor()
def query(self, sql, params=None):
self.cur.execute(sql, params or ())
return self.cur.fetchall()
def query_dict(self, sql, params=None):
self.cur.execute(sql, params or ())
cols = [d[0].strip().lower() for d in self.cur.description]
return [dict(zip(cols, row)) for row in self.cur.fetchall()]
def get_active_registered_patients(self):
sql = """
SELECT
kar.rodcis,
kar.prijmeni,
kar.jmeno,
kar.poj
FROM registr
JOIN kar ON registr.idpac = kar.idpac
WHERE registr.datum_zruseni IS NULL
AND registr.priznak IN ('A','D','V')
AND kar.rodcis IS NOT NULL
AND kar.rodcis <> ''
"""
return self.query(sql) # or self.query_dict(sql)
def close(self):
self.conn.close()

View File

@@ -1,79 +0,0 @@
import pandas as pd
import pymysql
from medicus_db import MedicusDB
import fdb
# FULL OUTPUT SETTINGS
pd.set_option("display.max_rows", None)
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 0)
pd.set_option("display.max_colwidth", None)
# ===========
#
# ===========================
# FIREBIRD → načtení registrovaných pacientů
# ======================================
db = MedicusDB("192.168.1.4", r"z:\Medicus 3\data\MEDICUS.FDB")
rows_fb = db.get_active_registered_patients() # vrací rc, prijmeni, jmeno, poj
db.close()
df_fb = pd.DataFrame(rows_fb, columns=["rc", "prijmeni", "jmeno", "poj_medicus"])
df_fb["poj_medicus"] = df_fb["poj_medicus"].astype(str).str.strip()
print("FB count:", len(df_fb))
# ======================================
# MYSQL → načtení dnešních výsledků
# ======================================
mysql = pymysql.connect(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
charset="utf8mb4"
)
sql_mysql = """
SELECT rc,
kod_pojistovny AS poj_mysql,
nazev_pojistovny,
stav,
stav_vyrizeni
FROM vzp_stav_pojisteni AS v
WHERE v.k_datu = CURDATE()
AND v.id = (
SELECT MAX(id)
FROM vzp_stav_pojisteni
WHERE rc = v.rc
AND k_datu = CURDATE()
);
"""
df_mysql = pd.read_sql(sql_mysql, mysql)
df_mysql["poj_mysql"] = df_mysql["poj_mysql"].astype(str).str.strip()
print("MySQL count:", len(df_mysql))
# ======================================
# LEFT JOIN: Medicus ↔ MySQL podle RC
# ======================================
df_merge = df_fb.merge(df_mysql, on="rc", how="left")
# ======================================
# Najít rozdíly pojišťovny
# ======================================
df_diff = df_merge[df_merge["poj_medicus"] != df_merge["poj_mysql"]]
print("\nPacienti s rozdílnou pojišťovnou:")
print(df_diff[["rc", "prijmeni", "jmeno", "poj_medicus", "poj_mysql", "nazev_pojistovny"]])
# Pokud chceš uložit do Excelu:
# df_diff.to_excel("rozdil_pojistoven.xlsx", index=False)

View File

@@ -1,137 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
import logging
from medicus_db import MedicusDB
from vzpb2b_client import VZPB2BClient
import pymysql
from datetime import date
# ==========================================
# LOGGING SETUP
# ==========================================
logging.basicConfig(
filename="insurance_check.log",
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
encoding="utf-8"
)
console = logging.getLogger("console")
console.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(message)s"))
console.addHandler(handler)
def log_info(msg):
logging.info(msg)
console.info(msg)
def log_error(msg):
logging.error(msg)
console.error(msg)
# ==========================================
# MYSQL CONNECTION
# ==========================================
mysql = pymysql.connect(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
charset="utf8mb4",
autocommit=True
)
def save_insurance_status(mysql_conn, rc, k_datu, result, xml_text):
sql = """
INSERT INTO vzp_stav_pojisteni
(rc, k_datu, stav, kod_pojistovny, nazev_pojistovny,
pojisteni_kod, stav_vyrizeni, response_xml)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
"""
try:
with mysql_conn.cursor() as cur:
cur.execute(sql, (
rc,
k_datu,
result["stav"],
result["kodPojistovny"],
result["nazevPojistovny"],
result["pojisteniKod"],
result["stavVyrizeni"],
xml_text
))
except Exception as e:
log_error(f"❌ MYSQL ERROR for RC {rc}: {e}")
log_error(f"---- RAW XML ----\n{xml_text}\n-----------------")
raise
# ==========================================
# CONFIGURATION
# ==========================================
HOST = "192.168.1.4"
DB_PATH = r"z:\Medicus 3\data\MEDICUS.FDB"
PFX_PATH = r"MBcert.pfx"
PFX_PASSWORD = "Vlado7309208104++"
ENV = "prod"
ICZ = "00000000"
DIC = "00000000"
# ==========================================
# INIT CONNECTIONS
# ==========================================
db = MedicusDB(HOST, DB_PATH)
vzp = VZPB2BClient(ENV, PFX_PATH, PFX_PASSWORD, icz=ICZ, dic=DIC)
# ==========================================
# FETCH REGISTERED PATIENTS
# ==========================================
patients = db.get_active_registered_patients()
log_info(f"Checking {len(patients)} registered patients...\n")
k_datu = date.today().isoformat()
# ==========================================
# LOOP ONE PATIENT PER SECOND
# ==========================================
for rodcis, prijmeni, jmeno, poj in patients:
log_info(f"=== Checking {prijmeni} {jmeno} ({rodcis}) ===")
xml = vzp.stav_pojisteni(rc=rodcis, k_datu=k_datu)
# 1) Check if response looks like XML
if not xml.strip().startswith("<"):
log_error(f"❌ INVALID XML for RC {rodcis}")
log_error(f"---- RAW RESPONSE ----\n{xml}\n----------------------")
time.sleep(2)
continue
# 2) Try parsing XML
try:
result = vzp.parse_stav_pojisteni(xml)
except Exception as e:
log_error(f"❌ XML PARSE ERROR for RC {rodcis}: {e}")
log_error(f"---- RAW RESPONSE ----\n{xml}\n----------------------")
time.sleep(2)
continue
log_info(f"Result: {result}")
# 3) Save into MySQL (with logging)
try:
save_insurance_status(mysql, rodcis, k_datu, result, xml)
except Exception:
log_error(f"❌ FAILURE inserting to MySQL for {rodcis}")
continue
time.sleep(2)
db.close()
log_info("\nDONE.")

View File

@@ -1,19 +0,0 @@
cur.execute("select rodcis,prijmeni,jmeno from kar where datum_zruseni is null and kar.vyrazen!='A' and kar.rodcis is not null and idicp!=0 order by ockzaz.datum desc")
from vzpb2b_client import VZPB2BClient
client = VZPB2BClient(
env="production",
pfx_path="mbcert.pfx",
pfx_password="Vlado7309208104++",
icz="00000000",
dic="00000000"
)
response = client.stav_pojisteni("0308020152")
# print(response)
print(client.parse_stav_pojisteni(response))

View File

@@ -1,12 +0,0 @@
from vzpb2b_client import VZPB2BClient
client = VZPB2BClient(
env="simu", # or "prod"
pfx_path="mbcert.pfx",
pfx_password="Vlado7309208104++",
icz="00000000",
dic="00000000"
)
result_xml = client.over_prukaz_pojistence("80203111194350000001")
print(result_xml)

View File

@@ -1,213 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from requests_pkcs12 import Pkcs12Adapter
import requests
import uuid
from datetime import date
class VZPB2BClient:
def __init__(self, env: str, pfx_path: str, pfx_password: str,
icz: str = "00000000", dic: str = "00000000"):
# Normalize environment name
env = env.lower().strip()
if env in ("prod", "production", "live", "real"):
self.env = "prod"
elif env in ("simu", "simulace", "test", "testing"):
self.env = "simu"
else:
raise ValueError(f"Unknown environment '{env}'. Use 'simu' or 'prod'.")
self.pfx_path = pfx_path
self.pfx_password = pfx_password
self.icz = icz
self.dic = dic
# Prepare mTLS session
session = requests.Session()
session.mount(
"https://",
Pkcs12Adapter(pkcs12_filename=pfx_path, pkcs12_password=pfx_password)
)
self.session = session
# --------------------------------------------------------------
# URL BUILDER
# --------------------------------------------------------------
def _build_endpoint(self, service_name: str) -> str:
"""
SIMU:
https://simu.b2b.vzp.cz/B2BProxy/HttpProxy/SIMU<Service>?sluzba=SIMU<Service>
PROD:
https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/<Service>
"""
if self.env == "simu":
simu_service = f"SIMU{service_name}"
return (
f"https://simu.b2b.vzp.cz/B2BProxy/HttpProxy/"
f"{simu_service}?sluzba={simu_service}"
)
# Production
return (
f"https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/{service_name}"
)
# --------------------------------------------------------------
# SOAP HEADER BUILDER
# --------------------------------------------------------------
def _header(self) -> str:
idZpravy = uuid.uuid4().hex[:12] # must be alphanumeric, max 12 chars
return f"""
<com:idZpravy>{idZpravy}</com:idZpravy>
<com:idSubjektu>
<com:icz>{self.icz}</com:icz>
<com:dic>{self.dic}</com:dic>
</com:idSubjektu>
"""
# --------------------------------------------------------------
# OVERPRUKAZ — EHIC CHECK
# --------------------------------------------------------------
def over_prukaz_pojistence(self, cislo_prukazu: str, k_datu: str = None) -> str:
"""
Calls OverPrukazPojistenceB2B (SIMU or PROD depending on env).
Returns raw XML string.
"""
service = "OverPrukazPojistenceB2B"
endpoint = self._build_endpoint(service)
if not k_datu:
k_datu = date.today().isoformat()
soap = f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:vzp="http://xmlns.gemsystem.cz/OverPrukazPojistenceB2B"
xmlns:com="http://xmlns.gemsystem.cz/CommonB2B">
<soap:Header>
{self._header()}
</soap:Header>
<soap:Body>
<vzp:OverPrukazPojistenceB2BPozadavek>
<vzp:cisloPrukazu>{cislo_prukazu}</vzp:cisloPrukazu>
<vzp:kDatu>{k_datu}</vzp:kDatu>
</vzp:OverPrukazPojistenceB2BPozadavek>
</soap:Body>
</soap:Envelope>
"""
headers = {"Content-Type": "text/xml; charset=utf-8"}
print(f"Calling: {endpoint}")
response = self.session.post(
endpoint,
data=soap.encode("utf-8"),
headers=headers,
timeout=30
)
print("HTTP:", response.status_code)
return response.text
def stav_pojisteni(self, rc: str, k_datu: str = None, prijmeni: str = None):
"""
Calls stavPojisteniB2B (SIMU or PROD).
"""
service = "stavPojisteniB2B"
endpoint = self._build_endpoint(service)
if not k_datu:
k_datu = date.today().isoformat()
prijmeni_xml = f"<vzp:prijmeni>{prijmeni}</vzp:prijmeni>" if prijmeni else ""
soap = f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:vzp="http://xmlns.gemsystem.cz/stavPojisteniB2B"
xmlns:com="http://xmlns.gemsystem.cz/CommonB2B">
<soap:Header>
{self._header()}
</soap:Header>
<soap:Body>
<vzp:stavPojisteniB2B>
<vzp:cisloPojistence>{rc}</vzp:cisloPojistence>
{prijmeni_xml}
<vzp:kDatu>{k_datu}</vzp:kDatu>
</vzp:stavPojisteniB2B>
</soap:Body>
</soap:Envelope>
"""
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "process"
}
print(f"Calling: {endpoint}")
resp = self.session.post(endpoint, data=soap.encode("utf-8"),
headers=headers, timeout=30)
print("HTTP:", resp.status_code)
return resp.text
def parse_stav_pojisteni(self, xml_text: str):
"""
Parses stavPojisteniB2B SOAP response into a Python dict.
Returned structure:
{
"stavVyrizeni": int,
"stav": str | None,
"kodPojistovny": str | None,
"nazevPojistovny": str | None,
"pojisteniKod": str | None
}
"""
import xml.etree.ElementTree as ET
NS = {
"soap": "http://schemas.xmlsoap.org/soap/envelope/",
"vzp": "http://xmlns.gemsystem.cz/stavPojisteniB2B"
}
root = ET.fromstring(xml_text)
# ---- Extract status ----
stav_vyr = root.find(".//vzp:stavVyrizeniPozadavku", NS)
stav_vyr = int(stav_vyr.text.strip()) if stav_vyr is not None else None
# ---- If no stavPojisteni element present (e.g. 0 or some errors) ----
node_stav = root.find(".//vzp:stavPojisteni", NS)
if node_stav is None:
return {
"stavVyrizeni": stav_vyr,
"stav": None,
"kodPojistovny": None,
"nazevPojistovny": None,
"pojisteniKod": None,
}
def get(tag):
el = node_stav.find(f"vzp:{tag}", NS)
return el.text.strip() if el is not None and el.text else None
return {
"stavVyrizeni": stav_vyr,
"stav": get("stav"),
"kodPojistovny": get("kodPojistovny"),
"nazevPojistovny": get("nazevPojistovny"),
"pojisteniKod": get("pojisteniKod"),
}

123
10Library.py Normal file
View File

@@ -0,0 +1,123 @@
import os
import psycopg2
from psycopg2 import extras
from tqdm import tqdm
import time
import sys
# --- KONFIGURACE ---
DB_CONFIG = {
"host": "192.168.1.76", # Doplňte IP adresu svého Unraidu/Postgresu
"database": "files",
"user": "vladimir.buzalka",
"password": "Vlado7309208104++",
"port": "5432"
}
DIRECTORY_TO_SCAN = "//tower/Library"
BATCH_SIZE = 2000 # Zvýšeno na 2000 pro ještě lepší efektivitu u 5M souborů
# --------- ----------
def scan_to_postgres():
conn = None
total_count = 0
files_batch = []
try:
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
# Inicializace tabulky
cur.execute("""
CREATE TABLE IF NOT EXISTS library_files
(
id
SERIAL
PRIMARY
KEY,
file_path
TEXT
NOT
NULL,
file_name
TEXT
NOT
NULL,
file_size_bytes
BIGINT,
indexed_at
TIMESTAMP
DEFAULT
CURRENT_TIMESTAMP
);
""")
conn.commit()
print(f"🚀 Zahajuji indexaci: {DIRECTORY_TO_SCAN}")
# Progress bar s automatickým škálováním jednotek (k, M)
pbar = tqdm(
unit=" soubor",
unit_scale=True,
unit_divisor=1000,
desc="Probíhá skenování",
dynamic_ncols=True
)
def save_batch(batch_data):
"""Pomocná funkce pro zápis do DB"""
insert_query = "INSERT INTO library_files (file_path, file_name, file_size_bytes) VALUES %s"
psycopg2.extras.execute_values(cur, insert_query, batch_data)
conn.commit()
# Rychlé procházení pomocí os.scandir
for root, dirs, files in os.walk(DIRECTORY_TO_SCAN):
for name in files:
full_path = os.path.join(root, name)
try:
# Získání metadat (velikost)
file_size = os.path.getsize(full_path)
files_batch.append((full_path, name, file_size))
total_count += 1
if len(files_batch) >= BATCH_SIZE:
save_batch(files_batch)
pbar.update(len(files_batch))
files_batch = []
except (OSError, PermissionError):
continue
# Uložení posledního neúplného zbytku
if files_batch:
save_batch(files_batch)
pbar.update(len(files_batch))
pbar.close()
print(f"\n✅ Hotovo! Celkem zaindexováno {total_count} souborů.")
except KeyboardInterrupt:
print("\n\n⚠️ Skenování přerušeno uživatelem. Ukládám rozpracovaná data...")
if files_batch:
try:
save_batch(files_batch)
print(f"Posledních {len(files_batch)} záznamů uloženo.")
except:
print("Nepodařilo se uložit poslední dávku.")
sys.exit(0)
except Exception as e:
print(f"\n❌ Chyba: {e}")
finally:
if conn:
cur.close()
conn.close()
if __name__ == "__main__":
start_time = time.time()
scan_to_postgres()
duration = time.time() - start_time
print(
f"⏱️ Celkový čas: {duration / 60:.2f} minut (rychlost: {int(5000000 / duration if duration > 0 else 0)} souborů/s)")

View File

@@ -0,0 +1,390 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
import re
import urllib.parse as urlparse
from pathlib import Path
import json
import requests
# ============================================================
# 1) MySQL CONNECTION
# ============================================================
db = pymysql.connect(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="torrents",
charset="utf8mb4",
autocommit=True
)
cursor = db.cursor()
# ============================================================
# 2) Selenium setup
# ============================================================
COOKIE_FILE = Path("sktorrent_cookies.json")
# Start URL pro kategorii 24, seřazeno podle data DESC
START_URL = (
"https://sktorrent.eu/torrent/torrents.php"
"?search=&category=24&zaner=&jazyk=&active=0"
)
chrome_options = Options()
chrome_options.add_argument("--start-maximized")
chrome_options.add_argument("--disable-notifications")
chrome_options.add_argument("--disable-popup-blocking")
chrome_options.add_argument("--disable-extensions")
driver = webdriver.Chrome(options=chrome_options)
# Pozice a velikost okna (aby nepřekrývalo PyCharm)
driver.set_window_position(380, 50) # 10 cm od levého okraje
driver.set_window_size(1350, 1000) # můžeš změnit dle monitoru
# Nejprve otevřeme hlavní stránku kvůli doméně pro cookies
driver.get("https://sktorrent.eu")
# Load cookies z JSON
if COOKIE_FILE.exists():
with open(COOKIE_FILE, "r") as f:
cookies = json.load(f)
for c in cookies:
driver.add_cookie(c)
print("🍪 Cookies loaded.")
else:
print("⚠️ Cookie file not found, you may not be logged in!")
# ============================================================
# 3) Převod cookies → requests.Session (pro stahování .torrent)
# ============================================================
requests_session = requests.Session()
for ck in driver.get_cookies():
requests_session.cookies.set(ck["name"], ck["value"])
print("🔗 Requests session initialized with Selenium cookies.")
# ============================================================
# 4) Funkce pro zavření popupu
# ============================================================
def close_popup_if_any():
"""Zkusí zavřít interstitial reklamu pomocí JS funkce interstitialBox.closeit()."""
try:
driver.execute_script("try { interstitialBox.closeit(); } catch(e) {}")
# Krátká pauza, ať se DOM uklidní
time.sleep(0.5)
print("🧹 Popup closed via JS fallback (if present).")
except Exception as e:
print(" Popup JS handler not found:", e)
# ============================================================
# 5) Funkce pro parsování jednoho řádku (jednoho torrentu)
# ============================================================
def parse_row(cells):
"""
cells: list<WebElement> o délce 7
Struktura:
0: kategorie
1: download link (.torrent)
2: název + velikost + datum + 'Obrázok' + žánr
3: -- (ignorujeme)
4: seeders
5: leechers
6: completed
"""
# --------------------------
# 1⃣ CATEGORY
# --------------------------
category = cells[0].text.strip()
# --------------------------
# 2⃣ DOWNLOAD LINK FOR TORRENT FILE (cells[1])
# --------------------------
try:
download_a = cells[1].find_element(By.TAG_NAME, "a")
download_link = download_a.get_attribute("href")
except:
print("⚠️ No download link in row, skipping.")
return None
parsed_dl = urlparse.urlparse(download_link)
dl_query = urlparse.parse_qs(parsed_dl.query)
torrent_filename = dl_query.get("f", ["unknown.torrent"])[0]
# --------------------------
# 3⃣ TITLE + DETAILS LINK (in cell[2])
# --------------------------
title_links = cells[2].find_elements(By.TAG_NAME, "a")
if not title_links:
print("⚠️ No title link — skipping row")
return None
a_tag = title_links[0]
visible_name = a_tag.text.strip()
full_title = a_tag.get_attribute("title")
details_link = a_tag.get_attribute("href")
if not details_link:
print("⚠️ Row has no details link — skipping")
return None
# --------------------------
# Extract torrent hash from ?id=
# --------------------------
parsed = urlparse.urlparse(details_link)
query = urlparse.parse_qs(parsed.query)
if "id" not in query:
print("⚠️ Skipping row with no torrent ID →", details_link)
return None
torrent_hash = query["id"][0]
# --------------------------
# 4⃣ Size + date parsing
# --------------------------
text_block = cells[2].get_attribute("innerText")
text_block_clean = " ".join(text_block.split())
size_match = re.search(r"Velkost ([0-9\.]+ ?[KMG]B)", text_block_clean, re.IGNORECASE)
added_match = re.search(r"Pridany (.+?)(?:\sObrázok|$)", text_block_clean, re.IGNORECASE)
size_pretty = size_match.group(1) if size_match else None
added_pretty = added_match.group(1) if added_match else None
# Robustní převod data/času do MySQL datetime
added_mysql = None
if added_pretty:
# "29/11/2025 o 02:29" → "29/11/2025 02:29"
clean = added_pretty.replace(" o ", " ").strip()
parts = clean.split(" ")
date_part = parts[0]
time_part = parts[1] if len(parts) > 1 else "00:00:00"
# pokud chybí sekundy, přidej
if len(time_part.split(":")) == 2:
time_part += ":00"
day, month, year = date_part.split("/")
added_mysql = f"{year}-{month}-{day} {time_part}"
# --------------------------
# 5⃣ Image preview
# --------------------------
img_link = None
try:
image_a = cells[2].find_element(
By.XPATH,
".//a[contains(text(),'Obrázok')]"
)
mouseover = image_a.get_attribute("onmouseover")
img_match = re.search(r"src=([^ ]+)", mouseover)
if img_match:
img_link = img_match.group(1).replace("'", "").strip()
if img_link.startswith("//"):
img_link = "https:" + img_link
except:
pass
# --------------------------
# 6⃣ SEEDERS / LEECHERS
# --------------------------
seeders_a = cells[4].find_element(By.TAG_NAME, "a")
seeders_number = int(seeders_a.text.strip())
seeders_link = seeders_a.get_attribute("href")
leechers_a = cells[5].find_element(By.TAG_NAME, "a")
leechers_number = int(leechers_a.text.strip())
leechers_link = leechers_a.get_attribute("href")
# --------------------------
# 7⃣ Check, zda už máme torrent_content v DB
# --------------------------
cursor.execute(
"SELECT torrent_content FROM torrents WHERE torrent_hash=%s",
(torrent_hash,)
)
row = cursor.fetchone()
already_have_torrent = row is not None and row[0] is not None
# --------------------------
# 8⃣ DOWNLOAD TORRENT CONTENT (.torrent) only if needed
# --------------------------
torrent_content = None
if already_have_torrent:
print(f" ↪️ Torrent file already stored, skipping download ({torrent_filename})")
else:
time.sleep(3) # mezera mezi torrenty
try:
resp = requests_session.get(download_link)
resp.raise_for_status()
torrent_content = resp.content
except Exception as e:
print(f"⚠️ Could not download torrent file for {torrent_hash}: {e}")
torrent_content = None
# --------------------------
# FINAL DICTIONARY
# --------------------------
return {
"torrent_hash": torrent_hash,
"details_link": details_link,
"category": category,
"title_visible": visible_name,
"title_full": full_title,
"size_pretty": size_pretty,
"added_datetime": added_mysql,
"preview_image": img_link,
"seeders": seeders_number,
"seeders_link": seeders_link,
"leechers": leechers_number,
"leechers_link": leechers_link,
"torrent_filename": torrent_filename,
# pokud jsme torrent už měli, vracíme None → UPDATE ho nepřepíše (COALESCE)
"torrent_content": torrent_content if not already_have_torrent else None,
}
# ============================================================
# 6) MySQL INSERT
# ============================================================
insert_sql = """
INSERT INTO torrents (
torrent_hash, details_link, category, title_visible, title_full,
size_pretty, added_datetime, preview_image,
seeders, seeders_link, leechers, leechers_link,
torrent_filename, torrent_content
) VALUES (
%(torrent_hash)s, %(details_link)s, %(category)s, %(title_visible)s, %(title_full)s,
%(size_pretty)s, %(added_datetime)s, %(preview_image)s,
%(seeders)s, %(seeders_link)s, %(leechers)s, %(leechers_link)s,
%(torrent_filename)s, %(torrent_content)s
)
ON DUPLICATE KEY UPDATE
details_link = VALUES(details_link),
category = VALUES(category),
title_visible = VALUES(title_visible),
title_full = VALUES(title_full),
size_pretty = VALUES(size_pretty),
added_datetime = VALUES(added_datetime),
preview_image = VALUES(preview_image),
seeders = VALUES(seeders),
seeders_link = VALUES(seeders_link),
leechers = VALUES(leechers),
leechers_link = VALUES(leechers_link),
torrent_filename = VALUES(torrent_filename),
torrent_content = COALESCE(VALUES(torrent_content), torrent_content);
"""
# ============================================================
# 7) Funkce pro zpracování jedné stránky
# ============================================================
def process_current_page(page_index: int):
"""
Zpracuje aktuálně otevřenou stránku:
- najde všechny "REAL TORRENT ROWS" (7 td)
- pro každý torrent:
* parse_row
* insert/update do DB
"""
rows = driver.find_elements(By.CSS_SELECTOR, "table tr")
real_rows = []
for row in rows:
cells = row.find_elements(By.TAG_NAME, "td")
# REAL TORRENT ROWS ALWAYS HAVE EXACTLY 7 TD CELLS
if len(cells) == 7:
real_rows.append(cells)
print(f"📄 Page {page_index}: {len(real_rows)} torrent rows")
for cells in real_rows:
data = parse_row(cells)
if not data:
continue
print(f" 💾 [{page_index}] Saving:", data["title_visible"])
cursor.execute(insert_sql, data)
# ============================================================
# 8) Hlavní stránkovací cyklus
# ============================================================
current_url = START_URL
page_index = 0
while True:
print(f"\n🌐 Loading page {page_index}: {current_url}")
driver.get(current_url)
time.sleep(2)
# zavři popup, pokud je
close_popup_if_any()
# zpracuj aktuální stránku
process_current_page(page_index)
# pokus se najít tlačítko "Dalsi >>"
try:
next_btn = driver.find_element(
By.XPATH,
"//a[b[contains(text(),'Dalsi')]]"
)
next_url = next_btn.get_attribute("href")
if not next_url:
print("⛔ Next link has no href, stopping.")
break
# pokud je relativní, doplň doménu
if next_url.startswith("/"):
next_url = "https://sktorrent.eu" + next_url
# když by náhodou bylo stejné URL → přeruš nekonečnou smyčku
if next_url == current_url:
print("⛔ Next URL equals current URL, stopping.")
break
print("➡️ Next page:", next_url)
current_url = next_url
page_index += 1
# malá pauza mezi stránkami
time.sleep(1)
except Exception:
print("✅ No 'Dalsi >>' link found, reached last page. Done.")
break
print("\n🎉 DONE — All pages processed, torrents saved & torrent files downloaded (without re-downloading existing ones).")
driver.quit()

View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
import re
import urllib.parse as urlparse
from pathlib import Path
import json
# ============================================================
# 1) MySQL CONNECTION
# ============================================================
db = pymysql.connect(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="torrents",
charset="utf8mb4",
autocommit=True
)
cursor = db.cursor()
# ============================================================
# 2) Selenium setup
# ============================================================
COOKIE_FILE = Path("sktorrent_cookies.json")
URL = "https://sktorrent.eu/torrent/torrents.php?active=0&category=24&order=data&by=DESC&zaner=&jazyk=&page=0"
chrome_options = Options()
chrome_options.add_argument("--start-maximized")
chrome_options.add_argument("--disable-notifications")
chrome_options.add_argument("--disable-popup-blocking")
chrome_options.add_argument("--disable-extensions")
driver = webdriver.Chrome(options=chrome_options)
driver.get("https://sktorrent.eu")
# Load cookies
if COOKIE_FILE.exists():
with open(COOKIE_FILE, "r") as f:
cookies = json.load(f)
for c in cookies:
driver.add_cookie(c)
print("🍪 Cookies loaded.")
driver.get(URL)
time.sleep(2)
# ============================================================
# Close interstitial popup reliably
# ============================================================
time.sleep(1)
try:
# JS close always exists even when HTML structure varies
driver.execute_script("try { interstitialBox.closeit(); } catch(e) {}")
print("🧹 Popup closed via JS fallback.")
time.sleep(1)
except:
print(" Popup JS handler not found (probably no popup).")
# ============================================================
# 3) Extract table rows
# ============================================================
rows = driver.find_elements(By.CSS_SELECTOR, "table tr")
print("Total rows found:", len(rows))
real_rows = []
for row in rows:
cells = row.find_elements(By.TAG_NAME, "td")
# REAL TORRENT ROWS ALWAYS HAVE EXACTLY 7 TD CELLS
if len(cells) == 7:
real_rows.append(cells)
print("Real torrent rows:", len(real_rows))
print("")
# ============================================================
# 4) Function to extract fields from one row
# ============================================================
def parse_row(cells):
# --------------------------
# 1⃣ CATEGORY (cells[0])
# --------------------------
category = cells[0].text.strip()
# --------------------------
# 2⃣ TITLE + DETAILS LINK (always inside cells[2])
# --------------------------
title_links = cells[2].find_elements(By.TAG_NAME, "a")
if not title_links:
print("⚠️ Missing title link — skipping row")
return None
a_tag = title_links[0]
visible_name = a_tag.text.strip()
full_title = a_tag.get_attribute("title")
details_link = a_tag.get_attribute("href")
if not details_link:
print("⚠️ Row has no details link — skipping")
return None
# --------------------------
# 3⃣ TORRENT HASH
# --------------------------
parsed = urlparse.urlparse(details_link)
query = urlparse.parse_qs(parsed.query)
if "id" not in query:
print("⚠️ Skipping row with no torrent ID →", details_link)
return None
torrent_hash = query["id"][0]
# --------------------------
# 4⃣ TEXT BLOCK (size + date)
# --------------------------
text_block = cells[2].get_attribute("innerText")
text_block_clean = " ".join(text_block.split())
size_match = re.search(r"Velkost ([0-9\.]+ ?[KMG]B)", text_block_clean, re.IGNORECASE)
added_match = re.search(r"Pridany (.+?)(?:\sObrázok|$)", text_block_clean, re.IGNORECASE)
size_pretty = size_match.group(1) if size_match else None
added_pretty = added_match.group(1) if added_match else None
# Convert “18/11/2025 o 07:00” → “2025-11-18 07:00:00”
added_mysql = None
if added_pretty:
# Normalize formats like "29/11/2025 o 02:29", "29/11/2025 02:29:18"
clean = added_pretty.replace(" o ", " ").strip()
# Split date and time
date_part, *time_parts = clean.split(" ")
# If seconds are missing, add :00
time_part = time_parts[0] if time_parts else "00:00"
if len(time_part.split(":")) == 2:
time_part += ":00"
day, month, year = date_part.split("/")
added_mysql = f"{year}-{month}-{day} {time_part}"
# --------------------------
# 5⃣ IMAGE PREVIEW
# --------------------------
img_link = None
try:
image_a = cells[2].find_element(By.XPATH, ".//a[contains(text(),'Obrázok')]")
mouseover = image_a.get_attribute("onmouseover")
img_match = re.search(r"src=([^ ]+)", mouseover)
if img_match:
img_link = img_match.group(1).replace("'", "").strip()
if img_link.startswith("//"):
img_link = "https:" + img_link
except:
pass
# --------------------------
# 6⃣ SEEDERS (cells[4])
# --------------------------
seeders_a = cells[4].find_element(By.TAG_NAME, "a")
seeders_number = int(seeders_a.text.strip())
seeders_link = seeders_a.get_attribute("href")
# --------------------------
# 7⃣ LEECHERS (cells[5])
# --------------------------
leechers_a = cells[5].find_element(By.TAG_NAME, "a")
leechers_number = int(leechers_a.text.strip())
leechers_link = leechers_a.get_attribute("href")
# --------------------------
# Return result
# --------------------------
return {
"torrent_hash": torrent_hash,
"details_link": details_link,
"category": category,
"title_visible": visible_name,
"title_full": full_title,
"size_pretty": size_pretty,
"added_datetime": added_mysql,
"preview_image": img_link,
"seeders": seeders_number,
"seeders_link": seeders_link,
"leechers": leechers_number,
"leechers_link": leechers_link,
}
# ============================================================
# 5) MySQL INSERT
# ============================================================
insert_sql = """
INSERT INTO torrents (
torrent_hash, details_link, category, title_visible, title_full,
size_pretty, added_datetime, preview_image,
seeders, seeders_link, leechers, leechers_link
) VALUES (
%(torrent_hash)s, %(details_link)s, %(category)s, %(title_visible)s, %(title_full)s,
%(size_pretty)s, %(added_datetime)s, %(preview_image)s,
%(seeders)s, %(seeders_link)s, %(leechers)s, %(leechers_link)s
)
ON DUPLICATE KEY UPDATE
details_link = VALUES(details_link),
category = VALUES(category),
title_visible = VALUES(title_visible),
title_full = VALUES(title_full),
size_pretty = VALUES(size_pretty),
added_datetime = VALUES(added_datetime),
preview_image = VALUES(preview_image),
seeders = VALUES(seeders),
seeders_link = VALUES(seeders_link),
leechers = VALUES(leechers),
leechers_link = VALUES(leechers_link);
"""
# ============================================================
# 6) PROCESS ALL ROWS
# ============================================================
for cells in real_rows:
data = parse_row(cells)
if not data:
continue
print("💾 Saving:", data["title_visible"])
cursor.execute(insert_sql, data)
print("\n✅ DONE — All torrents saved to MySQL.")
driver.quit()

View File

@@ -0,0 +1,291 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
import re
import urllib.parse as urlparse
from pathlib import Path
import json
import requests
# ============================================================
# 1) MySQL CONNECTION
# ============================================================
db = pymysql.connect(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="torrents",
charset="utf8mb4",
autocommit=True
)
cursor = db.cursor()
# ============================================================
# 2) Selenium setup
# ============================================================
COOKIE_FILE = Path("sktorrent_cookies.json")
URL = "https://sktorrent.eu/torrent/torrents.php?active=0"
chrome_options = Options()
chrome_options.add_argument("--start-maximized")
chrome_options.add_argument("--disable-notifications")
chrome_options.add_argument("--disable-popup-blocking")
chrome_options.add_argument("--disable-extensions")
driver = webdriver.Chrome(options=chrome_options)
driver.get("https://sktorrent.eu")
# Load cookies
session_cookies = []
if COOKIE_FILE.exists():
with open(COOKIE_FILE, "r") as f:
cookies = json.load(f)
for c in cookies:
driver.add_cookie(c)
session_cookies.append({c['name']: c['value']})
print("🍪 Cookies loaded.")
driver.get(URL)
time.sleep(2)
# ============================================================
# 3) Close interstitial popup robustly
# ============================================================
try:
driver.execute_script("try { interstitialBox.closeit(); } catch(e) {}")
print("🧹 Popup closed via JS fallback.")
time.sleep(1)
except:
print(" No popup found.")
# ============================================================
# Convert Selenium cookies → Python requests cookies
# ============================================================
requests_session = requests.Session()
for ck in driver.get_cookies():
requests_session.cookies.set(ck["name"], ck["value"])
# ============================================================
# 4) Extract table rows
# ============================================================
rows = driver.find_elements(By.CSS_SELECTOR, "table tr")
print("Total rows found:", len(rows))
real_rows = []
for row in rows:
cells = row.find_elements(By.TAG_NAME, "td")
# REAL TORRENT ROWS ALWAYS HAVE EXACTLY 7 TD CELLS
if len(cells) == 7:
real_rows.append(cells)
print("Real torrent rows:", len(real_rows))
print("")
# ============================================================
# 5) Function to extract fields from one row
# ============================================================
def parse_row(cells):
# --------------------------
# 1⃣ CATEGORY
# --------------------------
category = cells[0].text.strip()
# --------------------------
# 2⃣ DOWNLOAD LINK FOR TORRENT FILE
# --------------------------
try:
download_a = cells[1].find_element(By.TAG_NAME, "a")
download_link = download_a.get_attribute("href")
except:
print("⚠️ No download link in row, skipping.")
return None
parsed_dl = urlparse.urlparse(download_link)
dl_query = urlparse.parse_qs(parsed_dl.query)
torrent_filename = dl_query.get("f", ["unknown.torrent"])[0]
# --------------------------
# 3⃣ Title + details link (in cell[2])
# --------------------------
title_links = cells[2].find_elements(By.TAG_NAME, "a")
if not title_links:
print("⚠️ No title link — skipping row")
return None
a_tag = title_links[0]
visible_name = a_tag.text.strip()
full_title = a_tag.get_attribute("title")
details_link = a_tag.get_attribute("href")
if not details_link:
print("⚠️ Row has no details link — skipping")
return None
# --------------------------
# Extract torrent hash from ?id=
# --------------------------
parsed = urlparse.urlparse(details_link)
query = urlparse.parse_qs(parsed.query)
if "id" not in query:
print("⚠️ Skipping row with no torrent ID →", details_link)
return None
torrent_hash = query["id"][0]
# --------------------------
# 4⃣ Size + date parsing
# --------------------------
text_block = cells[2].get_attribute("innerText")
text_block_clean = " ".join(text_block.split())
size_match = re.search(r"Velkost ([0-9\.]+ ?[KMG]B)", text_block_clean, re.IGNORECASE)
added_match = re.search(r"Pridany (.+?)(?:\sObrázok|$)", text_block_clean, re.IGNORECASE)
size_pretty = size_match.group(1) if size_match else None
added_pretty = added_match.group(1) if added_match else None
# Robust time normalization
added_mysql = None
if added_pretty:
clean = added_pretty.replace(" o ", " ").strip()
parts = clean.split(" ")
date_part = parts[0]
time_part = parts[1] if len(parts) > 1 else "00:00:00"
# add seconds if missing
if len(time_part.split(":")) == 2:
time_part += ":00"
day, month, year = date_part.split("/")
added_mysql = f"{year}-{month}-{day} {time_part}"
# --------------------------
# 5⃣ Image preview
# --------------------------
img_link = None
try:
image_a = cells[2].find_element(By.XPATH, ".//a[contains(text(),'Obrázok')]")
mouseover = image_a.get_attribute("onmouseover")
img_match = re.search(r"src=([^ ]+)", mouseover)
if img_match:
img_link = img_match.group(1).replace("'", "").strip()
if img_link.startswith("//"):
img_link = "https:" + img_link
except:
pass
# --------------------------
# 6⃣ SEEDERS / LEECHERS
# --------------------------
seeders_a = cells[4].find_element(By.TAG_NAME, "a")
seeders_number = int(seeders_a.text.strip())
seeders_link = seeders_a.get_attribute("href")
leechers_a = cells[5].find_element(By.TAG_NAME, "a")
leechers_number = int(leechers_a.text.strip())
leechers_link = leechers_a.get_attribute("href")
# --------------------------
# 7⃣ DOWNLOAD TORRENT CONTENT (.torrent)
# --------------------------
try:
torrent_content = requests_session.get(download_link).content
except Exception as e:
print(f"⚠️ Could not download torrent file for {torrent_hash}: {e}")
torrent_content = None
# --------------------------
# FINAL DICTIONARY
# --------------------------
return {
"torrent_hash": torrent_hash,
"details_link": details_link,
"category": category,
"title_visible": visible_name,
"title_full": full_title,
"size_pretty": size_pretty,
"added_datetime": added_mysql,
"preview_image": img_link,
"seeders": seeders_number,
"seeders_link": seeders_link,
"leechers": leechers_number,
"leechers_link": leechers_link,
"torrent_filename": torrent_filename,
"torrent_content": torrent_content,
}
# ============================================================
# 6) MySQL INSERT
# ============================================================
insert_sql = """
INSERT INTO torrents (
torrent_hash, details_link, category, title_visible, title_full,
size_pretty, added_datetime, preview_image,
seeders, seeders_link, leechers, leechers_link,
torrent_filename, torrent_content
) VALUES (
%(torrent_hash)s, %(details_link)s, %(category)s, %(title_visible)s, %(title_full)s,
%(size_pretty)s, %(added_datetime)s, %(preview_image)s,
%(seeders)s, %(seeders_link)s, %(leechers)s, %(leechers_link)s,
%(torrent_filename)s, %(torrent_content)s
)
ON DUPLICATE KEY UPDATE
details_link = VALUES(details_link),
category = VALUES(category),
title_visible = VALUES(title_visible),
title_full = VALUES(title_full),
size_pretty = VALUES(size_pretty),
added_datetime = VALUES(added_datetime),
preview_image = VALUES(preview_image),
seeders = VALUES(seeders),
seeders_link = VALUES(seeders_link),
leechers = VALUES(leechers),
leechers_link = VALUES(leechers_link),
torrent_filename = VALUES(torrent_filename),
torrent_content = VALUES(torrent_content);
"""
# ============================================================
# 7) PROCESS ALL ROWS
# ============================================================
for cells in real_rows:
data = parse_row(cells)
if not data:
continue
print("💾 Saving:", data["title_visible"])
cursor.execute(insert_sql, data)
print("\n✅ DONE — All torrents saved to MySQL & torrent files downloaded.")
driver.quit()

View File

@@ -0,0 +1,375 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
import re
import urllib.parse as urlparse
from pathlib import Path
import json
import requests
# ============================================================
# 1) MySQL CONNECTION
# ============================================================
db = pymysql.connect(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="torrents",
charset="utf8mb4",
autocommit=True
)
cursor = db.cursor()
# ============================================================
# 2) Selenium setup
# ============================================================
COOKIE_FILE = Path("sktorrent_cookies.json")
# Start URL pro kategorii 24, seřazeno podle data DESC
START_URL = (
"https://sktorrent.eu/torrent/torrents.php"
"?active=0&category=24&order=data&by=DESC&zaner=&jazyk=&page=0"
)
chrome_options = Options()
chrome_options.add_argument("--start-maximized")
chrome_options.add_argument("--disable-notifications")
chrome_options.add_argument("--disable-popup-blocking")
chrome_options.add_argument("--disable-extensions")
driver = webdriver.Chrome(options=chrome_options)
# Pozice a velikost okna (aby nepřekrývalo PyCharm)
driver.set_window_position(380, 50) # 10 cm od levého okraje
driver.set_window_size(1350, 1000) # můžeš změnit dle monitoru
# Nejprve otevřeme hlavní stránku kvůli doméně pro cookies
driver.get("https://sktorrent.eu")
# Load cookies z JSON
if COOKIE_FILE.exists():
with open(COOKIE_FILE, "r") as f:
cookies = json.load(f)
for c in cookies:
driver.add_cookie(c)
print("🍪 Cookies loaded.")
else:
print("⚠️ Cookie file not found, you may not be logged in!")
# ============================================================
# 3) Převod cookies → requests.Session (pro stahování .torrent)
# ============================================================
requests_session = requests.Session()
for ck in driver.get_cookies():
requests_session.cookies.set(ck["name"], ck["value"])
print("🔗 Requests session initialized with Selenium cookies.")
# ============================================================
# 4) Funkce pro zavření popupu
# ============================================================
def close_popup_if_any():
"""Zkusí zavřít interstitial reklamu pomocí JS funkce interstitialBox.closeit()."""
try:
driver.execute_script("try { interstitialBox.closeit(); } catch(e) {}")
# Krátká pauza, ať se DOM uklidní
time.sleep(0.5)
print("🧹 Popup closed via JS fallback (if present).")
except Exception as e:
print(" Popup JS handler not found:", e)
# ============================================================
# 5) Funkce pro parsování jednoho řádku (jednoho torrentu)
# ============================================================
def parse_row(cells):
"""
cells: list<WebElement> o délce 7
Struktura:
0: kategorie
1: download link (.torrent)
2: název + velikost + datum + 'Obrázok' + žánr
3: -- (ignorujeme)
4: seeders
5: leechers
6: completed
"""
# --------------------------
# 1⃣ CATEGORY
# --------------------------
category = cells[0].text.strip()
# --------------------------
# 2⃣ DOWNLOAD LINK FOR TORRENT FILE (cells[1])
# --------------------------
try:
download_a = cells[1].find_element(By.TAG_NAME, "a")
download_link = download_a.get_attribute("href")
except:
print("⚠️ No download link in row, skipping.")
return None
parsed_dl = urlparse.urlparse(download_link)
dl_query = urlparse.parse_qs(parsed_dl.query)
torrent_filename = dl_query.get("f", ["unknown.torrent"])[0]
# --------------------------
# 3⃣ TITLE + DETAILS LINK (in cell[2])
# --------------------------
title_links = cells[2].find_elements(By.TAG_NAME, "a")
if not title_links:
print("⚠️ No title link — skipping row")
return None
a_tag = title_links[0]
visible_name = a_tag.text.strip()
full_title = a_tag.get_attribute("title")
details_link = a_tag.get_attribute("href")
if not details_link:
print("⚠️ Row has no details link — skipping")
return None
# --------------------------
# Extract torrent hash from ?id=
# --------------------------
parsed = urlparse.urlparse(details_link)
query = urlparse.parse_qs(parsed.query)
if "id" not in query:
print("⚠️ Skipping row with no torrent ID →", details_link)
return None
torrent_hash = query["id"][0]
# --------------------------
# 4⃣ Size + date parsing
# --------------------------
text_block = cells[2].get_attribute("innerText")
text_block_clean = " ".join(text_block.split())
size_match = re.search(r"Velkost ([0-9\.]+ ?[KMG]B)", text_block_clean, re.IGNORECASE)
added_match = re.search(r"Pridany (.+?)(?:\sObrázok|$)", text_block_clean, re.IGNORECASE)
size_pretty = size_match.group(1) if size_match else None
added_pretty = added_match.group(1) if added_match else None
# Robustní převod data/času do MySQL datetime
added_mysql = None
if added_pretty:
# "29/11/2025 o 02:29" → "29/11/2025 02:29"
clean = added_pretty.replace(" o ", " ").strip()
parts = clean.split(" ")
date_part = parts[0]
time_part = parts[1] if len(parts) > 1 else "00:00:00"
# pokud chybí sekundy, přidej
if len(time_part.split(":")) == 2:
time_part += ":00"
day, month, year = date_part.split("/")
added_mysql = f"{year}-{month}-{day} {time_part}"
# --------------------------
# 5⃣ Image preview
# --------------------------
img_link = None
try:
image_a = cells[2].find_element(
By.XPATH,
".//a[contains(text(),'Obrázok')]"
)
mouseover = image_a.get_attribute("onmouseover")
img_match = re.search(r"src=([^ ]+)", mouseover)
if img_match:
img_link = img_match.group(1).replace("'", "").strip()
if img_link.startswith("//"):
img_link = "https:" + img_link
except:
pass
# --------------------------
# 6⃣ SEEDERS / LEECHERS
# --------------------------
seeders_a = cells[4].find_element(By.TAG_NAME, "a")
seeders_number = int(seeders_a.text.strip())
seeders_link = seeders_a.get_attribute("href")
leechers_a = cells[5].find_element(By.TAG_NAME, "a")
leechers_number = int(leechers_a.text.strip())
leechers_link = leechers_a.get_attribute("href")
# --------------------------
# 7⃣ DOWNLOAD TORRENT CONTENT (.torrent)
# --------------------------
torrent_content = None
time.sleep(3) #mezera mezi torrenty
try:
resp = requests_session.get(download_link)
resp.raise_for_status()
torrent_content = resp.content
except Exception as e:
print(f"⚠️ Could not download torrent file for {torrent_hash}: {e}")
torrent_content = None
# --------------------------
# FINAL DICTIONARY
# --------------------------
return {
"torrent_hash": torrent_hash,
"details_link": details_link,
"category": category,
"title_visible": visible_name,
"title_full": full_title,
"size_pretty": size_pretty,
"added_datetime": added_mysql,
"preview_image": img_link,
"seeders": seeders_number,
"seeders_link": seeders_link,
"leechers": leechers_number,
"leechers_link": leechers_link,
"torrent_filename": torrent_filename,
"torrent_content": torrent_content,
}
# ============================================================
# 6) MySQL INSERT
# ============================================================
insert_sql = """
INSERT INTO torrents (
torrent_hash, details_link, category, title_visible, title_full,
size_pretty, added_datetime, preview_image,
seeders, seeders_link, leechers, leechers_link,
torrent_filename, torrent_content
) VALUES (
%(torrent_hash)s, %(details_link)s, %(category)s, %(title_visible)s, %(title_full)s,
%(size_pretty)s, %(added_datetime)s, %(preview_image)s,
%(seeders)s, %(seeders_link)s, %(leechers)s, %(leechers_link)s,
%(torrent_filename)s, %(torrent_content)s
)
ON DUPLICATE KEY UPDATE
details_link = VALUES(details_link),
category = VALUES(category),
title_visible = VALUES(title_visible),
title_full = VALUES(title_full),
size_pretty = VALUES(size_pretty),
added_datetime = VALUES(added_datetime),
preview_image = VALUES(preview_image),
seeders = VALUES(seeders),
seeders_link = VALUES(seeders_link),
leechers = VALUES(leechers),
leechers_link = VALUES(leechers_link),
torrent_filename = VALUES(torrent_filename),
torrent_content = VALUES(torrent_content);
"""
# ============================================================
# 7) Funkce pro zpracování jedné stránky
# ============================================================
def process_current_page(page_index: int):
"""
Zpracuje aktuálně otevřenou stránku:
- najde všechny "REAL TORRENT ROWS" (7 td)
- pro každý torrent:
* parse_row
* insert/update do DB
"""
rows = driver.find_elements(By.CSS_SELECTOR, "table tr")
real_rows = []
for row in rows:
cells = row.find_elements(By.TAG_NAME, "td")
# REAL TORRENT ROWS ALWAYS HAVE EXACTLY 7 TD CELLS
if len(cells) == 7:
real_rows.append(cells)
print(f"📄 Page {page_index}: {len(real_rows)} torrent rows")
for cells in real_rows:
data = parse_row(cells)
if not data:
continue
print(f" 💾 [{page_index}] Saving:", data["title_visible"])
cursor.execute(insert_sql, data)
# ============================================================
# 8) Hlavní stránkovací cyklus
# ============================================================
current_url = START_URL
page_index = 0
while True:
print(f"\n🌐 Loading page {page_index}: {current_url}")
driver.get(current_url)
time.sleep(2)
# zavři popup, pokud je
close_popup_if_any()
# zpracuj aktuální stránku
process_current_page(page_index)
# pokus se najít tlačítko "Dalsi >>"
try:
next_btn = driver.find_element(
By.XPATH,
"//a[b[contains(text(),'Dalsi')]]"
)
next_url = next_btn.get_attribute("href")
if not next_url:
print("⛔ Next link has no href, stopping.")
break
# pokud je relativní, doplň doménu
if next_url.startswith("/"):
next_url = "https://sktorrent.eu" + next_url
# když by náhodou bylo stejné URL → přeruš nekonečnou smyčku
if next_url == current_url:
print("⛔ Next URL equals current URL, stopping.")
break
print("➡️ Next page:", next_url)
current_url = next_url
page_index += 1
# malá pauza mezi stránkami
time.sleep(1)
except Exception:
print("✅ No 'Dalsi >>' link found, reached last page. Done.")
break
print("\n🎉 DONE — All pages processed, torrents saved & torrent files downloaded.")
driver.quit()

View File

@@ -23,7 +23,7 @@ HEADERS = {"User-Agent": USER_AGENT}
DB_CFG = { DB_CFG = {
"host": "192.168.1.76", "host": "192.168.1.76",
"port": 3307, "port": 3307,git remote set-url origin https://gitea.buzalka.cz/administrator/torrents.git
"user": "root", "user": "root",
"password": "Vlado9674+", "password": "Vlado9674+",
"database": "torrents", "database": "torrents",

295
50 TorrentManipulation.py Normal file
View File

@@ -0,0 +1,295 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
from datetime import datetime, timedelta
import pymysql
import qbittorrentapi
import bencodepy
# ==============================
# ⚙ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
QBT_CONFIG = {
"host": "192.168.1.76",
"port": 8080,
"username": "admin",
"password": "adminadmin",
}
MAX_ACTIVE_DOWNLOADS = 10
LOOP_SLEEP_SECONDS = 60
# Torrent označíme jako "dead" pokud nebyl nikdy "seen_complete"
# více než X minut od přidání
DEAD_TORRENT_MINUTES = 5
DEFAULT_SAVE_PATH = None
# ==============================
# 🔧 CONNECT
# ==============================
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor(pymysql.cursors.DictCursor)
qb = qbittorrentapi.Client(
host=QBT_CONFIG["host"],
port=QBT_CONFIG["port"],
username=QBT_CONFIG["username"],
password=QBT_CONFIG["password"],
)
try:
qb.auth_log_in()
print("✅ Connected to qBittorrent.")
except Exception as e:
print("❌ Could not connect:", e)
raise SystemExit(1)
# ==============================
# 🧪 TORRENT VALIDATION
# ==============================
def is_valid_torrent(blob: bytes) -> bool:
"""
Returns True only if BLOB is a valid .torrent file.
"""
try:
data = bencodepy.decode(blob)
return isinstance(data, dict) and b"info" in data
except Exception:
return False
# ==============================
# 🔄 SYNC FROM QB → DB
# ==============================
def sync_qb_to_db():
torrents = qb.torrents_info()
for t in torrents:
completion_dt = None
if getattr(t, "completion_on", 0):
try:
completion_dt = datetime.fromtimestamp(t.completion_on)
except:
pass
sql = """
UPDATE torrents
SET qb_added = 1,
qb_hash = COALESCE(qb_hash, %s),
qb_state = %s,
qb_progress = %s,
qb_savepath = %s,
qb_completed_datetime =
IF(%s IS NOT NULL AND qb_completed_datetime IS NULL, %s, qb_completed_datetime),
qb_last_update = NOW()
WHERE qb_hash = %s OR torrent_hash = %s
"""
cursor.execute(sql, (
t.hash,
t.state,
float(t.progress) * 100.0,
getattr(t, "save_path", None),
completion_dt,
completion_dt,
t.hash,
t.hash
))
# ==============================
# 🧹 HANDLE COMPLETED + DEAD TORRENTS
# ==============================
def handle_completed_and_dead():
torrents = qb.torrents_info()
for t in torrents:
t_hash = t.hash
state = t.state
progress = float(t.progress)
# ==========================
# ✔ COMPLETED
# ==========================
if progress >= 1.0 or state in {"completed", "uploading", "stalledUP", "queuedUP"}:
print(f"✅ Completed torrent → remove (keep data): {t.name}")
try:
qb.torrents_delete(torrent_hashes=t_hash, delete_files=False)
except Exception as e:
print("⚠️ delete failed:", e)
cursor.execute("""
UPDATE torrents
SET qb_state='completed',
qb_progress=100,
qb_completed_datetime=NOW(),
qb_last_update=NOW()
WHERE qb_hash=%s OR torrent_hash=%s
""", (t_hash, t_hash))
continue
# ==========================
# ❌ DEAD TORRENT (never seen_complete)
# ==========================
props = qb.torrents_properties(t_hash)
seen = getattr(props, "last_seen", 0)
if seen == -1: # never seen complete
added_dt = getattr(t, "added_on", 0)
if added_dt:
added_time = datetime.fromtimestamp(added_dt)
if datetime.now() - added_time > timedelta(minutes=DEAD_TORRENT_MINUTES):
print(f"💀 Dead torrent (> {DEAD_TORRENT_MINUTES} min unseen): {t.name}")
try:
qb.torrents_delete(torrent_hashes=t_hash, delete_files=True)
except:
pass
cursor.execute("""
UPDATE torrents
SET qb_state='dead',
qb_last_update=NOW()
WHERE qb_hash=%s OR torrent_hash=%s
""", (t_hash, t_hash))
# ==============================
# 📊 COUNT ACTIVE DOWNLOADS
# ==============================
def count_active_downloads():
torrents = qb.torrents_info(filter="all")
return sum(1 for t in torrents if float(t.progress) < 1.0)
# ==============================
# ENQUEUE NEW TORRENTS
# ==============================
def enqueue_new_torrents():
active = count_active_downloads()
print("DEBUG active =", active)
if active >= MAX_ACTIVE_DOWNLOADS:
print(f"📦 {active}/{MAX_ACTIVE_DOWNLOADS} active → no enqueue")
return
slots = MAX_ACTIVE_DOWNLOADS - active
sql = """
SELECT id, torrent_hash, torrent_content, torrent_filename, added_datetime
FROM torrents
WHERE (qb_added IS NULL OR qb_added = 0)
AND torrent_content IS NOT NULL
ORDER BY added_datetime DESC -- <── take NEWEST FIRST
LIMIT %s
"""
cursor.execute(sql, (slots,))
rows = cursor.fetchall()
if not rows:
print(" No new torrents")
return
for row in rows:
t_id = row["id"]
t_hash = row["torrent_hash"]
blob = row["torrent_content"]
filename = row.get("torrent_filename", "unknown.torrent")
if not blob:
print("⚠️ empty blob, skip")
continue
# ==========================
# 🧪 VALIDATION OF .TORRENT
# ==========================
if not is_valid_torrent(blob):
print(f"❌ INVALID TORRENT id={t_id}, size={len(blob)} → deleting content")
cursor.execute("""
UPDATE torrents
SET qb_state='invalid',
torrent_content=NULL,
qb_last_update=NOW()
WHERE id=%s
""", (t_id,))
continue
# ==========================
# ADD TORRENT
# ==========================
print(f" Adding torrent: {filename} ({t_hash})")
try:
qb.torrents_add(torrent_files=blob, savepath=DEFAULT_SAVE_PATH)
except Exception as e:
print(f"❌ Failed to add {t_hash}: {e}")
continue
cursor.execute("""
UPDATE torrents
SET qb_added=1,
qb_hash=COALESCE(qb_hash, %s),
qb_state='added',
qb_last_update=NOW()
WHERE id=%s
""", (t_hash, t_id))
# ==============================
# 🏁 MAIN LOOP
# ==============================
print("🚀 Worker started")
try:
while True:
print(f"\n⏱ Loop {datetime.now():%Y-%m-%d %H:%M:%S}")
sync_qb_to_db()
handle_completed_and_dead()
enqueue_new_torrents()
print(f"🛌 Sleep {LOOP_SLEEP_SECONDS}s\n")
time.sleep(LOOP_SLEEP_SECONDS)
except KeyboardInterrupt:
print("🛑 Stopping worker...")
finally:
db.close()
print("👋 Bye.")

72
60 Testcountoftorrents.py Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
import qbittorrentapi
# ==============================
# CONFIG přizpůsob si podle sebe
# ==============================
QBT_CONFIG = {
"host": "192.168.1.76",
"port": 8080,
"username": "admin",
"password": "adminadmin",
}
def fmt_ts(ts: int) -> str:
"""
Převod unix timestampu na čitelný string.
qBittorrent vrací -1 pokud hodnota není známá.
"""
if ts is None or ts <= 0:
return ""
try:
return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return f"invalid({ts})"
def main():
# Připojení
qb = qbittorrentapi.Client(
host=QBT_CONFIG["host"],
port=QBT_CONFIG["port"],
username=QBT_CONFIG["username"],
password=QBT_CONFIG["password"],
)
try:
qb.auth_log_in()
print("✅ Connected to qBittorrent\n")
except Exception as e:
print("❌ Could not connect to qBittorrent:", e)
return
# Všechno, žádný filter na downloading
torrents = qb.torrents_info(filter='all')
print(f"Found {len(torrents)} torrents (filter='all')\n")
for t in torrents:
# properties obsahují last_seen
try:
props = qb.torrents_properties(t.hash)
except Exception as e:
print(f"⚠️ Cannot get properties for {t.hash[:8]} {t.name}: {e}")
continue
seen_complete = getattr(t, "seen_complete", None) # z /torrents/info
last_seen = getattr(props, "last_seen", None) # z /torrents/properties
print("=" * 80)
print(f"Name : {t.name}")
print(f"Hash : {t.hash}")
print(f"State : {t.state}")
print(f"Progress : {float(t.progress) * 100:.2f}%")
print(f"Seen complete: {fmt_ts(seen_complete)} (t.seen_complete)")
print(f"Last seen : {fmt_ts(last_seen)} (props.last_seen)")
print("\n✅ Done.")
if __name__ == "__main__":
main()

143
70 MD5.py Normal file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
FAST MD5 indexer with in-memory cache
- prints every processed file
- skips unchanged files instantly
- restart-safe (no reprocessing same files)
"""
import os
import hashlib
from datetime import datetime
import pymysql
# ==============================
# CONFIG
# ==============================
ROOT_DIR = r"\\tower1\#ColdData\porno"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
CHUNK_SIZE = 1024 * 1024 # 1 MB
PRINT_SKIPPED = True # set False if too noisy
# ==============================
# HELPERS
# ==============================
def compute_md5(path: str) -> str:
h = hashlib.md5()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(CHUNK_SIZE), b""):
h.update(chunk)
return h.hexdigest()
def format_size(size):
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024:
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} PB"
# ==============================
# MAIN
# ==============================
def main():
db = pymysql.connect(**DB_CONFIG)
cur = db.cursor()
print("📥 Loading already indexed files into memory...")
cur.execute("""
SELECT full_path, file_size, UNIX_TIMESTAMP(mtime)
FROM file_md5_index
""")
indexed = {
(row[0], row[1], row[2])
for row in cur.fetchall()
}
print(f"✅ Loaded {len(indexed):,} indexed entries")
print("======================================")
new_files = 0
skipped = 0
for root, _, files in os.walk(ROOT_DIR):
for fname in files:
full_path = os.path.join(root, fname)
try:
stat = os.stat(full_path)
except (OSError, FileNotFoundError):
continue
mtime = int(stat.st_mtime)
size = stat.st_size
key = (full_path, size, mtime)
# FAST PATH
if key in indexed:
skipped += 1
if PRINT_SKIPPED:
print("⏭ SKIP")
print(f" File: {full_path}")
continue
print(" NEW / UPDATED")
print(f" Size: {format_size(size)}")
print(f" File: {full_path}")
try:
md5 = compute_md5(full_path)
except Exception as e:
print(f"❌ MD5 failed: {e}")
continue
cur.execute("""
INSERT INTO file_md5_index
(full_path, file_name, directory, file_size, mtime, md5)
VALUES (%s, %s, %s, %s, FROM_UNIXTIME(%s), %s)
ON DUPLICATE KEY UPDATE
file_size=VALUES(file_size),
mtime=VALUES(mtime),
md5=VALUES(md5),
updated_at=CURRENT_TIMESTAMP
""", (
full_path,
fname,
root,
size,
mtime,
md5,
))
new_files += 1
print(f" MD5 : {md5}")
print("--------------------------------------")
print("======================================")
print(f"✅ New / updated : {new_files}")
print(f"⏭ Skipped : {skipped}")
print("======================================")
cur.close()
db.close()
if __name__ == "__main__":
main()

356
80 TorrentManipulation.py Normal file
View File

@@ -0,0 +1,356 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
import pymysql
import qbittorrentapi
import bencodepy
from EmailMessagingGraph import send_mail
# ==============================
# ⚙ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
QBT_CONFIG = {
"host": "192.168.1.76",
"port": 8080,
"username": "admin",
"password": "adminadmin",
}
# ZVÝŠENO NA 100 dle požadavku
MAX_ACTIVE_DOWNLOADS = 250
# JAK DLOUHO ČEKAT?
# Doporučuji alespoň 3 dny (4320 minut).
# Pokud se do 3 dnů neobjeví nikdo, kdo má 100% souboru, je to pravděpodobně mrtvé.
DEAD_TORRENT_DAYS = 3
DEAD_TORRENT_MINUTES = DEAD_TORRENT_DAYS * 24 * 60
DEFAULT_SAVE_PATH = None
MAIL_TO = "vladimir.buzalka@buzalka.cz"
MAX_LIST_ITEMS = 50 # cap lists in email
# ==============================
# 🧮 RUNTIME STATS + LISTS
# ==============================
RUN_START = datetime.now()
stat_synced = 0
stat_completed = 0
stat_dead = 0
stat_enqueued = 0
deleted_completed = [] # list[str]
deleted_dead = [] # list[str]
added_new = [] # list[str]
active_downloading = [] # list[str]
# ==============================
# 🔧 CONNECT
# ==============================
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor(pymysql.cursors.DictCursor)
qb = qbittorrentapi.Client(**QBT_CONFIG)
try:
qb.auth_log_in()
print("✅ Connected to qBittorrent.")
except Exception as e:
raise SystemExit(f"❌ Could not connect to qBittorrent: {e}")
# ==============================
# 🧪 TORRENT VALIDATION
# ==============================
def is_valid_torrent(blob: bytes) -> bool:
try:
data = bencodepy.decode(blob)
return isinstance(data, dict) and b"info" in data
except Exception:
return False
# ==============================
# 🔄 SYNC FROM QB → DB
# ==============================
def sync_qb_to_db():
global stat_synced
torrents = qb.torrents_info()
stat_synced = len(torrents)
for t in torrents:
completion_dt = None
if getattr(t, "completion_on", 0):
try:
completion_dt = datetime.fromtimestamp(t.completion_on)
except Exception:
pass
cursor.execute("""
UPDATE torrents
SET qb_added = 1,
qb_hash = COALESCE(qb_hash, %s),
qb_state = %s,
qb_progress = %s,
qb_savepath = %s,
qb_completed_datetime =
IF(%s IS NOT NULL AND qb_completed_datetime IS NULL, %s, qb_completed_datetime),
qb_last_update = NOW()
WHERE qb_hash = %s OR torrent_hash = %s
""", (
t.hash,
t.state,
float(t.progress) * 100.0,
getattr(t, "save_path", None),
completion_dt,
completion_dt,
t.hash,
t.hash,
))
# ==============================
# 🧹 HANDLE COMPLETED + DEAD
# ==============================
def handle_completed_and_dead():
global stat_completed, stat_dead
# Načteme info o torrentech
torrents = qb.torrents_info()
for t in torrents:
t_hash = t.hash
state = t.state
progress = float(t.progress)
# Získání dostupnosti (availability) - defaultně -1 pokud není k dispozici
availability = float(getattr(t, "availability", -1))
# Získání času přidání
added_ts = getattr(t, "added_on", 0)
added_dt = datetime.fromtimestamp(added_ts) if added_ts > 0 else datetime.now()
age_in_minutes = (datetime.now() - added_dt).total_seconds() / 60
# ---------------------------
# 1. ✔ COMPLETED (Hotovo)
# ---------------------------
if progress >= 1.0 or state in {"completed", "uploading", "stalledUP", "queuedUP"}:
stat_completed += 1
deleted_completed.append(t.name)
try:
# Smažeme z QB, ale necháme data na disku
qb.torrents_delete(torrent_hashes=t_hash, delete_files=False)
except Exception as e:
print(f"⚠️ delete (keep data) failed for {t.name}: {e}")
cursor.execute("""
UPDATE torrents
SET qb_state='completed',
qb_progress=100,
qb_completed_datetime=NOW(),
qb_last_update=NOW()
WHERE qb_hash=%s OR torrent_hash=%s
""", (t_hash, t_hash))
continue
# ---------------------------
# 2. ❌ DEAD (Mrtvý)
# ---------------------------
# Logika: Je to starší než limit? A ZÁROVEŇ je dostupnost < 1 (nikdo nemá celý soubor)?
is_old_enough = age_in_minutes > DEAD_TORRENT_MINUTES
is_unavailable = availability < 1.0
if is_old_enough and is_unavailable:
stat_dead += 1
deleted_dead.append(f"{t.name} (Avail: {availability:.2f})")
try:
# Smažeme z QB včetně nedotažených souborů
qb.torrents_delete(torrent_hashes=t_hash, delete_files=True)
except Exception as e:
print(f"⚠️ delete (files) failed for {t.name}: {e}")
cursor.execute("""
UPDATE torrents
SET qb_state='dead',
qb_last_update=NOW()
WHERE qb_hash=%s OR torrent_hash=%s
""", (t_hash, t_hash))
# ==============================
# 📊 ACTIVE DOWNLOADS
# ==============================
def count_active_downloads():
# Počítáme jen ty, co nejsou hotové (progress < 100%)
return sum(1 for t in qb.torrents_info() if float(t.progress) < 1.0)
def snapshot_active_downloading():
"""
Capture current actively downloading torrents (progress < 100%).
"""
active = []
for t in qb.torrents_info():
prog = float(t.progress)
avail = float(getattr(t, "availability", 0))
if prog < 1.0:
active.append(f"{t.name}{prog * 100:.1f}% — Avail:{avail:.2f}")
return sorted(active)
# ==============================
# ENQUEUE NEW TORRENTS
# ==============================
def enqueue_new_torrents():
global stat_enqueued
active = count_active_downloads()
# Pokud máme plno (100+), nic nepřidáváme
if active >= MAX_ACTIVE_DOWNLOADS:
return
# Kolik slotů zbývá do 100
slots = MAX_ACTIVE_DOWNLOADS - active
cursor.execute("""
SELECT id, torrent_hash, torrent_content, torrent_filename
FROM torrents
WHERE (qb_added IS NULL OR qb_added = 0)
AND torrent_content IS NOT NULL
AND (qb_state IS NULL OR qb_state != 'dead')
ORDER BY added_datetime DESC
LIMIT %s
""", (slots,))
rows = cursor.fetchall()
for row in rows:
blob = row["torrent_content"]
if not blob:
continue
if not is_valid_torrent(blob):
cursor.execute("""
UPDATE torrents
SET qb_state='invalid',
torrent_content=NULL,
qb_last_update=NOW()
WHERE id=%s
""", (row["id"],))
continue
# Add torrent
try:
qb.torrents_add(torrent_files=blob, savepath=DEFAULT_SAVE_PATH)
except Exception as e:
print(f"❌ Failed to add {row['torrent_hash']}: {e}")
continue
stat_enqueued += 1
added_new.append(row.get("torrent_filename") or row["torrent_hash"])
cursor.execute("""
UPDATE torrents
SET qb_added=1,
qb_hash=COALESCE(qb_hash, %s),
qb_state='added',
qb_last_update=NOW()
WHERE id=%s
""", (row["torrent_hash"], row["id"]))
# ==============================
# ✉️ EMAIL HELPERS
# ==============================
def format_list(title: str, items: list[str]) -> list[str]:
lines = []
if not items:
return [f"{title}: (none)"]
lines.append(f"{title}: {len(items)}")
shown = items[:MAX_LIST_ITEMS]
for it in shown:
lines.append(f" - {it}")
if len(items) > MAX_LIST_ITEMS:
lines.append(f" ... (+{len(items) - MAX_LIST_ITEMS} more)")
return lines
# ==============================
# 🏁 MAIN (ONE RUN)
# ==============================
print("🚀 QB worker run started")
try:
sync_qb_to_db()
handle_completed_and_dead()
enqueue_new_torrents()
# Snapshot after enqueue/deletions, so email reflects end-state
active_downloading = snapshot_active_downloading()
finally:
db.close()
# ==============================
# 📧 EMAIL REPORT
# ==============================
RUN_END = datetime.now()
body_lines = [
f"Run started : {RUN_START:%Y-%m-%d %H:%M:%S}",
f"Run finished: {RUN_END:%Y-%m-%d %H:%M:%S}",
"",
f"QB torrents synced : {stat_synced}",
f"Completed removed : {stat_completed}",
f"Dead removed : {stat_dead}",
f"New torrents added : {stat_enqueued}",
f"Active downloads : {len(active_downloading)} (Max: {MAX_ACTIVE_DOWNLOADS})",
"",
]
body_lines += format_list("Deleted (completed, kept data)", deleted_completed)
body_lines.append("")
body_lines += format_list("Deleted (DEAD > 3 days & Avail < 1.0)", deleted_dead)
body_lines.append("")
body_lines += format_list("Newly added to qBittorrent", added_new)
body_lines.append("")
body_lines += format_list("Actively downloading now", active_downloading)
send_mail(
to=MAIL_TO,
subject=f"qBittorrent worker {RUN_START:%Y-%m-%d %H:%M}",
body="\n".join(body_lines),
html=False,
)
print("📧 Email report sent")
print("🎉 DONE")

362
81 TorrentManipulation.py Normal file
View File

@@ -0,0 +1,362 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
import pymysql
import qbittorrentapi
import bencodepy
from EmailMessagingGraph import send_mail
# ==============================
# ⚙ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
QBT_CONFIG = {
"host": "192.168.1.76",
"port": 8080,
"username": "admin",
"password": "adminadmin",
}
# ZVÝŠENO NA 100 dle požadavku
MAX_ACTIVE_DOWNLOADS = 250
# JAK DLOUHO ČEKAT?
# Doporučuji alespoň 3 dny (4320 minut).
# Pokud se do 3 dnů neobjeví nikdo, kdo má 100% souboru, je to pravděpodobně mrtvé.
DEAD_TORRENT_DAYS = 3
DEAD_TORRENT_MINUTES = DEAD_TORRENT_DAYS * 24 * 60
DEFAULT_SAVE_PATH = None
MAIL_TO = "vladimir.buzalka@buzalka.cz"
MAX_LIST_ITEMS = 50 # cap lists in email
# ==============================
# 🧮 RUNTIME STATS + LISTS
# ==============================
RUN_START = datetime.now()
stat_synced = 0
stat_completed = 0
stat_dead = 0
stat_enqueued = 0
deleted_completed = [] # list[str]
deleted_dead = [] # list[str]
added_new = [] # list[str]
active_downloading = [] # list[str]
# ==============================
# 🔧 CONNECT
# ==============================
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor(pymysql.cursors.DictCursor)
qb = qbittorrentapi.Client(**QBT_CONFIG)
try:
qb.auth_log_in()
print("✅ Connected to qBittorrent.")
except Exception as e:
raise SystemExit(f"❌ Could not connect to qBittorrent: {e}")
# ==============================
# 🧪 TORRENT VALIDATION
# ==============================
def is_valid_torrent(blob: bytes) -> bool:
try:
data = bencodepy.decode(blob)
return isinstance(data, dict) and b"info" in data
except Exception:
return False
# ==============================
# 🔄 SYNC FROM QB → DB
# ==============================
def sync_qb_to_db():
global stat_synced
torrents = qb.torrents_info(limit=1000)
stat_synced = len(torrents)
for t in torrents:
completion_dt = None
if getattr(t, "completion_on", 0):
try:
completion_dt = datetime.fromtimestamp(t.completion_on)
except Exception:
pass
cursor.execute("""
UPDATE torrents
SET qb_added = 1,
qb_hash = COALESCE(qb_hash, %s),
qb_state = %s,
qb_progress = %s,
qb_savepath = %s,
qb_completed_datetime =
IF(%s IS NOT NULL AND qb_completed_datetime IS NULL, %s, qb_completed_datetime),
qb_last_update = NOW()
WHERE qb_hash = %s OR torrent_hash = %s
""", (
t.hash,
t.state,
float(t.progress) * 100.0,
getattr(t, "save_path", None),
completion_dt,
completion_dt,
t.hash,
t.hash,
))
# ==============================
# 🧹 HANDLE COMPLETED + DEAD
# ==============================
def handle_completed_and_dead():
global stat_completed, stat_dead
# Načteme info o torrentech
torrents = qb.torrents_info(limit=1000)
for t in torrents:
t_hash = t.hash
state = t.state
progress = float(t.progress)
# Získání dostupnosti (availability) - defaultně -1 pokud není k dispozici
availability = float(getattr(t, "availability", -1))
# Získání času přidání
added_ts = getattr(t, "added_on", 0)
added_dt = datetime.fromtimestamp(added_ts) if added_ts > 0 else datetime.now()
age_in_minutes = (datetime.now() - added_dt).total_seconds() / 60
# ---------------------------
# 1. ✔ COMPLETED (Hotovo)
# ---------------------------
if progress >= 1.0 or state in {"completed", "uploading", "stalledUP", "queuedUP"}:
stat_completed += 1
deleted_completed.append(t.name)
try:
# Smažeme z QB, ale necháme data na disku
qb.torrents_delete(torrent_hashes=t_hash, delete_files=False)
except Exception as e:
print(f"⚠️ delete (keep data) failed for {t.name}: {e}")
cursor.execute("""
UPDATE torrents
SET qb_state='completed',
qb_progress=100,
qb_completed_datetime=NOW(),
qb_last_update=NOW()
WHERE qb_hash=%s OR torrent_hash=%s
""", (t_hash, t_hash))
continue
# ---------------------------
# 2. ❌ DEAD (Mrtvý)
# ---------------------------
# LOGIKA:
# A) Starší než limit (3 dny)
# B) Dostupnost < 1.0 (nikdo nemá celý soubor)
# C) Stav je VYLOŽENĚ "stalledDL" (zaseknuté stahování)
# Tím ignorujeme "queuedDL" (čeká ve frontě) i "downloading" (stahuje)
is_old_enough = age_in_minutes > DEAD_TORRENT_MINUTES
is_unavailable = availability < 1.0
is_stalled = (state == "stalledDL")
if is_old_enough and is_unavailable and is_stalled:
stat_dead += 1
deleted_dead.append(f"{t.name} (Avail: {availability:.2f}, State: {state})")
try:
# Smažeme z QB včetně nedotažených souborů
qb.torrents_delete(torrent_hashes=t_hash, delete_files=True)
except Exception as e:
print(f"⚠️ delete (files) failed for {t.name}: {e}")
cursor.execute("""
UPDATE torrents
SET qb_state='dead',
qb_last_update=NOW()
WHERE qb_hash=%s OR torrent_hash=%s
""", (t_hash, t_hash))
# ==============================
# 📊 ACTIVE DOWNLOADS
# ==============================
def count_active_downloads():
# Počítáme jen ty, co nejsou hotové (progress < 100%)
return sum(1 for t in qb.torrents_info(limit=1000) if float(t.progress) < 1.0)
def snapshot_active_downloading():
"""
Capture current actively downloading torrents (progress < 100%).
"""
active = []
for t in qb.torrents_info(limit=1000):
prog = float(t.progress)
avail = float(getattr(t, "availability", 0))
# Zobrazíme i stav, abychom v mailu viděli, zda je queued nebo stalled
state = t.state
if prog < 1.0:
active.append(f"{t.name}{prog * 100:.1f}% — Avail:{avail:.2f} — [{state}]")
return sorted(active)
# ==============================
# ENQUEUE NEW TORRENTS
# ==============================
def enqueue_new_torrents():
global stat_enqueued
active = count_active_downloads()
# Pokud máme plno, nic nepřidáváme
if active >= MAX_ACTIVE_DOWNLOADS:
return
# Kolik slotů zbývá
slots = MAX_ACTIVE_DOWNLOADS - active
cursor.execute("""
SELECT id, torrent_hash, torrent_content, torrent_filename
FROM torrents
WHERE (qb_added IS NULL OR qb_added = 0)
AND torrent_content IS NOT NULL
AND (qb_state IS NULL OR qb_state != 'dead')
ORDER BY added_datetime DESC
LIMIT %s
""", (slots,))
rows = cursor.fetchall()
for row in rows:
blob = row["torrent_content"]
if not blob:
continue
if not is_valid_torrent(blob):
cursor.execute("""
UPDATE torrents
SET qb_state='invalid',
torrent_content=NULL,
qb_last_update=NOW()
WHERE id=%s
""", (row["id"],))
continue
# Add torrent
try:
qb.torrents_add(torrent_files=blob, savepath=DEFAULT_SAVE_PATH)
except Exception as e:
print(f"❌ Failed to add {row['torrent_hash']}: {e}")
continue
stat_enqueued += 1
added_new.append(row.get("torrent_filename") or row["torrent_hash"])
cursor.execute("""
UPDATE torrents
SET qb_added=1,
qb_hash=COALESCE(qb_hash, %s),
qb_state='added',
qb_last_update=NOW()
WHERE id=%s
""", (row["torrent_hash"], row["id"]))
# ==============================
# ✉️ EMAIL HELPERS
# ==============================
def format_list(title: str, items: list[str]) -> list[str]:
lines = []
if not items:
return [f"{title}: (none)"]
lines.append(f"{title}: {len(items)}")
shown = items[:MAX_LIST_ITEMS]
for it in shown:
lines.append(f" - {it}")
if len(items) > MAX_LIST_ITEMS:
lines.append(f" ... (+{len(items) - MAX_LIST_ITEMS} more)")
return lines
# ==============================
# 🏁 MAIN (ONE RUN)
# ==============================
print("🚀 QB worker run started")
try:
sync_qb_to_db()
handle_completed_and_dead()
enqueue_new_torrents()
# Snapshot after enqueue/deletions, so email reflects end-state
active_downloading = snapshot_active_downloading()
finally:
db.close()
# ==============================
# 📧 EMAIL REPORT
# ==============================
RUN_END = datetime.now()
body_lines = [
f"Run started : {RUN_START:%Y-%m-%d %H:%M:%S}",
f"Run finished: {RUN_END:%Y-%m-%d %H:%M:%S}",
"",
f"QB torrents synced : {stat_synced}",
f"Completed removed : {stat_completed}",
f"Dead removed : {stat_dead}",
f"New torrents added : {stat_enqueued}",
f"Active downloads : {len(active_downloading)} (Max: {MAX_ACTIVE_DOWNLOADS})",
"",
]
body_lines += format_list("Deleted (completed, kept data)", deleted_completed)
body_lines.append("")
body_lines += format_list("Deleted (DEAD > 3 days & StalledDL & Avail < 1.0)", deleted_dead)
body_lines.append("")
body_lines += format_list("Newly added to qBittorrent", added_new)
body_lines.append("")
body_lines += format_list("Actively downloading now", active_downloading)
send_mail(
to=MAIL_TO,
subject=f"qBittorrent worker {RUN_START:%Y-%m-%d %H:%M}",
body="\n".join(body_lines),
html=False,
)
print("📧 Email report sent")
print("🎉 DONE")

153
82 Reporting.py Normal file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import pandas as pd
import os
from datetime import datetime
# ==============================
# ⚙ KONFIGURACE
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4"
}
# Cílová složka (používám 'r' před řetězcem pro bezpečné načtení zpětných lomítek)
OUTPUT_DIR = r"u:\Dropbox\!!!Days\Downloads Z230"
FILE_NAME = f"Torrents_Report_{datetime.now():%Y-%m-%d}.xlsx"
# Spojíme cestu a název souboru
FULL_OUTPUT_PATH = os.path.join(OUTPUT_DIR, FILE_NAME)
# ==============================
# 📥 NAČTENÍ DAT
# ==============================
def get_data():
print("⏳ Připojuji se k databázi a stahuji data...")
conn = pymysql.connect(**DB_CONFIG)
query = """
SELECT
id,
category,
title_visible AS 'Název',
size_pretty AS 'Velikost',
added_datetime AS 'Přidáno do DB',
qb_state AS 'Stav v QB',
qb_progress AS 'Postup (%)',
qb_savepath AS 'Cesta na disku',
qb_completed_datetime AS 'Dokončeno',
qb_last_update AS 'Poslední info'
FROM torrents
ORDER BY added_datetime DESC
"""
df = pd.read_sql(query, conn)
conn.close()
return df
# ==============================
# 🎨 FORMÁTOVÁNÍ EXCELU
# ==============================
def auto_adjust_columns(writer, df, sheet_name):
"""Bezpečné automatické nastavení šířky sloupců"""
worksheet = writer.sheets[sheet_name]
for idx, col in enumerate(df.columns):
series = df[col]
max_len = len(str(col)) # minimálně délka hlavičky
for val in series:
if val is None or (isinstance(val, float) and pd.isna(val)):
length = 0
else:
length = len(str(val))
if length > max_len:
max_len = length
max_len = min(max_len + 2, 60)
worksheet.set_column(idx, idx, max_len)
# ==============================
# 🚀 HLAVNÍ LOGIKA
# ==============================
def generate_report():
# 1. Kontrola cesty
if not os.path.exists(OUTPUT_DIR):
print(f"❌ CHYBA: Cílová složka neexistuje nebo není dostupná: {OUTPUT_DIR}")
print(" Ujistěte se, že je disk U: připojen.")
return
df = get_data()
print(f"✅ Načteno {len(df)} záznamů.")
# 2. ÚPRAVA DAT
df['Postup (%)'] = df['Postup (%)'].fillna(0).astype(float).round(1)
# 3. FILTROVÁNÍ
# A) DEAD
mask_dead = df['Stav v QB'].isin(['dead', 'invalid'])
df_dead = df[mask_dead].copy()
# B) COMPLETED
mask_completed = (
(df['Stav v QB'] == 'completed') |
(df['Postup (%)'] >= 100)
) & (~mask_dead)
df_completed = df[mask_completed].copy()
# C) ACTIVE / QUEUED
mask_active = (~mask_dead) & (~mask_completed)
df_active = df[mask_active].copy()
# Seřazení
df_active = df_active.sort_values(by=['Postup (%)', 'Přidáno do DB'], ascending=[False, False])
df_completed = df_completed.sort_values(by='Dokončeno', ascending=False)
df_dead = df_dead.sort_values(by='Poslední info', ascending=False)
# 4. EXPORT
print(f"💾 Ukládám do: {FULL_OUTPUT_PATH}")
try:
with pd.ExcelWriter(FULL_OUTPUT_PATH, engine='xlsxwriter') as writer:
# List 1: Ke stažení
df_active.to_excel(writer, sheet_name='Ke stažení', index=False)
auto_adjust_columns(writer, df_active, 'Ke stažení')
# List 2: Hotovo
df_completed.to_excel(writer, sheet_name='Hotovo', index=False)
auto_adjust_columns(writer, df_completed, 'Hotovo')
# List 3: Dead
df_dead.to_excel(writer, sheet_name='Smazáno (Dead)', index=False)
auto_adjust_columns(writer, df_dead, 'Smazáno (Dead)')
# List 4: Vše
df.to_excel(writer, sheet_name='Kompletní DB', index=False)
auto_adjust_columns(writer, df, 'Kompletní DB')
print("🎉 Hotovo! Report byl úspěšně uložen na disk U:")
except Exception as e:
print(f"❌ Chyba při zápisu souboru: {e}")
if __name__ == "__main__":
generate_report()

View File

@@ -0,0 +1,310 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
import re
import urllib.parse as urlparse
from pathlib import Path
import json
import requests
import datetime
import sys
# Ensure this file exists in your directory
from EmailMessagingGraph import send_mail
# ============================================================
# RUNTIME INFO
# ============================================================
RUN_START = datetime.datetime.now()
processed_count = 0
new_torrent_count = 0
existing_torrent_count = 0
new_titles = []
print(f"🕒 Run started at {RUN_START:%Y-%m-%d %H:%M:%S}")
sys.stdout.flush()
# ============================================================
# 1) MySQL CONNECTION
# ============================================================
db = pymysql.connect(
host="192.168.1.50",
port=3306,
user="root",
password="Vlado9674+",
database="torrents",
charset="utf8mb4",
autocommit=True,
)
cursor = db.cursor()
# ============================================================
# 2) Selenium setup
# ============================================================
COOKIE_FILE = Path("sktorrent_cookies.json")
# Updated to standard torrents.php as requested
BASE_URL = (
"https://sktorrent.eu/torrent/torrents.php"
"?active=0&category=24&order=data&by=DESC&zaner=&jazyk="
)
chrome_options = Options()
chrome_options.add_argument("--start-maximized")
chrome_options.add_argument("--disable-notifications")
chrome_options.add_argument("--disable-popup-blocking")
chrome_options.add_argument("--disable-extensions")
driver = webdriver.Chrome(options=chrome_options)
driver.set_window_position(380, 50)
driver.set_window_size(1350, 1000)
driver.get("https://sktorrent.eu")
if COOKIE_FILE.exists():
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies = json.load(f)
for c in cookies:
driver.add_cookie(c)
print("🍪 Cookies loaded.")
else:
print("⚠️ Cookie file not found login may be required.")
# ============================================================
# 3) requests.Session from Selenium cookies
# ============================================================
requests_session = requests.Session()
for ck in driver.get_cookies():
requests_session.cookies.set(ck["name"], ck["value"])
print("🔗 Requests session initialized.")
# ============================================================
# 4) Popup handler
# ============================================================
def close_popup_if_any():
try:
driver.execute_script("try { interstitialBox.closeit(); } catch(e) {}")
time.sleep(0.5)
except Exception:
pass
# ============================================================
# 5) Parse one torrent row (MODIFIED)
# ============================================================
def parse_row(cells):
# --- 1. INITIALIZE ---
torrent_hash = None
download_url = None
category = cells[0].text.strip()
try:
# --- 2. EXTRACT DOWNLOAD URL (Column 1) ---
download_a = cells[1].find_element(By.TAG_NAME, "a")
download_url = download_a.get_attribute("href")
parsed_dl = urlparse.urlparse(download_url)
dl_query = urlparse.parse_qs(parsed_dl.query)
torrent_filename = dl_query.get("f", ["unknown.torrent"])[0]
# --- 3. EXTRACT DETAILS & HASH (Column 2) ---
title_links = cells[2].find_elements(By.TAG_NAME, "a")
if not title_links:
return None
a_tag = title_links[0]
visible_name = a_tag.text.strip()
full_title = a_tag.get_attribute("title")
details_link = a_tag.get_attribute("href")
parsed = urlparse.urlparse(details_link)
query = urlparse.parse_qs(parsed.query)
if "id" not in query:
return None
torrent_hash = query["id"][0]
# --- 4. EXTRACT SIZE & DATE ---
text_block = cells[2].get_attribute("innerText")
text_block_clean = " ".join(text_block.split())
size_match = re.search(r"Velkost ([0-9\.]+ ?[KMG]B)", text_block_clean, re.IGNORECASE)
added_match = re.search(r"Pridany (.+?)(?:\sObrázok|$)", text_block_clean, re.IGNORECASE)
size_pretty = size_match.group(1) if size_match else None
added_pretty = added_match.group(1) if added_match else None
added_mysql = None
if added_pretty:
clean = added_pretty.replace(" o ", " ").strip()
parts = clean.split(" ")
if len(parts) >= 2:
date_part, time_part = parts[0], parts[1]
if len(time_part.split(":")) == 2: time_part += ":00"
try:
d, m, y = date_part.split("/")
added_mysql = f"{y}-{m}-{d} {time_part}"
except: pass
# --- 5. IMAGE & STATS ---
img_link = None
try:
image_a = cells[2].find_element(By.XPATH, ".//a[contains(text(),'Obrázok')]")
mouseover = image_a.get_attribute("onmouseover")
img_match = re.search(r"src=([^ ]+)", mouseover)
if img_match:
img_link = img_match.group(1).replace("'", "").strip()
if img_link.startswith("//"): img_link = "https:" + img_link
except: pass
seeders_number = int(cells[4].find_element(By.TAG_NAME, "a").text.strip())
seeders_link = cells[4].find_element(By.TAG_NAME, "a").get_attribute("href")
leechers_number = int(cells[5].find_element(By.TAG_NAME, "a").text.strip())
leechers_link = cells[5].find_element(By.TAG_NAME, "a").get_attribute("href")
# --- 6. DATABASE CHECK & DOWNLOAD ---
cursor.execute("SELECT torrent_content FROM torrents WHERE torrent_hash=%s", (torrent_hash,))
db_row = cursor.fetchone()
already_have_torrent = db_row is not None and db_row[0] is not None
torrent_content = None
if not already_have_torrent:
time.sleep(2)
try:
resp = requests_session.get(download_url, timeout=10)
resp.raise_for_status()
torrent_content = resp.content
except Exception as e:
print(f" ⚠️ Download failed for {visible_name}: {e}")
return {
"torrent_hash": torrent_hash,
"details_link": details_link,
"download_url": download_url,
"category": category,
"title_visible": visible_name,
"title_full": full_title,
"size_pretty": size_pretty,
"added_datetime": added_mysql,
"preview_image": img_link,
"seeders": seeders_number,
"seeders_link": seeders_link,
"leechers": leechers_number,
"leechers_link": leechers_link,
"torrent_filename": torrent_filename,
"torrent_content": torrent_content if not already_have_torrent else None,
"is_new_torrent": not already_have_torrent,
}
except Exception as e:
print(f"⚠️ parse_row logic failed: {e}")
return None
# ============================================================
# 6) INSERT SQL (MODIFIED)
# ============================================================
insert_sql = """
INSERT INTO torrents (
torrent_hash, details_link, download_url, category, title_visible, title_full,
size_pretty, added_datetime, preview_image,
seeders, seeders_link, leechers, leechers_link,
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, %(preview_image)s,
%(seeders)s, %(seeders_link)s, %(leechers)s, %(leechers_link)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);
"""
# Note: COALESCE(torrent_content, VALUES(torrent_content))
# keeps the old value if the new one is NULL,
# but updates it if the old one was NULL and the new one is binary.
# ============================================================
# 7) PROCESS ALL PAGES
# ============================================================
TOTAL_PAGES = 226
for page_num in range(0, TOTAL_PAGES):
current_url = f"{BASE_URL}&page={page_num}"
print(f"\n🌐 Loading Page Index {page_num} (Page {page_num + 1}/{TOTAL_PAGES})")
driver.get(current_url)
time.sleep(2)
close_popup_if_any()
# Find table rows
rows = driver.find_elements(By.CSS_SELECTOR, "table tr")
# FILTER: Only keep rows that have 7 columns AND a link in the 2nd column (index 1)
# This automatically discards headers and empty space rows.
real_rows = []
for r in rows:
cells = r.find_elements(By.TAG_NAME, "td")
if len(cells) == 7 and cells[1].find_elements(By.TAG_NAME, "a"):
real_rows.append(cells)
if not real_rows:
print("⚠️ No data rows found on this page. Ending loop.")
break
# === INSERT THIS LINE HERE ===
page_new_items = 0
# =============================
for cells in real_rows:
try:
data = parse_row(cells)
# ... rest of your logic ...
except Exception as e:
print(f"⚠️ parse_row failed: {e}")
continue
if not data: continue
processed_count += 1
if data["is_new_torrent"]:
new_torrent_count += 1
page_new_items += 1
new_titles.append(data["title_visible"])
print(f"💾 NEW: {data['title_visible']}")
else:
existing_torrent_count += 1
print(f"♻️ UPDATING: {data['title_visible']}")
cursor.execute(insert_sql, data)
# # If an entire page is old news, we can stop the deep crawl
# if page_new_items == 0 and page_num > 0:
# print("🛑 Page contained only known items. Sync complete.")
# break
time.sleep(1)
# ============================================================
# 8) SEND EMAIL REPORT
# ============================================================
RUN_END = datetime.datetime.now()
subject = f"SKTorrent run {RUN_START:%Y-%m-%d %H:%M}"
body = (
f"Run started: {RUN_START:%Y-%m-%d %H:%M:%S}\n"
f"Run finished: {RUN_END:%Y-%m-%d %H:%M:%S}\n\n"
f"Processed torrents: {processed_count}\n"
f"New torrents saved: {new_torrent_count}\n"
f"Existing torrents updated: {existing_torrent_count}\n"
)
if new_titles:
body += "\nNew torrents list:\n- " + "\n- ".join(new_titles)
send_mail(to="vladimir.buzalka@buzalka.cz", subject=subject, body=body, html=False)
print("📧 Email report sent.")
driver.quit()
print("🎉 DONE")

292
91 5threaddownloader.py Normal file
View File

@@ -0,0 +1,292 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
import re
import urllib.parse as urlparse
from pathlib import Path
import json
import requests
import datetime
import sys
import threading
from concurrent.futures import ThreadPoolExecutor
# Ensure this file exists in your directory
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIGURATION
# ============================================================
TOTAL_PAGES = 226
THREADS = 5
COOKIE_FILE = Path("sktorrent_cookies.json")
# Database settings
DB_CONFIG = {
"host": "192.168.1.50",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
BASE_URL = (
"https://sktorrent.eu/torrent/torrents.php"
"?active=0&category=24&order=data&by=DESC&zaner=&jazyk="
)
# Global counters for reporting (Thread-safe lock needed)
stats_lock = threading.Lock()
stats = {
"processed": 0,
"new": 0,
"existing": 0,
"new_titles": []
}
# ============================================================
# 1) WORKER FUNCTION (Runs inside each thread)
# ============================================================
def process_page_chunk(page_indices, thread_id):
"""
This function creates its OWN browser and OWN database connection.
It processes the specific list of page numbers assigned to it.
"""
print(f"🧵 [Thread-{thread_id}] Starting. Assigned {len(page_indices)} pages.")
# --- A. Setup Independent DB Connection ---
try:
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor()
except Exception as e:
print(f"❌ [Thread-{thread_id}] DB Connection failed: {e}")
return
# --- B. Setup Independent Selenium Driver ---
chrome_options = Options()
# HEADLESS MODE is safer for 5 threads to avoid popping up 5 windows
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--disable-notifications")
chrome_options.add_argument("--disable-popup-blocking")
chrome_options.add_argument("--disable-extensions")
chrome_options.add_argument("--log-level=3") # Reduce noise
driver = webdriver.Chrome(options=chrome_options)
driver.set_window_size(1350, 1000)
# --- C. Login / Cookies ---
driver.get("https://sktorrent.eu")
if COOKIE_FILE.exists():
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies = json.load(f)
for c in cookies:
driver.add_cookie(c)
# --- D. Requests Session ---
requests_session = requests.Session()
for ck in driver.get_cookies():
requests_session.cookies.set(ck["name"], ck["value"])
# --- E. Helper: Parse Row (Local scope) ---
def parse_row(cells):
try:
category = cells[0].text.strip()
# Download URL
download_a = cells[1].find_element(By.TAG_NAME, "a")
download_url = download_a.get_attribute("href")
parsed_dl = urlparse.urlparse(download_url)
dl_query = urlparse.parse_qs(parsed_dl.query)
torrent_filename = dl_query.get("f", ["unknown.torrent"])[0]
# Details & Hash
title_links = cells[2].find_elements(By.TAG_NAME, "a")
if not title_links: return None
a_tag = title_links[0]
visible_name = a_tag.text.strip()
full_title = a_tag.get_attribute("title")
details_link = a_tag.get_attribute("href")
parsed = urlparse.urlparse(details_link)
query = urlparse.parse_qs(parsed.query)
if "id" not in query: return None
torrent_hash = query["id"][0]
# Size & Date
text_block = cells[2].get_attribute("innerText")
clean_text = " ".join(text_block.split())
size_match = re.search(r"Velkost ([0-9\.]+ ?[KMG]B)", clean_text, re.IGNORECASE)
added_match = re.search(r"Pridany (.+?)(?:\sObrázok|$)", clean_text, re.IGNORECASE)
size_pretty = size_match.group(1) if size_match else None
added_mysql = None
if added_match:
clean = added_match.group(1).replace(" o ", " ").strip()
parts = clean.split(" ")
if len(parts) >= 2:
d, m, y = parts[0].split("/")
t = parts[1] + ":00" if len(parts[1].split(":")) == 2 else parts[1]
try:
added_mysql = f"{y}-{m}-{d} {t}"
except:
pass
# Image
img_link = None
try:
img_a = cells[2].find_element(By.XPATH, ".//a[contains(text(),'Obrázok')]")
img_src = re.search(r"src=([^ ]+)", img_a.get_attribute("onmouseover"))
if img_src:
img_link = img_src.group(1).replace("'", "").strip()
if img_link.startswith("//"): img_link = "https:" + img_link
except:
pass
# Stats
seeders = int(cells[4].find_element(By.TAG_NAME, "a").text.strip())
seeders_link = cells[4].find_element(By.TAG_NAME, "a").get_attribute("href")
leechers = int(cells[5].find_element(By.TAG_NAME, "a").text.strip())
leechers_link = cells[5].find_element(By.TAG_NAME, "a").get_attribute("href")
# Check DB
cursor.execute("SELECT torrent_content FROM torrents WHERE torrent_hash=%s", (torrent_hash,))
row = cursor.fetchone()
already_have_file = row is not None and row[0] is not None
content = None
if not already_have_file:
# Politeness sleep only if downloading
time.sleep(1)
try:
r = requests_session.get(download_url, timeout=10)
r.raise_for_status()
content = r.content
except:
pass
return {
"torrent_hash": torrent_hash, "details_link": details_link, "download_url": download_url,
"category": category, "title_visible": visible_name, "title_full": full_title,
"size_pretty": size_pretty, "added_datetime": added_mysql, "preview_image": img_link,
"seeders": seeders, "seeders_link": seeders_link, "leechers": leechers, "leechers_link": leechers_link,
"torrent_filename": torrent_filename, "torrent_content": content,
"is_new_torrent": not already_have_file
}
except Exception:
return None
# --- F. Loop through Assigned Pages ---
for page_num in page_indices:
url = f"{BASE_URL}&page={page_num}"
print(f" 🔄 [Thread-{thread_id}] Scraping Page {page_num}")
try:
driver.get(url)
# Close popup (simplified JS)
driver.execute_script("try { interstitialBox.closeit(); } catch(e) {}")
# Row Filtering
rows = driver.find_elements(By.CSS_SELECTOR, "table tr")
real_rows = []
for r in rows:
cs = r.find_elements(By.TAG_NAME, "td")
if len(cs) == 7 and cs[1].find_elements(By.TAG_NAME, "a"):
real_rows.append(cs)
if not real_rows:
print(f" ⚠️ [Thread-{thread_id}] Page {page_num} empty.")
continue
# Process Rows
for cells in real_rows:
data = parse_row(cells)
if not data: continue
# Update Global Stats safely
with stats_lock:
stats["processed"] += 1
if data["is_new_torrent"]:
stats["new"] += 1
stats["new_titles"].append(data["title_visible"])
else:
stats["existing"] += 1
# Insert SQL
sql = """
INSERT INTO torrents (
torrent_hash, details_link, download_url, category, title_visible, title_full,
size_pretty, added_datetime, preview_image,
seeders, seeders_link, leechers, leechers_link,
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, %(preview_image)s,
%(seeders)s, %(seeders_link)s, %(leechers)s, %(leechers_link)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);
"""
cursor.execute(sql, data)
except Exception as e:
print(f" 💥 [Thread-{thread_id}] Error on page {page_num}: {e}")
# Cleanup
driver.quit()
db.close()
print(f"🏁 [Thread-{thread_id}] Finished assigned pages.")
# ============================================================
# 2) MAIN EXECUTION
# ============================================================
if __name__ == "__main__":
RUN_START = datetime.datetime.now()
print(f"🚀 Starting Multithreaded Scraper with {THREADS} threads...")
# 1. Distribute pages among threads
# Example: If 226 pages and 5 threads, each gets ~45 pages
all_pages = list(range(TOTAL_PAGES))
chunk_size = len(all_pages) // THREADS + 1
chunks = [all_pages[i:i + chunk_size] for i in range(0, len(all_pages), chunk_size)]
# 2. Start Threads
with ThreadPoolExecutor(max_workers=THREADS) as executor:
futures = []
for i, page_chunk in enumerate(chunks):
if page_chunk: # Only start if chunk is not empty
futures.append(executor.submit(process_page_chunk, page_chunk, i + 1))
# Wait for all to finish
for f in futures:
f.result()
# 3. Final Report
RUN_END = datetime.datetime.now()
print("\n✅ All threads completed.")
body = (
f"Run started: {RUN_START:%Y-%m-%d %H:%M:%S}\n"
f"Run finished: {RUN_END:%Y-%m-%d %H:%M:%S}\n\n"
f"Processed torrents: {stats['processed']}\n"
f"New torrents saved: {stats['new']}\n"
f"Existing torrents updated: {stats['existing']}\n"
)
if stats["new_titles"]:
body += "\nNew torrents list:\n- " + "\n- ".join(stats["new_titles"])
send_mail(to="vladimir.buzalka@buzalka.cz", subject=f"SKTorrent Multi-Thread Run", body=body, html=False)
print("📧 Email report sent.")

View File

@@ -0,0 +1,212 @@
import pymysql
import requests
import json
import time
import random
import os
import re
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from threading import Lock
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# ============================================================
# KONFIGURACE
# ============================================================
DB_CONFIG = {
"host": "192.168.1.50",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
COOKIE_FILE = Path("sktorrent_cookies.json")
BACKUP_DIR = "saved_torrents" # Adresář pro lokální zálohu
THREADS = 5 # Počet vláken
# Globální zámek pro výpisy do konzole, aby se nepřepisovaly
print_lock = Lock()
stats = {"fixed": 0, "failed": 0, "saved_to_disk": 0}
# ============================================================
# POMOCNÉ FUNKCE
# ============================================================
def sanitize_filename(name):
"""Odstraní z názvu souboru nepovolené znaky"""
# Povolíme jen písmena, čísla, tečky, pomlčky a mezery
clean = re.sub(r'[^\w\s\.-]', '', name)
return clean.strip()[:100] # Ořízneme na 100 znaků pro jistotu
def ensure_backup_dir():
"""Vytvoří adresář pro torrenty, pokud neexistuje"""
if not os.path.exists(BACKUP_DIR):
os.makedirs(BACKUP_DIR)
print(f"📁 Vytvořen adresář pro zálohu: {os.path.abspath(BACKUP_DIR)}")
def get_browser_identity():
"""
Spustí Selenium (Chrome) JEN JEDNOU, aby získal validní
User-Agent a čerstvé Cookies pro threads.
"""
print("🤖 Startuji Selenium pro získání identity prohlížeče...")
opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=opts)
# Jdeme na web nastavit doménu pro cookies
driver.get("https://sktorrent.eu")
# Načteme cookies ze souboru
if COOKIE_FILE.exists():
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies_list = json.load(f)
for c in cookies_list:
driver.add_cookie(c)
driver.refresh()
time.sleep(2)
# Exportujeme identitu
user_agent = driver.execute_script("return navigator.userAgent;")
browser_cookies = driver.get_cookies()
driver.quit()
print("✅ Identita získána.")
return user_agent, browser_cookies
# ============================================================
# WORKER (Pracovní vlákno)
# ============================================================
def worker_task(rows_chunk, thread_id, user_agent, cookies_list):
"""
Tato funkce běží v každém vlákně zvlášť.
"""
# 1. Vytvoření vlastní Session pro toto vlákno
session = requests.Session()
session.headers.update({"User-Agent": user_agent})
for c in cookies_list:
session.cookies.set(c['name'], c['value'])
# 2. Vlastní připojení k DB (nutné pro thread-safety)
try:
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor()
except Exception as e:
with print_lock:
print(f"❌ [Thread-{thread_id}] Chyba DB připojení: {e}")
return
for row in rows_chunk:
t_hash, url, title = row
# Ochrana: krátká náhodná pauza, aby 5 vláken nezabilo server
time.sleep(random.uniform(0.5, 2.0))
try:
# Stažení
resp = session.get(url, timeout=15)
if resp.status_code == 403:
with print_lock:
print(f"⛔ [Thread-{thread_id}] 403 Forbidden! {title[:20]}...")
stats["failed"] += 1
continue
resp.raise_for_status()
content = resp.content
if len(content) > 100:
# A) Uložit do DB (BLOB)
sql = "UPDATE torrents SET torrent_content = %s WHERE torrent_hash = %s"
cursor.execute(sql, (content, t_hash))
# B) Uložit na DISK (Soubor)
clean_name = sanitize_filename(title)
# Přidáme kousek hashe do názvu, aby se nepřepsaly soubory se stejným jménem
filename = f"{clean_name}_{t_hash[:6]}.torrent"
file_path = os.path.join(BACKUP_DIR, filename)
with open(file_path, "wb") as f:
f.write(content)
with print_lock:
print(f"✅ [Thread-{thread_id}] OK: {clean_name}")
stats["fixed"] += 1
stats["saved_to_disk"] += 1
else:
with print_lock:
print(f"⚠️ [Thread-{thread_id}] Prázdný soubor: {title}")
stats["failed"] += 1
except Exception as e:
with print_lock:
print(f"❌ [Thread-{thread_id}] Chyba: {title[:20]}... -> {e}")
stats["failed"] += 1
db.close()
with print_lock:
print(f"🏁 [Thread-{thread_id}] Dokončil práci.")
# ============================================================
# HLAVNÍ LOOP
# ============================================================
if __name__ == "__main__":
ensure_backup_dir()
# 1. Získat data z DB
print("🔍 Načítám seznam chybějících souborů z DB...")
main_db = pymysql.connect(**DB_CONFIG)
with main_db.cursor() as c:
# Hledáme ty, co mají URL, ale nemají obsah
c.execute(
"SELECT torrent_hash, download_url, title_visible FROM torrents WHERE torrent_content IS NULL AND download_url IS NOT NULL")
all_rows = c.fetchall()
main_db.close()
total = len(all_rows)
print(f"📋 K opravě: {total} položek.")
if total == 0:
print("🎉 Není co opravovat.")
exit()
# 2. Získat "Super Identitu" přes Selenium (jen jednou)
u_agent, browser_cookies = get_browser_identity()
# 3. Rozdělit práci pro 5 vláken
chunk_size = total // THREADS + 1
chunks = [all_rows[i:i + chunk_size] for i in range(0, total, chunk_size)]
print(f"🚀 Spouštím {THREADS} vláken (ukládání do DB + do složky '{BACKUP_DIR}')...")
# 4. Spustit multithreading
with ThreadPoolExecutor(max_workers=THREADS) as executor:
futures = []
for i, chunk in enumerate(chunks):
if chunk:
# Každému vláknu předáme kus práce + identitu prohlížeče
futures.append(executor.submit(worker_task, chunk, i + 1, u_agent, browser_cookies))
# Čekáme na dokončení
for f in futures:
f.result()
print("\n" + "=" * 40)
print(f"🏁 DOKONČENO")
print(f"✅ Opraveno v DB: {stats['fixed']}")
print(f"💾 Uloženo na disk: {stats['saved_to_disk']}")
print(f"❌ Chyby: {stats['failed']}")
print(f"📁 Soubory najdeš v: {os.path.abspath(BACKUP_DIR)}")
print("=" * 40)

View File

@@ -0,0 +1,133 @@
import pymysql
import requests
import json
import time
import random
import os
import re
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# ============================================================
# KONFIGURACE
# ============================================================
DB_CONFIG = {
"host": "192.168.1.50",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
COOKIE_FILE = Path("sktorrent_cookies.json")
BACKUP_DIR = "saved_torrents"
# ============================================================
# POMOCNÉ FUNKCE
# ============================================================
def sanitize_filename(name):
clean = re.sub(r'[^\w\s\.-]', '', name)
return clean.strip()[:100]
def get_browser_identity():
print("🤖 Startuji Selenium (Single Thread Mode)...")
opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=opts)
driver.get("https://sktorrent.eu")
if COOKIE_FILE.exists():
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies_list = json.load(f)
for c in cookies_list:
driver.add_cookie(c)
driver.refresh()
time.sleep(2)
user_agent = driver.execute_script("return navigator.userAgent;")
browser_cookies = driver.get_cookies()
driver.quit()
return user_agent, browser_cookies
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
if not os.path.exists(BACKUP_DIR):
os.makedirs(BACKUP_DIR)
# 1. Načíst zbývající chyby
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor()
cursor.execute(
"SELECT torrent_hash, download_url, title_visible FROM torrents WHERE torrent_content IS NULL AND download_url IS NOT NULL")
rows = cursor.fetchall()
print(f"📋 Zbývá opravit: {len(rows)} položek.")
if not rows:
print("🎉 Hotovo! Vše je staženo.")
exit()
# 2. Získat identitu
ua, cookies = get_browser_identity()
session = requests.Session()
session.headers.update({"User-Agent": ua})
for c in cookies:
session.cookies.set(c['name'], c['value'])
# 3. Pomalá smyčka (1 vlákno)
success = 0
dead_links = 0
print("🚀 Spouštím jemné dočištění...")
for i, row in enumerate(rows):
t_hash, url, title = row
print(f"[{i + 1}/{len(rows)}] {title[:50]}...", end=" ")
try:
# Delší pauza pro stabilitu
time.sleep(random.uniform(1.5, 3.0))
resp = session.get(url, timeout=20) # Delší timeout
if resp.status_code == 404:
print("❌ 404 Nenalezeno (soubor na serveru neexistuje)")
dead_links += 1
continue
if resp.status_code != 200:
print(f"❌ Chyba {resp.status_code}")
continue
content = resp.content
if len(content) > 100:
# DB
cursor.execute("UPDATE torrents SET torrent_content = %s WHERE torrent_hash = %s", (content, t_hash))
# Disk
fname = f"{sanitize_filename(title)}_{t_hash[:6]}.torrent"
with open(os.path.join(BACKUP_DIR, fname), "wb") as f:
f.write(content)
print("✅ OK")
success += 1
else:
print("⚠️ Prázdný soubor")
except Exception as e:
print(f"❌ Selhalo: {e}")
db.close()
print("\n" + "=" * 30)
print(f"🏁 FINÁLE: Opraveno {success} z {len(rows)}")
if dead_links > 0:
print(f"💀 Mrtvé odkazy (404): {dead_links} (ty už opravit nejdou)")

View File

@@ -0,0 +1,158 @@
import pymysql
import bencodepy
import os
from pathlib import Path
# ============================================================
# CONFIGURATION
# ============================================================
# Your network path (Use raw string r"..." for backslashes)
# PHYSICAL_DIR = Path(r"\\tower\torrents\downloads")
PHYSICAL_DIR = Path(r"\\tower1\#Colddata\Porno")
DB_CONFIG = {
"host": "192.168.1.50",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
# ============================================================
# HELPER FUNCTIONS
# ============================================================
def decode_bytes(b):
"""
Decodes bytes from Bencode into a string.
Tries UTF-8 first, then common fallbacks.
"""
if isinstance(b, str): return b
encodings = ['utf-8', 'windows-1250', 'latin-1', 'cp1252']
for enc in encodings:
try:
return b.decode(enc)
except:
continue
return b.decode('utf-8', errors='ignore')
def check_torrent_in_filesystem(torrent_blob, root_path):
"""
Parses the binary BLOB, calculates expected paths,
and checks if they exist in the root_path.
"""
try:
# Decode the binary BLOB
data = bencodepy.decode(torrent_blob)
info = data.get(b'info')
if not info: return False
# Get the name of the root file/folder defined in the torrent
name = decode_bytes(info.get(b'name'))
# Calculate expected location
target_path = root_path / name
# 1. Check if the main path exists
if not target_path.exists():
return False
# 2. Size Verification (Basic)
# If it's a single file
if b'files' not in info:
expected_size = info[b'length']
real_size = target_path.stat().st_size
# Allow 1% variance or 1KB (sometimes filesystems vary slightly)
if abs(real_size - expected_size) < 4096:
return True
return False
# If it's a multi-file torrent (folder)
else:
# If the folder exists, we assume it's mostly good,
# but let's check at least one file inside to be sure it's not empty.
files = info[b'files']
if not files: return True # Empty folder torrent? rare but possible.
# Check the first file in the list
first_file_path = target_path.joinpath(*[decode_bytes(p) for p in files[0][b'path']])
return first_file_path.exists()
except Exception as e:
# If Bencode fails or path is weird
return False
# ============================================================
# MAIN EXECUTION
# ============================================================
if __name__ == "__main__":
if not PHYSICAL_DIR.exists():
print(f"❌ ERROR: Cannot access path: {PHYSICAL_DIR}")
print("Make sure the drive is mapped or the network path is accessible.")
exit()
print(f"📂 Scanning storage: {PHYSICAL_DIR}")
print("🚀 Connecting to Database...")
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor()
# 1. Get all torrents that have content (BLOB)
# We only select ID and Content to keep memory usage reasonable
cursor.execute(
"SELECT torrent_hash, title_visible, torrent_content FROM torrents WHERE torrent_content IS NOT NULL")
rows = cursor.fetchall()
total = len(rows)
print(f"📋 Analysing {total} torrents from database against disk files...")
found_count = 0
missing_count = 0
# 2. Iterate and Check
updates = [] # Store successful hashes to batch update later
for index, row in enumerate(rows):
t_hash, title, blob = row
is_downloaded = check_torrent_in_filesystem(blob, PHYSICAL_DIR)
if is_downloaded:
found_count += 1
updates.append(t_hash)
# Print only every 50th line to reduce clutter, or if found
# print(f"✅ Found: {title[:50]}")
else:
missing_count += 1
if index % 100 == 0:
print(f" Processed {index}/{total} ... (Found: {found_count})")
# 3. Batch Update Database
print(f"\n💾 Updating Database: Marking {len(updates)} torrents as 'physical_exists = 1'...")
# Reset everything to 0 first (in case you deleted files since last run)
cursor.execute("UPDATE torrents SET physical_exists = 0")
if updates:
# Update in chunks of 1000 to be safe
chunk_size = 1000
for i in range(0, len(updates), chunk_size):
chunk = updates[i:i + chunk_size]
format_strings = ','.join(['%s'] * len(chunk))
cursor.execute(f"UPDATE torrents SET physical_exists = 1 WHERE torrent_hash IN ({format_strings})",
tuple(chunk))
db.commit()
db.close()
print("\n" + "=" * 40)
print(f"🏁 SCAN COMPLETE")
print(f"✅ Physically Available: {found_count}")
print(f"❌ Missing / Not Downloaded: {missing_count}")
print(f"📊 Completion Rate: {int((found_count / total) * 100)}%")
print("=" * 40)

91
EmailMessagingGraph.py Normal file
View File

@@ -0,0 +1,91 @@
"""
EmailMessagingGraph.py
----------------------
Private Microsoft Graph mail sender
Application permissions, shared mailbox
"""
import msal
import requests
from functools import lru_cache
from typing import Union, List
# =========================
# PRIVATE CONFIG (ONLY YOU)
# =========================
TENANT_ID = "7d269944-37a4-43a1-8140-c7517dc426e9"
CLIENT_ID = "4b222bfd-78c9-4239-a53f-43006b3ed07f"
CLIENT_SECRET = "Txg8Q~MjhocuopxsJyJBhPmDfMxZ2r5WpTFj1dfk"
SENDER = "reports@buzalka.cz"
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPE = ["https://graph.microsoft.com/.default"]
@lru_cache(maxsize=1)
def _get_token() -> str:
app = msal.ConfidentialClientApplication(
CLIENT_ID,
authority=AUTHORITY,
client_credential=CLIENT_SECRET,
)
token = app.acquire_token_for_client(scopes=SCOPE)
if "access_token" not in token:
raise RuntimeError(f"Graph auth failed: {token}")
return token["access_token"]
def send_mail(
to: Union[str, List[str]],
subject: str,
body: str,
*,
html: bool = False,
):
"""
Send email via Microsoft Graph.
:param to: email or list of emails
:param subject: subject
:param body: email body
:param html: True = HTML, False = plain text
"""
if isinstance(to, str):
to = [to]
payload = {
"message": {
"subject": subject,
"body": {
"contentType": "HTML" if html else "Text",
"content": body,
},
"toRecipients": [
{"emailAddress": {"address": addr}} for addr in to
],
},
"saveToSentItems": "true",
}
headers = {
"Authorization": f"Bearer {_get_token()}",
"Content-Type": "application/json",
}
r = requests.post(
f"https://graph.microsoft.com/v1.0/users/{SENDER}/sendMail",
headers=headers,
json=payload,
timeout=30,
)
if r.status_code != 202:
raise RuntimeError(
f"sendMail failed [{r.status_code}]: {r.text}"
)

View File

@@ -1,24 +0,0 @@
from functions import check_insurance
import time
import fdb
# MEDICUS local CFG (table already created as per previous DDL)
MEDICUS_CFG = dict(
dsn=r"192.168.1.4:z:\medicus 3\data\medicus.fdb",
user="SYSDBA",
password="masterkey",
charset="win1250"
)
conn=fdb.connect(**MEDICUS_CFG)
cur=conn.cursor()
cur.execute("select rodcis, prijmeni, jmeno from kar where rodcis starting with '8'")
rows=cur.fetchall()
print(len(rows))
for row in rows:
print(row[0], row[1],row[2])
print(check_insurance(row[0]))
time.sleep(.25)

Binary file not shown.

342
Reporter_ReadNewTorrents.py Normal file
View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
import re
import urllib.parse as urlparse
from pathlib import Path
import json
import requests
import datetime
import sys
from EmailMessagingGraph import send_mail
# ============================================================
# RUNTIME INFO
# ============================================================
RUN_START = datetime.datetime.now()
processed_count = 0
new_torrent_count = 0
existing_torrent_count = 0
new_titles = []
print(f"🕒 Run started at {RUN_START:%Y-%m-%d %H:%M:%S}")
sys.stdout.flush()
# ============================================================
# 1) MySQL CONNECTION
# ============================================================
db = pymysql.connect(
host="192.168.1.50",
port=3306,
user="root",
password="Vlado9674+",
database="torrents",
charset="utf8mb4",
autocommit=True,
)
cursor = db.cursor()
# ============================================================
# 2) Selenium setup
# ============================================================
COOKIE_FILE = Path("sktorrent_cookies.json")
START_URL = (
"https://sktorrent.eu/torrent/torrents.php"
"?search=&category=24&zaner=&jazyk=&active=0"
)
chrome_options = Options()
chrome_options.add_argument("--start-maximized")
chrome_options.add_argument("--disable-notifications")
chrome_options.add_argument("--disable-popup-blocking")
chrome_options.add_argument("--disable-extensions")
driver = webdriver.Chrome(options=chrome_options)
driver.set_window_position(380, 50)
driver.set_window_size(1350, 1000)
driver.get("https://sktorrent.eu")
if COOKIE_FILE.exists():
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies = json.load(f)
for c in cookies:
driver.add_cookie(c)
print("🍪 Cookies loaded.")
else:
print("⚠️ Cookie file not found login may be required.")
# ============================================================
# 3) requests.Session from Selenium cookies
# ============================================================
requests_session = requests.Session()
for ck in driver.get_cookies():
requests_session.cookies.set(ck["name"], ck["value"])
print("🔗 Requests session initialized.")
# ============================================================
# 4) Popup handler
# ============================================================
def close_popup_if_any():
try:
driver.execute_script("try { interstitialBox.closeit(); } catch(e) {}")
time.sleep(0.5)
except Exception:
pass
# ============================================================
# 5) Parse one torrent row
# ============================================================
def parse_row(cells):
category = cells[0].text.strip()
try:
download_a = cells[1].find_element(By.TAG_NAME, "a")
download_link = download_a.get_attribute("href")
except:
return None
parsed_dl = urlparse.urlparse(download_link)
dl_query = urlparse.parse_qs(parsed_dl.query)
torrent_filename = dl_query.get("f", ["unknown.torrent"])[0]
title_links = cells[2].find_elements(By.TAG_NAME, "a")
if not title_links:
return None
a_tag = title_links[0]
visible_name = a_tag.text.strip()
full_title = a_tag.get_attribute("title")
details_link = a_tag.get_attribute("href")
parsed = urlparse.urlparse(details_link)
query = urlparse.parse_qs(parsed.query)
if "id" not in query:
return None
torrent_hash = query["id"][0]
text_block = cells[2].get_attribute("innerText")
text_block_clean = " ".join(text_block.split())
size_match = re.search(r"Velkost ([0-9\.]+ ?[KMG]B)", text_block_clean, re.IGNORECASE)
added_match = re.search(r"Pridany (.+?)(?:\sObrázok|$)", text_block_clean, re.IGNORECASE)
size_pretty = size_match.group(1) if size_match else None
added_pretty = added_match.group(1) if added_match else None
# ======================================================
# EXACT DATE PROCESSING COPIED 1:1 FROM YOUR FILE
# ======================================================
added_mysql = None
if added_pretty:
# "29/11/2025 o 02:29" → "29/11/2025 02:29"
clean = added_pretty.replace(" o ", " ").strip()
parts = clean.split(" ")
date_part = parts[0]
time_part = parts[1] if len(parts) > 1 else "00:00:00"
# pokud chybí sekundy, přidej
if len(time_part.split(":")) == 2:
time_part += ":00"
day, month, year = date_part.split("/")
added_mysql = f"{year}-{month}-{day} {time_part}"
# ======================================================
# Image preview
# ======================================================
img_link = None
try:
image_a = cells[2].find_element(
By.XPATH,
".//a[contains(text(),'Obrázok')]"
)
mouseover = image_a.get_attribute("onmouseover")
img_match = re.search(r"src=([^ ]+)", mouseover)
if img_match:
img_link = img_match.group(1).replace("'", "").strip()
if img_link.startswith("//"):
img_link = "https:" + img_link
except:
pass
seeders_a = cells[4].find_element(By.TAG_NAME, "a")
seeders_number = int(seeders_a.text.strip())
seeders_link = seeders_a.get_attribute("href")
leechers_a = cells[5].find_element(By.TAG_NAME, "a")
leechers_number = int(leechers_a.text.strip())
leechers_link = leechers_a.get_attribute("href")
cursor.execute(
"SELECT torrent_content FROM torrents WHERE torrent_hash=%s",
(torrent_hash,),
)
row = cursor.fetchone()
already_have_torrent = row is not None and row[0] is not None
torrent_content = None
if not already_have_torrent:
time.sleep(3)
try:
resp = requests_session.get(download_link)
resp.raise_for_status()
torrent_content = resp.content
except:
torrent_content = None
return {
"torrent_hash": torrent_hash,
"details_link": details_link,
"category": category,
"title_visible": visible_name,
"title_full": full_title,
"size_pretty": size_pretty,
"added_datetime": added_mysql,
"preview_image": img_link,
"seeders": seeders_number,
"seeders_link": seeders_link,
"leechers": leechers_number,
"leechers_link": leechers_link,
"torrent_filename": torrent_filename,
"torrent_content": torrent_content if not already_have_torrent else None,
"is_new_torrent": not already_have_torrent,
}
# ============================================================
# 6) INSERT SQL
# ============================================================
insert_sql = """
INSERT INTO torrents (
torrent_hash, details_link, category, title_visible, title_full,
size_pretty, added_datetime, preview_image,
seeders, seeders_link, leechers, leechers_link,
torrent_filename, torrent_content
) VALUES (
%(torrent_hash)s, %(details_link)s, %(category)s, %(title_visible)s, %(title_full)s,
%(size_pretty)s, %(added_datetime)s, %(preview_image)s,
%(seeders)s, %(seeders_link)s, %(leechers)s, %(leechers_link)s,
%(torrent_filename)s, %(torrent_content)s
)
ON DUPLICATE KEY UPDATE
details_link = VALUES(details_link),
category = VALUES(category),
title_visible = VALUES(title_visible),
title_full = VALUES(title_full),
size_pretty = VALUES(size_pretty),
added_datetime = VALUES(added_datetime),
preview_image = VALUES(preview_image),
seeders = VALUES(seeders),
seeders_link = VALUES(seeders_link),
leechers = VALUES(leechers),
leechers_link = VALUES(leechers_link),
torrent_filename = VALUES(torrent_filename),
torrent_content = COALESCE(VALUES(torrent_content), torrent_content);
"""
# ============================================================
# 7) PROCESS FIRST PAGE ONLY
# ============================================================
print("\n🌐 Loading FIRST page")
driver.get(START_URL)
time.sleep(2)
close_popup_if_any()
rows = driver.find_elements(By.CSS_SELECTOR, "table tr")
real_rows = [
r.find_elements(By.TAG_NAME, "td")
for r in rows
if len(r.find_elements(By.TAG_NAME, "td")) == 7
]
print(f"📄 Found {len(real_rows)} torrent rows")
for cells in real_rows:
try:
data = parse_row(cells)
except Exception as e:
print(f"⚠️ parse_row failed: {e}")
continue
if not data:
continue
processed_count += 1
if data["is_new_torrent"]:
new_torrent_count += 1
new_titles.append(data["title_visible"])
else:
existing_torrent_count += 1
print("💾 Saving:", data["title_visible"])
cursor.execute(insert_sql, data)
# ============================================================
# 8) SEND EMAIL REPORT
# ============================================================
RUN_END = datetime.datetime.now()
subject = f"SKTorrent hourly run {RUN_START:%Y-%m-%d %H:%M}"
lines = [
f"Run started: {RUN_START:%Y-%m-%d %H:%M:%S}",
f"Run finished: {RUN_END:%Y-%m-%d %H:%M:%S}",
"",
f"Processed torrents: {processed_count}",
f"New torrent files downloaded: {new_torrent_count}",
f"Already known torrents: {existing_torrent_count}",
]
if new_titles:
lines.append("")
lines.append("New torrents:")
for t in new_titles:
lines.append(f"- {t}")
body = "\n".join(lines)
send_mail(
to="vladimir.buzalka@buzalka.cz",
subject=subject,
body=body,
html=False,
)
print("📧 Email report sent.")
driver.quit()
print("🎉 DONE")

View File

@@ -0,0 +1,337 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
import pymysql
import qbittorrentapi
import bencodepy
from EmailMessagingGraph import send_mail
# ==============================
# ⚙ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
QBT_CONFIG = {
"host": "192.168.1.76",
"port": 8080,
"username": "admin",
"password": "adminadmin",
}
MAX_ACTIVE_DOWNLOADS = 10
DEAD_TORRENT_MINUTES = 60
DEFAULT_SAVE_PATH = None
MAIL_TO = "vladimir.buzalka@buzalka.cz"
MAX_LIST_ITEMS = 50 # cap lists in email
# ==============================
# 🧮 RUNTIME STATS + LISTS
# ==============================
RUN_START = datetime.now()
stat_synced = 0
stat_completed = 0
stat_dead = 0
stat_enqueued = 0
deleted_completed = [] # list[str]
deleted_dead = [] # list[str]
added_new = [] # list[str]
active_downloading = [] # list[str]
# ==============================
# 🔧 CONNECT
# ==============================
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor(pymysql.cursors.DictCursor)
qb = qbittorrentapi.Client(**QBT_CONFIG)
try:
qb.auth_log_in()
print("✅ Connected to qBittorrent.")
except Exception as e:
raise SystemExit(f"❌ Could not connect to qBittorrent: {e}")
# ==============================
# 🧪 TORRENT VALIDATION
# ==============================
def is_valid_torrent(blob: bytes) -> bool:
try:
data = bencodepy.decode(blob)
return isinstance(data, dict) and b"info" in data
except Exception:
return False
# ==============================
# 🔄 SYNC FROM QB → DB
# ==============================
def sync_qb_to_db():
global stat_synced
torrents = qb.torrents_info()
stat_synced = len(torrents)
for t in torrents:
completion_dt = None
if getattr(t, "completion_on", 0):
try:
completion_dt = datetime.fromtimestamp(t.completion_on)
except Exception:
pass
cursor.execute("""
UPDATE torrents
SET qb_added = 1,
qb_hash = COALESCE(qb_hash, %s),
qb_state = %s,
qb_progress = %s,
qb_savepath = %s,
qb_completed_datetime =
IF(%s IS NOT NULL AND qb_completed_datetime IS NULL, %s, qb_completed_datetime),
qb_last_update = NOW()
WHERE qb_hash = %s OR torrent_hash = %s
""", (
t.hash,
t.state,
float(t.progress) * 100.0,
getattr(t, "save_path", None),
completion_dt,
completion_dt,
t.hash,
t.hash,
))
# ==============================
# 🧹 HANDLE COMPLETED + DEAD
# ==============================
def handle_completed_and_dead():
global stat_completed, stat_dead
torrents = qb.torrents_info()
for t in torrents:
t_hash = t.hash
state = t.state
progress = float(t.progress)
# ✔ COMPLETED
if progress >= 1.0 or state in {"completed", "uploading", "stalledUP", "queuedUP"}:
stat_completed += 1
deleted_completed.append(t.name)
try:
qb.torrents_delete(torrent_hashes=t_hash, delete_files=False)
except Exception as e:
# keep name in report; just note error in DB state if you want later
print(f"⚠️ delete (keep data) failed for {t.name}: {e}")
cursor.execute("""
UPDATE torrents
SET qb_state='completed',
qb_progress=100,
qb_completed_datetime=NOW(),
qb_last_update=NOW()
WHERE qb_hash=%s OR torrent_hash=%s
""", (t_hash, t_hash))
continue
# ❌ DEAD (never seen_complete)
try:
props = qb.torrents_properties(t_hash)
except Exception:
continue
if getattr(props, "last_seen", 0) == -1:
added_dt = getattr(t, "added_on", 0)
if added_dt:
if datetime.now() - datetime.fromtimestamp(added_dt) > timedelta(minutes=DEAD_TORRENT_MINUTES):
stat_dead += 1
deleted_dead.append(t.name)
try:
qb.torrents_delete(torrent_hashes=t_hash, delete_files=True)
except Exception as e:
print(f"⚠️ delete (files) failed for {t.name}: {e}")
cursor.execute("""
UPDATE torrents
SET qb_state='dead',
qb_last_update=NOW()
WHERE qb_hash=%s OR torrent_hash=%s
""", (t_hash, t_hash))
# ==============================
# 📊 ACTIVE DOWNLOADS
# ==============================
def count_active_downloads():
return sum(1 for t in qb.torrents_info() if float(t.progress) < 1.0)
def snapshot_active_downloading():
"""
Capture current actively downloading torrents (progress < 100%).
"""
active = []
for t in qb.torrents_info():
prog = float(t.progress)
if prog < 1.0:
active.append(f"{t.name}{prog*100:.1f}% — {t.state}")
return sorted(active)
# ==============================
# ENQUEUE NEW TORRENTS
# ==============================
def enqueue_new_torrents():
global stat_enqueued
active = count_active_downloads()
if active >= MAX_ACTIVE_DOWNLOADS:
return
slots = MAX_ACTIVE_DOWNLOADS - active
cursor.execute("""
SELECT id, torrent_hash, torrent_content, torrent_filename
FROM torrents
WHERE (qb_added IS NULL OR qb_added = 0)
AND torrent_content IS NOT NULL
ORDER BY added_datetime DESC
LIMIT %s
""", (slots,))
for row in cursor.fetchall():
blob = row["torrent_content"]
if not blob:
continue
if not is_valid_torrent(blob):
cursor.execute("""
UPDATE torrents
SET qb_state='invalid',
torrent_content=NULL,
qb_last_update=NOW()
WHERE id=%s
""", (row["id"],))
continue
# Add torrent
try:
qb.torrents_add(torrent_files=blob, savepath=DEFAULT_SAVE_PATH)
except Exception as e:
print(f"❌ Failed to add {row['torrent_hash']}: {e}")
continue
stat_enqueued += 1
added_new.append(row.get("torrent_filename") or row["torrent_hash"])
cursor.execute("""
UPDATE torrents
SET qb_added=1,
qb_hash=COALESCE(qb_hash, %s),
qb_state='added',
qb_last_update=NOW()
WHERE id=%s
""", (row["torrent_hash"], row["id"]))
# ==============================
# ✉️ EMAIL HELPERS
# ==============================
def format_list(title: str, items: list[str]) -> list[str]:
lines = []
if not items:
return [f"{title}: (none)"]
lines.append(f"{title}: {len(items)}")
shown = items[:MAX_LIST_ITEMS]
for it in shown:
lines.append(f" - {it}")
if len(items) > MAX_LIST_ITEMS:
lines.append(f" ... (+{len(items) - MAX_LIST_ITEMS} more)")
return lines
# ==============================
# 🏁 MAIN (ONE RUN)
# ==============================
print("🚀 QB worker run started")
try:
sync_qb_to_db()
handle_completed_and_dead()
enqueue_new_torrents()
# Snapshot after enqueue/deletions, so email reflects end-state
active_downloading = snapshot_active_downloading()
finally:
db.close()
# ==============================
# 📧 EMAIL REPORT
# ==============================
RUN_END = datetime.now()
body_lines = [
f"Run started : {RUN_START:%Y-%m-%d %H:%M:%S}",
f"Run finished: {RUN_END:%Y-%m-%d %H:%M:%S}",
"",
f"QB torrents synced : {stat_synced}",
f"Completed removed : {stat_completed}",
f"Dead removed : {stat_dead}",
f"New torrents added : {stat_enqueued}",
f"Active downloads : {sum(1 for _ in active_downloading)}",
"",
]
body_lines += format_list("Deleted (completed, kept data)", deleted_completed)
body_lines.append("")
body_lines += format_list("Deleted (dead, deleted files)", deleted_dead)
body_lines.append("")
body_lines += format_list("Newly added to qBittorrent", added_new)
body_lines.append("")
body_lines += format_list("Actively downloading now", active_downloading)
send_mail(
to=MAIL_TO,
subject=f"qBittorrent worker {RUN_START:%Y-%m-%d %H:%M}",
body="\n".join(body_lines),
html=False,
)
print("📧 Email report sent")
print("🎉 DONE")

0
Results/.gitkeep Normal file
View File

View File

@@ -0,0 +1,150 @@
import pymysql
import re
import time
import qbittorrentapi
# ============================================================
# KONFIGURACE
# ============================================================
MAX_SIZE_GB = 950
QBT_URL = "https://vladob.zen.usbx.me/qbittorrent"
QBT_USER = "vladob"
QBT_PASS = "jCni3U6d#y4bfcm"
DB_CONFIG = {
"host": "192.168.1.50",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "torrents",
"charset": "utf8mb4",
"autocommit": True,
}
# ============================================================
# POMOCNÉ FUNKCE
# ============================================================
def parse_size_to_gb(size_str):
"""Převede text '1.5 GB' nebo '500 MB' na float v GB"""
if not size_str: return 0.0
s = str(size_str).upper().replace(",", ".").strip()
match = re.search(r"([\d\.]+)", s)
if not match: return 0.0
val = float(match.group(1))
if "TB" in s: return val * 1024
if "GB" in s: return val
if "MB" in s: return val / 1024
if "KB" in s: return val / 1024 / 1024
return 0.0
# ============================================================
# HLAVNÍ LOGIKA
# ============================================================
def main():
print(f"🚀 Plánuji přímý upload z DB (Limit: {MAX_SIZE_GB} GB, řazeno dle seederů)...")
# 1. Načtení dat z DB
# Stahujeme i BLOB (torrent_content), takže to může chvilku trvat
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor()
print("⏳ Načítám data z MySQL...")
sql = """
SELECT torrent_hash, title_visible, size_pretty, seeders, torrent_content
FROM torrents
WHERE physical_exists = 0 AND torrent_content IS NOT NULL
ORDER BY seeders DESC
"""
cursor.execute(sql)
rows = cursor.fetchall()
db.close()
print(f"🔍 Nalezeno {len(rows)} kandidátů. Vybírám ty nejlepší...")
# 2. Výběr do kapacity 950 GB
selected_torrents = []
total_size_gb = 0.0
for row in rows:
t_hash, title, size_str, seeders, content = row
size_gb = parse_size_to_gb(size_str)
# Pojistka proti nesmyslně velkým souborům nebo chybám v parsování
if size_gb == 0 and "MB" not in str(size_str).upper() and "KB" not in str(size_str).upper():
pass
# Kontrola limitu
if total_size_gb + size_gb > MAX_SIZE_GB:
# Jakmile narazíme na něco, co se nevejde, končíme výběr (protože jsou seřazeny dle priority)
print(f"🛑 Limit naplněn! '{title}' ({size_gb:.2f} GB) by přesáhl {MAX_SIZE_GB} GB.")
break
selected_torrents.append({
"filename": f"{t_hash}.torrent", # Virtuální název souboru
"content": content, # Binární data
"title": title,
"size": size_gb,
"seeders": seeders
})
total_size_gb += size_gb
# 3. Report
print("-" * 40)
print(f"📦 Vybráno: {len(selected_torrents)} torrentů")
print(f"💾 Celková velikost: {total_size_gb:.2f} GB / {MAX_SIZE_GB} GB")
if selected_torrents:
avg_seeders = sum(t['seeders'] for t in selected_torrents) / len(selected_torrents)
print(f"⚡ Průměrně seederů: {avg_seeders:.1f}")
print("-" * 40)
if not selected_torrents:
print("Nic k nahrání.")
exit()
confirm = input("❓ Nahrát tento výběr na Seedbox? (ano/ne): ")
if confirm.lower() not in ['ano', 'y', 'yes']:
print("❌ Zrušeno.")
exit()
# 4. Připojení k qBittorrent
try:
qbt = qbittorrentapi.Client(
host=QBT_URL,
username=QBT_USER,
password=QBT_PASS,
VERIFY_WEBUI_CERTIFICATE=False
)
qbt.auth_log_in()
print("✅ Připojeno k Seedboxu.")
except Exception as e:
print(f"❌ Chyba připojení: {e}")
exit()
# 5. Odeslání dat
print("🚀 Odesílám...")
success_count = 0
for i, item in enumerate(selected_torrents):
try:
# Posíláme binární data přímo (tváříme se, že posíláme soubor)
# formát: {'nazev_souboru.torrent': b'binarni_data...'}
file_dict = {item['filename']: item['content']}
qbt.torrents_add(torrent_files=file_dict, is_paused=False)
print(f"[{i + 1}/{len(selected_torrents)}] 📤 {item['title']} ({item['size']:.1f} GB)")
success_count += 1
time.sleep(0.2) # Malá pauza pro stabilitu API
except Exception as e:
print(f"❌ Chyba u {item['title']}: {e}")
print("\n✅ HOTOVO.")
print("Torrenty jsou na Seedboxu. Až se stáhnou, stáhni je domů a spusť skript 99_Scan...")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import bencodepy
# ===============================
# DB CONFIG
# ===============================
DB_CONFIG = dict(
host="192.168.1.50",
user="root",
password="Vlado9674+",
database="torrents",
charset="utf8mb4"
)
LIMIT = 5 # kolik torrentů zobrazit
# ===============================
# TORRENT PARSER
# ===============================
def parse_torrent(blob):
data = bencodepy.decode(blob)
info = data[b'info']
files = []
# multi-file torrent
if b'files' in info:
for f in info[b'files']:
path = "/".join(p.decode(errors="ignore") for p in f[b'path'])
size = f[b'length']
files.append((path, size))
# single-file torrent
else:
name = info[b'name'].decode(errors="ignore")
size = info[b'length']
files.append((name, size))
return files
# ===============================
# MAIN
# ===============================
def main():
conn = pymysql.connect(**DB_CONFIG)
cur = conn.cursor()
cur.execute(f"""
SELECT id, title_visible, qb_savepath, torrent_content
FROM torrents
WHERE torrent_content IS NOT NULL
LIMIT {LIMIT}
""")
rows = cur.fetchall()
for tid, title, savepath, blob in rows:
print("\n" + "="*80)
print(f"Torrent ID : {tid}")
print(f"Title : {title}")
print(f"Savepath : {savepath}")
try:
files = parse_torrent(blob)
print(f"Files inside torrent: {len(files)}")
for path, size in files:
print(f" {size:>12} B {path}")
except Exception as e:
print("ERROR parsing torrent:", e)
cur.close()
conn.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import pymysql
import bencodepy
from tqdm import tqdm
# =====================================
# CONFIG
# =====================================
ULTRACC_ROOT = r"\\tower\torrents\ultracc"
DRY_MODE = False
DB_CONFIG = dict(
host="192.168.1.50",
user="root",
password="Vlado9674+",
database="torrents",
charset="utf8mb4"
)
# =====================================
# TORRENT PARSER
# =====================================
def parse_torrent(blob):
data = bencodepy.decode(blob)
info = data[b'info']
files = []
# multi file
if b'files' in info:
for f in info[b'files']:
rel_path = "/".join(p.decode(errors="ignore") for p in f[b'path'])
size = f[b'length']
files.append((rel_path, size))
multi = True
else:
name = info[b'name'].decode(errors="ignore")
size = info[b'length']
files.append((name, size))
multi = False
return files, multi
# =====================================
# BUILD FILESYSTEM INDEX
# =====================================
import pickle
import os
INDEX_FILE = r"U:\PycharmProjects\Torrents\fs_index_ultracc.pkl"
FORCE_REBUILD = False
def build_fs_index():
# ============================
# LOAD EXISTING INDEX
# ============================
if os.path.exists(INDEX_FILE) and not FORCE_REBUILD:
print("Načítám uložený filesystem index...")
with open(INDEX_FILE, "rb") as f:
index = pickle.load(f)
print(f"Načten index ({len(index)} klíčů)")
return index
# ============================
# BUILD NEW INDEX
# ============================
print("Indexuji filesystem...")
index = {}
pocet = 0
for root, _, files in os.walk(ULTRACC_ROOT):
for f in files:
full = os.path.join(root, f)
try:
size = os.path.getsize(full)
except OSError:
continue
key = (f.lower(), size)
pocet += 1
if pocet % 100 == 0:
print(pocet)
index.setdefault(key, []).append(full)
print(f"Index obsahuje {len(index)} unikátních souborů")
# ============================
# SAVE INDEX
# ============================
print("Ukládám index na disk...")
with open(INDEX_FILE, "wb") as f:
pickle.dump(index, f, protocol=pickle.HIGHEST_PROTOCOL)
print("Index uložen")
return index
# =====================================
# VALIDACE ROOTU
# =====================================
def validate_root(root, torrent_files):
for rel_path, size in torrent_files:
check = os.path.join(root, rel_path.replace("/", os.sep))
if not os.path.exists(check):
return False
return True
# =====================================
# MAIN
# =====================================
def main():
fs_index = build_fs_index()
conn = pymysql.connect(**DB_CONFIG)
cur = conn.cursor()
cur.execute("""
SELECT id, torrent_content
FROM torrents
WHERE torrent_content IS NOT NULL and physical_exists=FALSE
""")
rows = cur.fetchall()
print(f"Torrentů ke kontrole: {len(rows)}")
success = 0
for tid, blob in tqdm(rows):
try:
torrent_files, multi = parse_torrent(blob)
# vezmeme největší soubor pro lookup
rel_path, size = max(torrent_files, key=lambda x: x[1])
fname = os.path.basename(rel_path).lower()
key = (fname, size)
if key not in fs_index:
continue
found = False
for full_path in fs_index[key]:
if multi:
# ROOT = odečtení relativní cesty
root = full_path[:-len(rel_path)]
root = root.rstrip("\\/")
else:
# single file
root = os.path.dirname(full_path)
if validate_root(root, torrent_files):
found = True
success += 1
print(f"[FOUND] Torrent {tid}{root}")
if not DRY_MODE:
cur.execute("""
UPDATE torrents
SET physical_exists = 1
WHERE id = %s
""", (tid,))
break
if not found:
pass
except Exception as e:
print(f"ERROR torrent {tid}: {e}")
if not DRY_MODE:
conn.commit()
print(f"Celkem nalezeno: {success}")
cur.close()
conn.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,64 @@
import pymysql
import pymysql.cursors
def get_unfinished_torrents():
# Konfigurace připojení
connection_config = {
'host': '192.168.1.76',
'user': 'root',
'password': 'Vlado9674+',
'database': 'torrents',
'port': 3307,
'cursorclass': pymysql.cursors.DictCursor # Vrací výsledky jako slovník
}
try:
# Navázání spojení
connection = pymysql.connect(**connection_config)
with connection.cursor() as cursor:
# SQL Dotaz
sql = """
SELECT
title_visible,
qb_progress,
qb_state,
size_pretty,
added_datetime
FROM torrents
WHERE qb_added = 1
AND qb_progress < 1
AND qb_state NOT IN ('seeding', 'uploading', 'stalledUP', 'pausedUP', 'completed')
ORDER BY qb_progress DESC;
"""
cursor.execute(sql)
results = cursor.fetchall()
print(f"\n--- NEDOKONČENÉ TORRENTY (Port {connection_config['port']}) ---")
if not results:
print("Vše je hotovo nebo nic neběží.")
else:
for row in results:
# Předpokládáme, že qb_progress je float (0.0 až 1.0)
progress_pct = row['qb_progress'] * 100
print(f"Torrent: {row['title_visible']}")
print(f"Stav: {row['qb_state']}")
print(f"Pokrok: {progress_pct:.2f}%")
print(f"Velikost: {row['size_pretty']}")
print("-" * 40)
except pymysql.MySQLError as e:
print(f"Chyba při komunikaci s DB: {e}")
finally:
if 'connection' in locals():
connection.close()
print("Spojení s databází bylo uzavřeno.")
if __name__ == "__main__":
get_unfinished_torrents()

View File

@@ -1,194 +0,0 @@
# functions.py
# pip install requests_pkcs12 pymysql
from __future__ import annotations
import os
from datetime import date
from pprint import pprint
import xml.etree.ElementTree as ET
import requests
from requests_pkcs12 import Pkcs12Adapter
import pymysql
from pymysql.cursors import DictCursor
import fdb
import socket
def get_medicus_connection():
"""
Attempt to create a Firebird connection to the Medicus database.
Returns:
fdb.Connection object on success
None on failure
"""
if socket.gethostname().strip() in ("NTBVBHP470G10","Z230"):
MEDICUS_CFG = dict(
dsn=r"192.168.1.4:z:\medicus 3\data\medicus.fdb",
user="SYSDBA",
password="masterkey",
charset="win1250",
)
elif socket.gethostname().strip()=="SESTRA":
MEDICUS_CFG = dict(
dsn=r"192.168.1.10:m:\medicus\data\medicus.fdb",
user="SYSDBA",
password="masterkey",
charset="win1250",
)
try:
return fdb.connect(**MEDICUS_CFG)
except fdb.fbcore.DatabaseError as e:
print(f"Medicus DB connection failed: {e}")
return None
def get_mysql_connection():
"""
Return a PyMySQL connection or None if the connection fails.
"""
if socket.gethostname().strip() in ("NTBVBHP470G10","Z230"):
MYSQL_CFG = dict(
host="192.168.1.76",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
cursorclass=DictCursor,
autocommit=True, # or False if you prefer manual commit
)
elif socket.gethostname().strip() == "SESTRA":
MYSQL_CFG = dict(
host="127.0.0.1",
port=3307,
user="root",
password="Vlado9674+",
database="medevio",
cursorclass=DictCursor,
autocommit=True, # or False if you prefer manual commit
)
try:
return pymysql.connect(**MYSQL_CFG)
except pymysql.MySQLError as e:
print(f"MySQL connection failed: {e}")
return None
# ======== CONFIG (env overrides allowed) ========
PFX_PATH = os.getenv("VZP_PFX_PATH", r"mbcert.pfx")
PFX_PASS = os.getenv("VZP_PFX_PASS", "Vlado7309208104++")
VERIFY = os.getenv("VZP_CA_VERIFY", "true").lower() != "false" # set to path or "false" to disable
EP_STAV = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/stavPojisteniB2B"
SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/"
NS_STAV = "http://xmlns.gemsystem.cz/stavPojisteniB2B"
# ======== HELPERS ========
def normalize_rc(rc: str) -> str:
"""Return rodné číslo without slash/spaces."""
return rc.replace("/", "").replace(" ", "").strip()
def to_int_or_none(val):
if val is None:
return None
s = str(val).strip()
return int(s) if s.isdigit() else None
def _session():
s = requests.Session()
s.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_PATH, pkcs12_password=PFX_PASS))
return s
def post_soap(endpoint: str, inner_xml: str) -> str:
"""Wrap body in SOAP 1.1 envelope and POST with mTLS."""
envelope = f'''<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="{SOAP_NS}">
<soap:Body>{inner_xml}</soap:Body>
</soap:Envelope>'''
r = _session().post(
endpoint,
data=envelope.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": "process"},
timeout=30,
verify=VERIFY,
)
# You may log r.status_code if needed.
return r.text
def parse_stav(xml_text: str) -> dict:
"""Parse StavPojisteniB2B response → dict with keys:
'stav','kodPojistovny','nazevPojistovny','pojisteniKod','stavVyrizeniPozadavku'."""
NS = {"soap": SOAP_NS, "s": NS_STAV}
root = ET.fromstring(xml_text)
out = {"stav": None, "kodPojistovny": None, "nazevPojistovny": None,
"pojisteniKod": None, "stavVyrizeniPozadavku": None}
node = root.find(".//s:stavPojisteni", NS)
if node is not None:
for tag in ("stav", "kodPojistovny", "nazevPojistovny", "pojisteniKod"):
el = node.find(f"s:{tag}", NS)
out[tag] = el.text.strip() if (el is not None and el.text) else None
st = root.find(".//s:stavVyrizeniPozadavku", NS)
out["stavVyrizeniPozadavku"] = st.text.strip() if (st is not None and st.text) else None
return out
def save_stav_pojisteni(rc: str, k_datu: str, parsed: dict, response_xml: str) -> None:
"""Insert one log row into medevio.vzp_stav_pojisteni and print what is being saved."""
payload = {
"rc": normalize_rc(rc),
"k_datu": k_datu,
"stav": to_int_or_none(parsed.get("stav")), # 'X' -> None
"kod_pojistovny": parsed.get("kodPojistovny"),
"nazev_pojistovny": parsed.get("nazevPojistovny"),
"pojisteni_kod": parsed.get("pojisteniKod"),
"stav_vyrizeni": to_int_or_none(parsed.get("stavVyrizeniPozadavku")),
"response_xml": response_xml,
}
print("\n=== vzp_stav_pojisteni: will insert ===")
pprint(payload)
sql = """
INSERT INTO vzp_stav_pojisteni
(rc, k_datu, stav, kod_pojistovny, nazev_pojistovny, pojisteni_kod, stav_vyrizeni, response_xml)
VALUES
(%(rc)s, %(k_datu)s, %(stav)s, %(kod_pojistovny)s, %(nazev_pojistovny)s, %(pojisteni_kod)s, %(stav_vyrizeni)s, %(response_xml)s)
"""
conn = get_mysql_connection()
try:
with conn.cursor() as cur:
cur.execute(sql, payload)
conn.commit()
finally:
conn.close()
print("Inserted 1 row into vzp_stav_pojisteni.")
# ======== PUBLIC API ========
def check_insurance(rc: str, mode: str | None = None, k_datu: str | None = None):
"""
Call VZP 'StavPojisteniB2B' for given RC and date.
Returns (is_active, info_dict).
- is_active: True iff info_dict['stav'] == '1'
- info_dict: {'stav','kodPojistovny','nazevPojistovny','pojisteniKod','stavVyrizeniPozadavku'}
If mode == 'nodb' (case-insensitive), DOES NOT write to DB. Otherwise logs into medevio.vzp_stav_pojisteni.
"""
rc_norm = normalize_rc(rc)
kd = k_datu or date.today().isoformat()
body = f"""
<ns1:stavPojisteniB2B xmlns:ns1="{NS_STAV}">
<ns1:cisloPojistence>{rc_norm}</ns1:cisloPojistence>
<ns1:kDatu>{kd}</ns1:kDatu>
</ns1:stavPojisteniB2B>""".strip()
xml = post_soap(EP_STAV, body)
info = parse_stav(xml)
is_active = (info.get("stav") == "1")
if not (mode and mode.lower() == "nodb"):
save_stav_pojisteni(rc_norm, kd, info, xml)
return is_active, info

BIN
library_paths.db Normal file

Binary file not shown.

BIN
paths.pkl Normal file

Binary file not shown.

Binary file not shown.

22
sktorrent_cookies.json Normal file
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"
}
]