Fotos: Fotowand je Verband + Gesichtssuche (lokales SCRFD/MobileFaceNet via onnxruntime)
This commit is contained in:
+65
@@ -1,14 +1,20 @@
|
||||
"""CIS API client for FH-Burgenland (basic auth, HTML/iCal scraping)."""
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
||||
PERSON_ID_CACHE = DATA_DIR / "person_ids.json"
|
||||
PHOTO_DIR = DATA_DIR / "photos"
|
||||
|
||||
TZ = timezone(timedelta(hours=1)) # Europe/Vienna winter; events carry their own tz, we just parse naive
|
||||
|
||||
SLOTS = [
|
||||
@@ -206,6 +212,65 @@ class CISClient:
|
||||
return Person(uid=u, name=name.strip(), stg=stg.strip(),
|
||||
semester=sem.strip(), verband=ver.strip(), gruppe="")
|
||||
|
||||
# ---------- person_id + photos ----------
|
||||
def person_id_for(self, uid: str) -> str:
|
||||
"""Matrikel/username → CIS person_id (persistent JSON cache)."""
|
||||
uid = (uid or "").strip()
|
||||
if not uid:
|
||||
return ""
|
||||
cache: dict = {}
|
||||
if PERSON_ID_CACHE.exists():
|
||||
try:
|
||||
cache = json.loads(PERSON_ID_CACHE.read_text())
|
||||
except Exception:
|
||||
cache = {}
|
||||
if uid in cache:
|
||||
return cache[uid]
|
||||
try:
|
||||
html = self._get(f"{self.base}/private/profile/index.php?uid={uid}")
|
||||
except requests.HTTPError:
|
||||
return ""
|
||||
m = re.search(r"bild\.php\?src=person&person_id=(\d+)", html)
|
||||
if not m:
|
||||
return ""
|
||||
pid = m.group(1)
|
||||
cache[uid] = pid
|
||||
try:
|
||||
PERSON_ID_CACHE.parent.mkdir(parents=True, exist_ok=True)
|
||||
PERSON_ID_CACHE.write_text(json.dumps(cache, indent=0))
|
||||
except OSError:
|
||||
pass
|
||||
return pid
|
||||
|
||||
def photo_bytes(self, person_id: str, max_age: int = 7 * 86400) -> Optional[tuple[bytes, str]]:
|
||||
"""Profilfoto holen (7-Tage-Disk-Cache). (bytes, content_type) oder None."""
|
||||
person_id = (person_id or "").strip()
|
||||
if not person_id.isdigit():
|
||||
return None
|
||||
f = PHOTO_DIR / f"{person_id}.bin"
|
||||
if f.exists() and time.time() - f.stat().st_mtime < max_age:
|
||||
try:
|
||||
return f.read_bytes(), "image/jpeg"
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
r = self.s.get(f"{self.base}/public/bild.php?src=person&person_id={person_id}", timeout=30)
|
||||
r.raise_for_status()
|
||||
except requests.RequestException:
|
||||
return None
|
||||
data = r.content
|
||||
if not data:
|
||||
return None
|
||||
ct = (r.headers.get("Content-Type") or "image/jpeg").split(";")[0].strip()
|
||||
if ct not in ("image/jpeg", "image/png", "image/gif"):
|
||||
return None
|
||||
try:
|
||||
PHOTO_DIR.mkdir(parents=True, exist_ok=True)
|
||||
f.write_bytes(data)
|
||||
except OSError:
|
||||
pass
|
||||
return data, ct
|
||||
|
||||
# ---------- Namensliste ----------
|
||||
def namensliste(self, kz: str, sem: str, ver: str) -> list[Student]:
|
||||
html = self._get(f"{self.base}/private/stud_in_grp.php?kz={kz}&sem={sem}&verband={ver}")
|
||||
|
||||
Reference in New Issue
Block a user