Fotos: Fotowand je Verband + Gesichtssuche (lokales SCRFD/MobileFaceNet via onnxruntime)

This commit is contained in:
Nicolas Gruber
2026-10-19 11:28:59 +02:00
parent 32b4dc4823
commit d97164cddc
12 changed files with 640 additions and 2 deletions
+65
View File
@@ -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}")