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:
@@ -398,23 +398,37 @@ async def admin_secrets_delete(request: Request, navn: str):
|
||||
|
||||
|
||||
# ---------------- podcast (Slægtens Stemmer) ----------------
|
||||
def _klient_ip(request: Request) -> str:
|
||||
return request.headers.get("x-real-ip") or (request.client.host if request.client else "?")
|
||||
|
||||
|
||||
def _podcast_kontekst(request: Request, rod: str, med_kladder: bool) -> list[dict]:
|
||||
"""Shows til podcast.html; rod = "/podcast/<nøgle>" eller "/podcast/f/<fælles nøgle>"."""
|
||||
base = str(request.base_url).rstrip("/")
|
||||
ud = []
|
||||
for name in podcast.shows():
|
||||
s = podcast.show(name)
|
||||
feed = f"{base}{rod}/{name}.xml"
|
||||
ud.append({**s, "feed": feed, "uden_skema": feed.split("://", 1)[1],
|
||||
"feed_enc": urllib.parse.quote(feed, safe=""),
|
||||
"episoder": podcast.episodes(name, med_kladder=med_kladder)})
|
||||
return ud
|
||||
|
||||
|
||||
def _ikke_fundet(request: Request):
|
||||
podcast.forkert(_klient_ip(request))
|
||||
raise HTTPException(404)
|
||||
|
||||
|
||||
@app.get("/podcast", response_class=HTMLResponse)
|
||||
async def podcast_side(request: Request, saved: str = ""):
|
||||
user = require_familie(request)
|
||||
admin = is_admin(user)
|
||||
key = podcast.noegle_til(user["username"], admin)
|
||||
base = str(request.base_url).rstrip("/")
|
||||
udsendelser = []
|
||||
for name in podcast.shows():
|
||||
s = podcast.show(name)
|
||||
feed = f"{base}/podcast/{key}/{name}.xml"
|
||||
uden = feed.split("://", 1)[1]
|
||||
udsendelser.append({**s, "feed": feed, "uden_skema": uden,
|
||||
"feed_enc": urllib.parse.quote(feed, safe=""),
|
||||
"episoder": podcast.episodes(name, med_kladder=admin)})
|
||||
rod = f"/podcast/{key}"
|
||||
return templates.TemplateResponse(request, "podcast.html", {
|
||||
"request": request, "user": user, "is_admin": admin, "key": key,
|
||||
"udsendelser": udsendelser, "saved": saved})
|
||||
"request": request, "user": user, "is_admin": admin, "rod": rod, "delt": False,
|
||||
"udsendelser": _podcast_kontekst(request, rod, med_kladder=admin), "saved": saved})
|
||||
|
||||
|
||||
@app.post("/podcast/ny-noegle")
|
||||
@@ -425,12 +439,60 @@ async def podcast_ny_noegle(request: Request):
|
||||
status_code=303)
|
||||
|
||||
|
||||
# Det fælles familielink: ingen login, ingen kladder, noindex. Står før de
|
||||
# personlige ruter, så "f" aldrig forveksles med en nøgle.
|
||||
@app.get("/podcast/f/{key}/", response_class=HTMLResponse)
|
||||
async def podcast_faelles_side(request: Request, key: str):
|
||||
if podcast.blokeret(_klient_ip(request)) or not podcast.faelles_slaa_op(key, "side"):
|
||||
_ikke_fundet(request)
|
||||
rod = f"/podcast/f/{key}"
|
||||
return templates.TemplateResponse(request, "podcast.html", {
|
||||
"request": request, "user": None, "is_admin": False, "rod": rod, "delt": True,
|
||||
"udsendelser": _podcast_kontekst(request, rod, med_kladder=False), "saved": ""},
|
||||
headers={"X-Robots-Tag": "noindex, nofollow, noarchive", "Referrer-Policy": "no-referrer",
|
||||
"Cache-Control": "private, no-store"})
|
||||
|
||||
|
||||
@app.get("/podcast/f/{key}/{name}.xml")
|
||||
async def podcast_faelles_feed(request: Request, key: str, name: str):
|
||||
if podcast.blokeret(_klient_ip(request)) or not podcast.faelles_slaa_op(key, "feed"):
|
||||
_ikke_fundet(request)
|
||||
xml = podcast.feed(name, f"/podcast/f/{key}", str(request.base_url).rstrip("/"), med_kladder=False)
|
||||
if xml is None:
|
||||
raise HTTPException(404)
|
||||
return Response(xml, media_type="application/rss+xml; charset=utf-8",
|
||||
headers={"Cache-Control": "private, max-age=300", "X-Robots-Tag": "noindex, nofollow"})
|
||||
|
||||
|
||||
@app.get("/podcast/f/{key}/{name}/cover.jpg")
|
||||
async def podcast_faelles_cover(request: Request, key: str, name: str):
|
||||
p = podcast.cover(name)
|
||||
if podcast.blokeret(_klient_ip(request)) or not podcast.faelles_slaa_op(key) or not p:
|
||||
_ikke_fundet(request)
|
||||
return FileResponse(p, media_type="image/jpeg", headers={"Cache-Control": "private, max-age=86400"})
|
||||
|
||||
|
||||
@app.get("/podcast/f/{key}/{name}/{slug}.mp3")
|
||||
async def podcast_faelles_mp3(request: Request, key: str, name: str, slug: str):
|
||||
rng = request.headers.get("range", "")
|
||||
fra_start = not rng or rng.replace(" ", "").startswith("bytes=0-")
|
||||
p = podcast.episode_fil(name, slug)
|
||||
meta = podcast.episode_meta(name, slug) or {}
|
||||
if (podcast.blokeret(_klient_ip(request)) or not p or meta.get("kladde")
|
||||
or not podcast.faelles_slaa_op(key, slug if fra_start else "")):
|
||||
_ikke_fundet(request)
|
||||
return FileResponse(p, media_type="audio/mpeg",
|
||||
headers={"Cache-Control": "private, max-age=86400", "X-Robots-Tag": "noindex, nofollow"})
|
||||
|
||||
|
||||
@app.get("/podcast/{key}/{name}.xml")
|
||||
async def podcast_feed(request: Request, key: str, name: str):
|
||||
if podcast.blokeret(_klient_ip(request)):
|
||||
_ikke_fundet(request)
|
||||
v = podcast.slaa_op(key, request.headers.get("user-agent", ""), "feed")
|
||||
if not v:
|
||||
raise HTTPException(404)
|
||||
xml = podcast.feed(name, key, str(request.base_url).rstrip("/"), med_kladder=bool(v.get("admin")))
|
||||
_ikke_fundet(request)
|
||||
xml = podcast.feed(name, f"/podcast/{key}", str(request.base_url).rstrip("/"), med_kladder=bool(v.get("admin")))
|
||||
if xml is None:
|
||||
raise HTTPException(404)
|
||||
return Response(xml, media_type="application/rss+xml; charset=utf-8",
|
||||
@@ -438,17 +500,18 @@ async def podcast_feed(request: Request, key: str, name: str):
|
||||
|
||||
|
||||
@app.get("/podcast/{key}/{name}/cover.jpg")
|
||||
async def podcast_cover(key: str, name: str):
|
||||
if not podcast.KEY.match(key) or not any(k["key"] == key and not k.get("tilbagekaldt") for k in podcast.alle_noegler()):
|
||||
raise HTTPException(404)
|
||||
async def podcast_cover(request: Request, key: str, name: str):
|
||||
p = podcast.cover(name)
|
||||
if not p:
|
||||
raise HTTPException(404)
|
||||
if (podcast.blokeret(_klient_ip(request)) or not p or not podcast.KEY.match(key)
|
||||
or not any(k["key"] == key and not k.get("tilbagekaldt") for k in podcast.alle_noegler())):
|
||||
_ikke_fundet(request)
|
||||
return FileResponse(p, media_type="image/jpeg", headers={"Cache-Control": "private, max-age=86400"})
|
||||
|
||||
|
||||
@app.get("/podcast/{key}/{name}/{slug}.mp3")
|
||||
async def podcast_mp3(request: Request, key: str, name: str, slug: str):
|
||||
if podcast.blokeret(_klient_ip(request)):
|
||||
_ikke_fundet(request)
|
||||
p = podcast.episode_fil(name, slug)
|
||||
rng = request.headers.get("range", "")
|
||||
# Apps henter i bidder; kun en hentning fra filens start tælles som en afspilning.
|
||||
@@ -458,7 +521,7 @@ async def podcast_mp3(request: Request, key: str, name: str, slug: str):
|
||||
else:
|
||||
v = next((k for k in podcast.alle_noegler() if k["key"] == key and not k.get("tilbagekaldt")), None)
|
||||
if not v or not p:
|
||||
raise HTTPException(404)
|
||||
_ikke_fundet(request)
|
||||
meta = podcast.episode_meta(name, slug) or {}
|
||||
if meta.get("kladde") and not v.get("admin"):
|
||||
raise HTTPException(404)
|
||||
@@ -471,10 +534,27 @@ async def admin_podcast(request: Request, saved: str = ""):
|
||||
user = require_admin(request)
|
||||
return templates.TemplateResponse(request, "admin_podcast.html", {
|
||||
"request": request, "user": user, "is_admin": True, "saved": saved,
|
||||
"noegler": podcast.alle_noegler(),
|
||||
"noegler": podcast.alle_noegler(), "faelles": podcast.faelles(),
|
||||
"base": str(request.base_url).rstrip("/"),
|
||||
"udsendelser": [{**podcast.show(n), "episoder": podcast.episodes(n, med_kladder=True)} for n in podcast.shows()]})
|
||||
|
||||
|
||||
@app.post("/admin/podcast/faelles")
|
||||
async def admin_podcast_faelles(request: Request):
|
||||
require_admin(request)
|
||||
f = await request.form()
|
||||
handling = f.get("handling")
|
||||
if handling == "skift":
|
||||
podcast.faelles_skift()
|
||||
besked = "Nyt fælles link lavet. Det gamle virker ikke længere."
|
||||
elif handling in ("til", "fra"):
|
||||
podcast.faelles_aktiv(handling == "til")
|
||||
besked = "Fælles link slået til" if handling == "til" else "Fælles link slået fra"
|
||||
else:
|
||||
besked = "Ukendt handling"
|
||||
return RedirectResponse(f"/admin/podcast?saved={urllib.parse.quote(besked)}", status_code=303)
|
||||
|
||||
|
||||
@app.post("/admin/podcast/noegle/{key}")
|
||||
async def admin_podcast_noegle(request: Request, key: str):
|
||||
require_admin(request)
|
||||
|
||||
Reference in New Issue
Block a user