feat(podcast): fælles familielink uden login
Et valgfrit fælles link, /podcast/f/<nøgle>/, til fx familiens Messenger- gruppe: samme afspillerside som den personlige, med abonnér-knapper og feed under samme sti, men uden login og uden admin-ting. Kladder vises aldrig der. I /admin/podcast laves linket, nøglen kan skiftes (det gamle dør med det samme), det kan slås fra, og hentningerne tælles. Siden er noindex (header og meta) og sender ingen referrer. Forkerte nøgler giver 404 ligesom ukendte stier, og en klient der rammer 20 forkerte nøgler på ti minutter, får 404 på alt under /podcast/<nøgle> i et stykke tid. Det gælder også de personlige nøgler. Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
This commit is contained in:
+70
-2
@@ -11,6 +11,7 @@ Data (ikke i git, persondata):
|
||||
DATA/podcast/<show>/episodes/<slug>.mp3 selve episoden
|
||||
DATA/podcast/<show>/episodes/<slug>.json titel, beskrivelse, nr, udgivet, kladde
|
||||
DATA/podcast_keys.json nøgle -> bruger, statistik
|
||||
DATA/podcast_faelles.json det fælles familielink (én nøgle, kan slås fra)
|
||||
|
||||
En episode med "kladde": true ses kun af administratorer (i deres eget feed og
|
||||
på siden), så den kan høres i en rigtig podcast-app før familien får den.
|
||||
@@ -29,6 +30,7 @@ import config
|
||||
|
||||
ROOT = config.DATA / "podcast"
|
||||
KEYS = config.DATA / "podcast_keys.json"
|
||||
DELT = config.DATA / "podcast_faelles.json"
|
||||
SHOW = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
|
||||
SLUG = re.compile(r"^[a-z0-9][a-z0-9._-]{0,120}$")
|
||||
KEY = re.compile(r"^[A-Za-z0-9_-]{32,64}$")
|
||||
@@ -213,11 +215,12 @@ def _hms(sek: int) -> str:
|
||||
return f"{sek // 3600:d}:{sek // 60 % 60:02d}:{sek % 60:02d}"
|
||||
|
||||
|
||||
def feed(name: str, key: str, base: str, med_kladder: bool) -> str | None:
|
||||
def feed(name: str, rod: str, base: str, med_kladder: bool) -> str | None:
|
||||
"""rod = stien foran showet, fx "/podcast/<nøgle>" eller "/podcast/f/<fælles nøgle>"."""
|
||||
s = show(name)
|
||||
if not s:
|
||||
return None
|
||||
pre = f"{base}/podcast/{key}/{name}"
|
||||
pre = f"{base}{rod}/{name}"
|
||||
items = []
|
||||
for e in episodes(name, med_kladder):
|
||||
titel = ("[Kladde] " if e.get("kladde") else "") + e.get("titel", e["slug"])
|
||||
@@ -254,3 +257,68 @@ def feed(name: str, key: str, base: str, med_kladder: bool) -> str | None:
|
||||
</channel>
|
||||
</rss>
|
||||
"""
|
||||
|
||||
|
||||
# ---------------- fælles familielink ----------------
|
||||
# Ét link uden login til fx familiens Messenger-gruppe: /podcast/f/<nøgle>/ med
|
||||
# afspiller og feed. Viser aldrig kladder. Kan skiftes (det gamle dør med det
|
||||
# samme) eller slås helt fra i /admin/podcast.
|
||||
|
||||
def faelles() -> dict:
|
||||
try:
|
||||
return json.loads(DELT.read_text())
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def faelles_skift() -> str:
|
||||
d = faelles()
|
||||
d.update(key=secrets.token_urlsafe(32), aktiv=True, oprettet=int(time.time()), hentninger=0, hentet={})
|
||||
d.pop("sidst", None)
|
||||
_skriv(DELT, d)
|
||||
return d["key"]
|
||||
|
||||
|
||||
def faelles_aktiv(aktiv: bool) -> None:
|
||||
d = faelles()
|
||||
if not d.get("key"):
|
||||
faelles_skift()
|
||||
d = faelles()
|
||||
d["aktiv"] = aktiv
|
||||
_skriv(DELT, d)
|
||||
|
||||
|
||||
def faelles_slaa_op(key: str, hvad: str = "") -> bool:
|
||||
d = faelles()
|
||||
if not (KEY.match(key or "") and d.get("aktiv") and d.get("key")
|
||||
and secrets.compare_digest(key, d["key"])):
|
||||
return False
|
||||
if hvad:
|
||||
d["hentninger"] = d.get("hentninger", 0) + 1
|
||||
d["sidst"] = int(time.time())
|
||||
d.setdefault("hentet", {})
|
||||
d["hentet"][hvad] = d["hentet"].get(hvad, 0) + 1
|
||||
_skriv(DELT, d)
|
||||
return True
|
||||
|
||||
|
||||
# ---------------- gæt på nøgler ----------------
|
||||
# Nøglerne er 256 bit og kan ikke gættes, men en klient der bliver ved med at
|
||||
# ramme forkerte nøgler, får 404 uden at vi overhovedet slår op (og samme svar
|
||||
# som en ukendt sti, så den ikke røber noget).
|
||||
_MISS: dict[str, list[float]] = {}
|
||||
MISS_GRAENSE, MISS_VINDUE = 20, 600
|
||||
|
||||
|
||||
def blokeret(ip: str) -> bool:
|
||||
nu = time.time()
|
||||
t = [x for x in _MISS.get(ip, []) if nu - x < MISS_VINDUE]
|
||||
_MISS[ip] = t
|
||||
return len(t) >= MISS_GRAENSE
|
||||
|
||||
|
||||
def forkert(ip: str) -> None:
|
||||
_MISS.setdefault(ip, []).append(time.time())
|
||||
if len(_MISS) > 5000: # hold hukommelsen nede ved en storm
|
||||
for k in list(_MISS)[:2500]:
|
||||
_MISS.pop(k, None)
|
||||
|
||||
Reference in New Issue
Block a user