diff --git a/pgz_sport_v2_router.py b/pgz_sport_v2_router.py index 821bd83..d63089f 100644 --- a/pgz_sport_v2_router.py +++ b/pgz_sport_v2_router.py @@ -5856,3 +5856,87 @@ def v2_clanovi_priority_sort(only: bool = False, limit: int = 500): """, (limit,)) return {"count": len(rows), "rows": rows} + +# =================================================================== +# HNS-3 (2026-05-05) — explicit drill-down endpoints for sport2.html +# Author: Damir Radulić (dradulic@outlook.com / damir@rinet.one) +# Description: 3-tab drill-down (HNS Karijera / Utakmice / Profil) +# =================================================================== + +@router.get("/clan/{clan_id}/hns-matches") +def v2_clan_hns_matches(clan_id: int, limit: int = 30): + """Posljednje N utakmica iz hns_player_matches za sportaša (sortirano DESC po datumu).""" + if limit < 1: limit = 1 + if limit > 500: limit = 500 + rows = db_query(""" + SELECT id, hns_igrac_id, clan_id, datum, natjecanje, + domacin, gost, rezultat, pozicija, startna, + minute_od, minute_do, + CASE WHEN minute_od IS NOT NULL AND minute_do IS NOT NULL + THEN (minute_do - minute_od) ELSE NULL END AS minute, + golovi, asistencije, zuti, crveni, source_url, scraped_at + FROM pgz_sport.hns_player_matches + WHERE clan_id = %s + ORDER BY datum DESC NULLS LAST, id DESC + LIMIT %s + """, (clan_id, limit)) + return {"clan_id": clan_id, "limit": limit, "count": len(rows), "rows": rows} + + +@router.get("/clan/{clan_id}/hns-profile") +def v2_clan_hns_profile(clan_id: int): + """Bio + HNS profil block za drill-down 'Profil' tab. + Vraća sve relevantne kolone iz pgz_sport.clanovi + HNS deep link + agregirane HNS statistike. + """ + p = db_one(""" + SELECT c.id, c.ime, c.prezime, c.oib, + c.datum_rodenja, c.datum_rodjenja, + c.mjesto_rodenja, c.mjesto_rodjenja, + c.spol, c.sport, c.pozicija, + c.dominantna_noga, c.visina_cm, c.tezina_kg, + c.broj_dresa, c.broj_dres, + c.kategorija, c.podkategorija, c.kategorije, + c.reprezentativac, c.kategoriziran, c.stipendiran, + c.aktivan, c.aktivni_status, + c.email, c.telefon, c.adresa, c.grad, + c.biografija, + c.slug, c.slika_url, + c.hns_igrac_id, c.profile_url, c.source_url, + c.klub_id, c.klub_naziv_godisnjak, + k.naziv AS klub_naziv, + c.dob_age, + EXTRACT(YEAR FROM age(COALESCE(c.datum_rodjenja, c.datum_rodenja)))::int AS dob_calc + FROM pgz_sport.clanovi c + LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id + WHERE c.id = %s + """, (clan_id,)) + if not p: + raise HTTPException(404, "Sportaš nije pronađen") + + # Aggregate HNS career stats (cheap, single row) + summary = db_one(""" + SELECT count(DISTINCT sezona) AS sezona_broj, + COALESCE(sum(nastupi),0) AS ukupno_nastupa, + COALESCE(sum(golovi),0) AS ukupno_golova, + COALESCE(sum(asistencije),0) AS ukupno_asistencija, + COALESCE(sum(zuti),0) AS ukupno_zutih, + COALESCE(sum(crveni),0) AS ukupno_crvenih, + COALESCE(sum(minute),0) AS ukupno_minuta, + min(sezona) AS prva_sezona, + max(sezona) AS zadnja_sezona + FROM pgz_sport.hns_player_seasons WHERE clan_id = %s + """, (clan_id,)) or {} + + # Build HNS deep link + hns_url = p.get("profile_url") + if not hns_url and p.get("hns_igrac_id"): + slug = p.get("slug") or f"{(p.get('ime') or '').lower()}-{(p.get('prezime') or '').lower()}".replace(" ", "-") + hns_url = f"https://semafor.hns.family/igraci/{p['hns_igrac_id']}/{slug}/" + + return { + "clan_id": clan_id, + "profile": p, + "hns_summary": summary, + "hns_url": hns_url, + } + diff --git a/scripts/hns_dispatch.sh b/scripts/hns_dispatch.sh new file mode 100755 index 0000000..8ad30e5 --- /dev/null +++ b/scripts/hns_dispatch.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# hns_dispatch.sh — parallel HNS player deep-scrape dispatcher +# ────────────────────────────────────────────────────────────── +# Author: dradulic@outlook.com / damir@rinet.one +# Date: 2026-05-05 +# Version: 1.0 +# Description: +# Reads a shard file of HNS player IDs (one per line) and invokes +# /opt/pgz-sport/scripts/hns_player_deep.py --player --no-telegram +# sequentially for each ID. Designed to be launched 5x in parallel +# (one per shard) so 5 worker processes each chew through ~175 IDs. +# +# Usage: +# bash /opt/pgz-sport/scripts/hns_dispatch.sh +# bash /opt/pgz-sport/scripts/hns_dispatch.sh /tmp/hns_shard_aa 1 +# +# Per-worker log: /tmp/hns_worker_.log +# Each player attempt: ~1s (HTTP + parse + UPSERT). +# Stagger workers manually with `sleep N &&` when launching. + +set -u + +SHARD_FILE="${1:?usage: hns_dispatch.sh }" +WORKER_ID="${2:?usage: hns_dispatch.sh }" +LOG="/tmp/hns_worker_${WORKER_ID}.log" +SCRIPT="/opt/pgz-sport/scripts/hns_player_deep.py" + +cd /opt/pgz-sport || exit 2 + +echo "[$(date -Iseconds)] worker=${WORKER_ID} shard=${SHARD_FILE} start ($(wc -l < "$SHARD_FILE") IDs)" >> "$LOG" + +count=0 +ok=0 +fail=0 +while IFS= read -r ID; do + [ -z "$ID" ] && continue + count=$((count + 1)) + if python3 "$SCRIPT" --player "$ID" --no-telegram >> "$LOG" 2>&1; then + ok=$((ok + 1)) + else + fail=$((fail + 1)) + echo "[$(date -Iseconds)] worker=${WORKER_ID} FAIL id=${ID}" >> "$LOG" + fi + # tiny politeness pause; HNS is light traffic but 5 parallel workers + # = 5 req/s aggregate; small jitter helps avoid bursts + sleep 0.2 +done < "$SHARD_FILE" + +echo "[$(date -Iseconds)] worker=${WORKER_ID} done count=${count} ok=${ok} fail=${fail}" >> "$LOG" diff --git a/scripts/hns_youth_categories.py b/scripts/hns_youth_categories.py index 74f9424..650bb8d 100644 --- a/scripts/hns_youth_categories.py +++ b/scripts/hns_youth_categories.py @@ -129,6 +129,11 @@ COMP_CATALOG = [ ("100381484", "kvalifikacije-za-prvu-nl-pioniri", "pioniri-u15", "2025/2026"), ("100569152", "treca-nl-istok", "seniori", "2025/2026"), # Treća NL Istok ("100585203", "treca-nl-zapad", "seniori", "2025/2026"), # Treća NL Zapad (PGŽ klubovi) + # PGŽ-region ŽNL leagues discovered via klub raspored auto-discovery + ("101555188", "1-znl-seniori", "seniori", "2025/2026"), # 1.ŽNL PGŽ seniori + ("112195128", "kup-zns", "seniori", "2025/2026"), # Kup ŽNS Vinodolsko-Senjsko + ("104425442", "kup-mladezi-juniori", "juniori-u19", "2025/2026"), + ("104464435", "kup-mladezi-kadeti", "kadeti-u17", "2025/2026"), ("100391485", "supersport-hnl", "seniori", "2025/2026"), ("100413651", "supersport-prva-nl", "seniori", "2025/2026"), ("100418001", "supersport-druga-nl", "seniori", "2025/2026"), diff --git a/static/sport2.html b/static/sport2.html index 324e15a..3bc8df7 100644 --- a/static/sport2.html +++ b/static/sport2.html @@ -205,6 +205,30 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1 .utlogo{width:22px;height:22px;border-radius:50%;background:var(--bg3);object-fit:contain;vertical-align:middle;margin-right:6px} +/* HNS-3 (2026-05-05) — Profil tab styles (Palantir Gotham aesthetic) */ +.prof-top{display:flex;align-items:flex-start;gap:18px;padding:14px;background:var(--bg2);border:1px solid var(--rim);border-radius:6px;margin-bottom:14px} +.prof-name-block{flex:1;min-width:0} +.prof-name{font-size:20px;font-weight:800;color:var(--t0);letter-spacing:.3px;margin-bottom:4px} +.prof-sub{color:var(--t2);font-size:12px;margin-bottom:6px} +.prof-club{font-size:12.5px;color:var(--t1);display:flex;align-items:center;flex-wrap:wrap;gap:6px} +.prof-dres{flex-shrink:0;width:96px;height:96px;border:2px solid var(--pgz-gold);border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,var(--bg3),var(--bg2))} +.prof-dres-num{font-size:38px;font-weight:900;color:var(--pgz-gold);line-height:1;font-family:var(--mono)} +.prof-dres-l{font-size:9.5px;color:var(--t3);letter-spacing:1.5px;font-weight:700;margin-top:4px} +.prof-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:6px} +.prof-cell{padding:9px 12px;background:var(--bg2);border:1px solid var(--rim);border-radius:5px} +.prof-cell .l{font-size:10px;color:var(--t3);text-transform:uppercase;letter-spacing:.5px;font-weight:700;margin-bottom:3px} +.prof-cell .v{font-size:13px;color:var(--t0);font-weight:600;word-break:break-word} +.prof-cell .v b{color:var(--pgz-gold);font-family:var(--mono)} +@media (max-width:760px){ + .prof-grid{grid-template-columns:repeat(2,1fr)} + .prof-top{flex-direction:column-reverse;align-items:stretch} + .prof-dres{width:100%;height:78px;flex-direction:row;gap:14px} + .prof-dres-num{font-size:32px} +} +@media (max-width:420px){ + .prof-grid{grid-template-columns:1fr} +} + .score{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;background:var(--bg3);font-size:11px;font-weight:600} .score.high{background:rgba(0,232,143,.15);color:var(--green)} .score.mid{background:rgba(245,158,11,.15);color:var(--amber)} @@ -1973,7 +1997,7 @@ async function openSportas(id){ const d = Object.assign({}, dRaw); if(dV2 && dV2.profile){ const p = dV2.profile; - ['visina_cm','tezina_kg','dominantna_noga','broj_dresa','biografija','mjesto_rodenja','mjesto_rodjenja','datum_rodenja','datum_rodjenja','spol','sport','aktivan'] + ['visina_cm','tezina_kg','dominantna_noga','broj_dresa','biografija','mjesto_rodenja','mjesto_rodjenja','datum_rodenja','datum_rodjenja','spol','sport','aktivan','pozicija','klub_naziv','klub_id','dob_age','hns_igrac_id','profile_url','reprezentativac','stipendiran','datum_pristupa','email','telefon','oib','slug','slika_url'] .forEach(k => { if((d[k]===null||d[k]===undefined||d[k]==='') && p[k]!==null && p[k]!==undefined && p[k]!=='') d[k]=p[k]; }); } const stats = d.stats || {}; @@ -2040,40 +2064,39 @@ async function openSportas(id){
${fmtNum(stats.sezone_aktivne||sezone.length)}
Sezona
+
🏆 HNS Karijera (${sezone.length})
-
📅 Posljednje utakmice (${utakmice.length})
-
🏷️ Kategorije (${kategorije.length})
-
Bio
-
Godišnjaci (${godisnjaci.length})
-
Nagrade (${nagrade.length})
+
📅 Utakmice (poslj. ${Math.min(utakmice.length,30)})
+
👤 Profil
🏆 HNS Karijera ${sezone.length} sezon${sezone.length===1?'a':(sezone.length<5&&sezone.length>1?'e':'a')}
${sezone.length ? `
- - ${sezone.map(s => ` + + ${[...sezone].sort((a,b)=>String(b.sezona||'').localeCompare(String(a.sezona||''))).map(s => ` - + - - + + + `).join('')} -
SezonaNatjecanjeKlubNas.Gol.Asis.ŽutiCrv.
SezonaKlubNatjecanjeNastupiGoloviAsis.ŽutiCrv.Min.
${esc(s.sezona||'')}${esc(s.natjecanje||'')} ${esc(s.klub_naziv||'')}${esc(s.natjecanje||'')} ${fmtNum(s.nastupi)} ${fmtNum(s.pogoci ?? s.golovi)} ${fmtNum(s.asistencije)}${fmtNum(s.zuti_kartoni ?? s.zuti)}${fmtNum(s.crveni_kartoni ?? s.crveni)}${fmtNum(s.zuti_kartoni ?? s.zuti)}${fmtNum(s.crveni_kartoni ?? s.crveni)}${fmtNum(s.minute)} ${(s.natjecanje_url||s.source_url)?'':''}
` : '
Nema sezonskih podataka
'} +
` : '
Nema sezonskih podataka iz HNS Semafora
'} - + ` : ''} - - - + ` : ''} - ` : ''} ${enrichBlock('sportas', d.id)}