Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
176859b15a | ||
|
|
70bc700a11 | ||
|
|
94e5a0ca01 |
@@ -13,8 +13,9 @@ API_TOKEN = os.environ["AUTHENTIK_API_TOKEN"]
|
|||||||
SECRET_KEY = os.environ["SESSION_SECRET"]
|
SECRET_KEY = os.environ["SESSION_SECRET"]
|
||||||
REDIRECT_URI = os.environ.get("OIDC_REDIRECT_URI", "https://weircon.dk/auth/callback")
|
REDIRECT_URI = os.environ.get("OIDC_REDIRECT_URI", "https://weircon.dk/auth/callback")
|
||||||
ADMIN_GROUP = os.environ.get("ADMIN_GROUP", "authentik Admins")
|
ADMIN_GROUP = os.environ.get("ADMIN_GROUP", "authentik Admins")
|
||||||
# Hvem der må høre familiens podcasts (/podcast); admins kan altid.
|
# Hvem der må høre familiens podcasts (/podcast); admins kan altid. Kommasepareret:
|
||||||
PODCAST_GROUP = os.environ.get("PODCAST_GROUP", "Familie")
|
# Familie er den nære familie, Gramps er slægten der kun har Gramps + podcast + portal.
|
||||||
|
PODCAST_GROUPS = {g.strip() for g in os.environ.get("PODCAST_GROUP", "Familie,Gramps").split(",") if g.strip()}
|
||||||
|
|
||||||
MATOMO_BASE = os.environ.get("MATOMO_BASE", "https://matomo.weircon.dk").rstrip("/")
|
MATOMO_BASE = os.environ.get("MATOMO_BASE", "https://matomo.weircon.dk").rstrip("/")
|
||||||
MATOMO_TOKEN = os.environ.get("MATOMO_TOKEN", "")
|
MATOMO_TOKEN = os.environ.get("MATOMO_TOKEN", "")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
@@ -71,14 +72,14 @@ def require_admin(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
def require_familie(request: Request):
|
def require_familie(request: Request):
|
||||||
"""Logget ind og i PODCAST_GROUP (eller admin). Uden login: login og tilbage hertil."""
|
"""Logget ind og i en af PODCAST_GROUPS (eller admin). Uden login: login og tilbage hertil."""
|
||||||
user = current_user(request)
|
user = current_user(request)
|
||||||
if not user and request.method == "GET":
|
if not user and request.method == "GET":
|
||||||
nxt = request.url.path + (f"?{request.url.query}" if request.url.query else "")
|
nxt = request.url.path + (f"?{request.url.query}" if request.url.query else "")
|
||||||
raise HTTPException(303, headers={"Location": "/auth/login?next=" + urllib.parse.quote(nxt, safe="")})
|
raise HTTPException(303, headers={"Location": "/auth/login?next=" + urllib.parse.quote(nxt, safe="")})
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(401, "Log ind først")
|
raise HTTPException(401, "Log ind først")
|
||||||
if not (is_admin(user) or config.PODCAST_GROUP in (user.get("groups") or [])):
|
if not (is_admin(user) or config.PODCAST_GROUPS & set(user.get("groups") or [])):
|
||||||
raise HTTPException(403, "Kun for familien")
|
raise HTTPException(403, "Kun for familien")
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ async def dashboard_content(request: Request):
|
|||||||
|
|
||||||
def _podcast_kort(user) -> list[dict]:
|
def _podcast_kort(user) -> list[dict]:
|
||||||
"""Kort til forsiden: én pr. podcast med udgivne episoder (admins ser også kladder)."""
|
"""Kort til forsiden: én pr. podcast med udgivne episoder (admins ser også kladder)."""
|
||||||
if not (is_admin(user) or config.PODCAST_GROUP in (user.get("groups") or [])):
|
if not (is_admin(user) or config.PODCAST_GROUPS & set(user.get("groups") or [])):
|
||||||
return []
|
return []
|
||||||
ud = []
|
ud = []
|
||||||
for name in podcast.shows():
|
for name in podcast.shows():
|
||||||
@@ -398,23 +399,37 @@ async def admin_secrets_delete(request: Request, navn: str):
|
|||||||
|
|
||||||
|
|
||||||
# ---------------- podcast (Slægtens Stemmer) ----------------
|
# ---------------- 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)
|
@app.get("/podcast", response_class=HTMLResponse)
|
||||||
async def podcast_side(request: Request, saved: str = ""):
|
async def podcast_side(request: Request, saved: str = ""):
|
||||||
user = require_familie(request)
|
user = require_familie(request)
|
||||||
admin = is_admin(user)
|
admin = is_admin(user)
|
||||||
key = podcast.noegle_til(user["username"], admin)
|
key = podcast.noegle_til(user["username"], admin)
|
||||||
base = str(request.base_url).rstrip("/")
|
rod = f"/podcast/{key}"
|
||||||
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)})
|
|
||||||
return templates.TemplateResponse(request, "podcast.html", {
|
return templates.TemplateResponse(request, "podcast.html", {
|
||||||
"request": request, "user": user, "is_admin": admin, "key": key,
|
"request": request, "user": user, "is_admin": admin, "rod": rod, "delt": False,
|
||||||
"udsendelser": udsendelser, "saved": saved})
|
"udsendelser": _podcast_kontekst(request, rod, med_kladder=admin), "saved": saved})
|
||||||
|
|
||||||
|
|
||||||
@app.post("/podcast/ny-noegle")
|
@app.post("/podcast/ny-noegle")
|
||||||
@@ -425,12 +440,86 @@ async def podcast_ny_noegle(request: Request):
|
|||||||
status_code=303)
|
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.
|
||||||
|
# Messenger/Facebooks link-forhåndsvisning og deres indbyggede browsere.
|
||||||
|
_FORHAANDSVISNING = re.compile(r"facebookexternalhit|meta-external(agent|fetcher)|Facebot|WhatsApp", re.I)
|
||||||
|
_INDBYGGET = re.compile(r"FBAN|FBAV|FB_IAB|FBIOS|Messenger|Instagram|LinkedInApp", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/podcast/f/{key}/", response_class=HTMLResponse)
|
||||||
|
async def podcast_faelles_side(request: Request, key: str):
|
||||||
|
ua = request.headers.get("user-agent", "")
|
||||||
|
forhaandsvisning = bool(_FORHAANDSVISNING.search(ua))
|
||||||
|
if podcast.blokeret(_klient_ip(request)) or not podcast.faelles_slaa_op(key, "" if forhaandsvisning else "side"):
|
||||||
|
_ikke_fundet(request)
|
||||||
|
rod = f"/podcast/f/{key}"
|
||||||
|
base = str(request.base_url).rstrip("/")
|
||||||
|
udsendelser = _podcast_kontekst(request, rod, med_kladder=False)
|
||||||
|
s = udsendelser[0] if udsendelser else {"titel": "Podcast", "slug": "", "har_cover": False}
|
||||||
|
og = {"titel": s["titel"], "url": f"{base}{rod}/",
|
||||||
|
"beskrivelse": "Familiens podcast. Lyt i browseren, eller abonnér i din podcast-app.",
|
||||||
|
"billede": f"{base}{rod}/{s['slug']}/cover.jpg" if s.get("har_cover") else ""}
|
||||||
|
headers = {"X-Robots-Tag": "noindex, nofollow, noarchive", "Referrer-Policy": "no-referrer",
|
||||||
|
"Cache-Control": "private, no-store"}
|
||||||
|
if forhaandsvisning:
|
||||||
|
# Forhåndsvisningen får kun titel, en neutral tekst og omslaget, aldrig
|
||||||
|
# episodelisten (den nævner familiemedlemmer ved navn).
|
||||||
|
return templates.TemplateResponse(request, "podcast_og.html", {"request": request, "og": og}, headers=headers)
|
||||||
|
indbygget = None
|
||||||
|
if _INDBYGGET.search(ua):
|
||||||
|
sti = f"{request.url.hostname}{rod}/"
|
||||||
|
if re.search(r"iPhone|iPad|iOS", ua):
|
||||||
|
indbygget = {"knap": "Åbn i Safari", "href": f"x-safari-https://{sti}"}
|
||||||
|
elif "Android" in ua:
|
||||||
|
indbygget = {"knap": "Åbn i Chrome", "href": f"intent://{sti}#Intent;scheme=https;end"}
|
||||||
|
else:
|
||||||
|
indbygget = {"knap": "", "href": ""}
|
||||||
|
return templates.TemplateResponse(request, "podcast.html", {
|
||||||
|
"request": request, "user": None, "is_admin": False, "rod": rod, "delt": True, "og": og,
|
||||||
|
"indbygget": indbygget, "udsendelser": udsendelser, "saved": ""}, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
@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")
|
@app.get("/podcast/{key}/{name}.xml")
|
||||||
async def podcast_feed(request: Request, key: str, name: str):
|
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")
|
v = podcast.slaa_op(key, request.headers.get("user-agent", ""), "feed")
|
||||||
if not v:
|
if not v:
|
||||||
raise HTTPException(404)
|
_ikke_fundet(request)
|
||||||
xml = podcast.feed(name, key, str(request.base_url).rstrip("/"), med_kladder=bool(v.get("admin")))
|
xml = podcast.feed(name, f"/podcast/{key}", str(request.base_url).rstrip("/"), med_kladder=bool(v.get("admin")))
|
||||||
if xml is None:
|
if xml is None:
|
||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
return Response(xml, media_type="application/rss+xml; charset=utf-8",
|
return Response(xml, media_type="application/rss+xml; charset=utf-8",
|
||||||
@@ -438,17 +527,18 @@ async def podcast_feed(request: Request, key: str, name: str):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/podcast/{key}/{name}/cover.jpg")
|
@app.get("/podcast/{key}/{name}/cover.jpg")
|
||||||
async def podcast_cover(key: str, name: str):
|
async def podcast_cover(request: Request, 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)
|
|
||||||
p = podcast.cover(name)
|
p = podcast.cover(name)
|
||||||
if not p:
|
if (podcast.blokeret(_klient_ip(request)) or not p or not podcast.KEY.match(key)
|
||||||
raise HTTPException(404)
|
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"})
|
return FileResponse(p, media_type="image/jpeg", headers={"Cache-Control": "private, max-age=86400"})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/podcast/{key}/{name}/{slug}.mp3")
|
@app.get("/podcast/{key}/{name}/{slug}.mp3")
|
||||||
async def podcast_mp3(request: Request, key: str, name: str, slug: str):
|
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)
|
p = podcast.episode_fil(name, slug)
|
||||||
rng = request.headers.get("range", "")
|
rng = request.headers.get("range", "")
|
||||||
# Apps henter i bidder; kun en hentning fra filens start tælles som en afspilning.
|
# Apps henter i bidder; kun en hentning fra filens start tælles som en afspilning.
|
||||||
@@ -458,7 +548,7 @@ async def podcast_mp3(request: Request, key: str, name: str, slug: str):
|
|||||||
else:
|
else:
|
||||||
v = next((k for k in podcast.alle_noegler() if k["key"] == key and not k.get("tilbagekaldt")), None)
|
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:
|
if not v or not p:
|
||||||
raise HTTPException(404)
|
_ikke_fundet(request)
|
||||||
meta = podcast.episode_meta(name, slug) or {}
|
meta = podcast.episode_meta(name, slug) or {}
|
||||||
if meta.get("kladde") and not v.get("admin"):
|
if meta.get("kladde") and not v.get("admin"):
|
||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
@@ -471,10 +561,27 @@ async def admin_podcast(request: Request, saved: str = ""):
|
|||||||
user = require_admin(request)
|
user = require_admin(request)
|
||||||
return templates.TemplateResponse(request, "admin_podcast.html", {
|
return templates.TemplateResponse(request, "admin_podcast.html", {
|
||||||
"request": request, "user": user, "is_admin": True, "saved": saved,
|
"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()]})
|
"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}")
|
@app.post("/admin/podcast/noegle/{key}")
|
||||||
async def admin_podcast_noegle(request: Request, key: str):
|
async def admin_podcast_noegle(request: Request, key: str):
|
||||||
require_admin(request)
|
require_admin(request)
|
||||||
|
|||||||
+70
-2
@@ -11,6 +11,7 @@ Data (ikke i git, persondata):
|
|||||||
DATA/podcast/<show>/episodes/<slug>.mp3 selve episoden
|
DATA/podcast/<show>/episodes/<slug>.mp3 selve episoden
|
||||||
DATA/podcast/<show>/episodes/<slug>.json titel, beskrivelse, nr, udgivet, kladde
|
DATA/podcast/<show>/episodes/<slug>.json titel, beskrivelse, nr, udgivet, kladde
|
||||||
DATA/podcast_keys.json nøgle -> bruger, statistik
|
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
|
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.
|
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"
|
ROOT = config.DATA / "podcast"
|
||||||
KEYS = config.DATA / "podcast_keys.json"
|
KEYS = config.DATA / "podcast_keys.json"
|
||||||
|
DELT = config.DATA / "podcast_faelles.json"
|
||||||
SHOW = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
|
SHOW = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
|
||||||
SLUG = re.compile(r"^[a-z0-9][a-z0-9._-]{0,120}$")
|
SLUG = re.compile(r"^[a-z0-9][a-z0-9._-]{0,120}$")
|
||||||
KEY = re.compile(r"^[A-Za-z0-9_-]{32,64}$")
|
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}"
|
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)
|
s = show(name)
|
||||||
if not s:
|
if not s:
|
||||||
return None
|
return None
|
||||||
pre = f"{base}/podcast/{key}/{name}"
|
pre = f"{base}{rod}/{name}"
|
||||||
items = []
|
items = []
|
||||||
for e in episodes(name, med_kladder):
|
for e in episodes(name, med_kladder):
|
||||||
titel = ("[Kladde] " if e.get("kladde") else "") + e.get("titel", e["slug"])
|
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>
|
</channel>
|
||||||
</rss>
|
</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)
|
||||||
|
|||||||
@@ -24,13 +24,18 @@
|
|||||||
|
|
||||||
<p style="margin:.9rem 0 .4rem"><strong>Grupper de skal med i</strong></p>
|
<p style="margin:.9rem 0 .4rem"><strong>Grupper de skal med i</strong></p>
|
||||||
<div class="wt-grid wt-grid--tight">
|
<div class="wt-grid wt-grid--tight">
|
||||||
|
{% set gruppe_hjaelp = {
|
||||||
|
"Familie": "den nære familie: Sønderlund, Mealie, Emby, Gramps (redigér), podcast m.m.",
|
||||||
|
"Gramps": "kun Gramps (læse), familiens podcast og portalen",
|
||||||
|
"Crawlkit-team": "kun crawlkit",
|
||||||
|
} %}
|
||||||
{% for g in grupper %}
|
{% for g in grupper %}
|
||||||
<label class="wt-select-card">
|
<label class="wt-select-card">
|
||||||
<input type="checkbox" name="grupper" value="{{ g.pk }}" {% if g.name == "Familie" %}checked{% endif %}>
|
<input type="checkbox" name="grupper" value="{{ g.pk }}" {% if g.name == "Familie" %}checked{% endif %}>
|
||||||
<span class="wt-select-card-ini">{{ g.name[0]|upper }}</span>
|
<span class="wt-select-card-ini">{{ g.name[0]|upper }}</span>
|
||||||
<span class="wt-select-card-body">
|
<span class="wt-select-card-body">
|
||||||
<span class="wt-select-card-name">{{ g.name }}</span>
|
<span class="wt-select-card-name">{{ g.name }}</span>
|
||||||
<span class="wt-select-card-sub">{{ g.members }} medlemmer{% if g.name == "authentik Admins" %} · ⚠️ fuld admin-adgang{% endif %}</span>
|
<span class="wt-select-card-sub">{{ g.members }} medlemmer{% if g.name == "authentik Admins" %} · ⚠️ fuld admin-adgang{% elif g.name in gruppe_hjaelp %} · {{ gruppe_hjaelp[g.name] }}{% endif %}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="wt-select-card-chk">✓</span>
|
<span class="wt-select-card-chk">✓</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -34,6 +34,35 @@
|
|||||||
</table>
|
</table>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<h2 style="font-size:1.15rem;margin-top:2rem">Fælles familielink</h2>
|
||||||
|
<p class="wt-muted" style="margin:.3rem 0 .8rem">Ét link uden login, fx til familiens Messenger-gruppe: afspiller og feed i ét. Kladder vises aldrig her. Skifter du nøglen, holder det gamle link op med at virke med det samme, også for dem der abonnerer via det.</p>
|
||||||
|
<div class="wt-card" style="padding:1rem 1.2rem;max-width:48rem">
|
||||||
|
{% if faelles.key %}
|
||||||
|
<p style="margin:0 0 .6rem"><strong>{{ 'Slået til' if faelles.aktiv else 'Slået fra' }}</strong>
|
||||||
|
· {{ faelles.hentninger or 0 }} hentninger{% if faelles.sidst %} · sidst <span data-ts="{{ faelles.sidst }}"></span>{% endif %}</p>
|
||||||
|
{% if faelles.aktiv %}
|
||||||
|
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.8rem">
|
||||||
|
<input class="wt-input" readonly id="faelles-link" value="{{ base }}/podcast/f/{{ faelles.key }}/"
|
||||||
|
style="flex:1 1 22rem;font-family:ui-monospace,monospace;font-size:.85rem" onclick="this.select()">
|
||||||
|
<button class="wt-btn" type="button" onclick="var i=document.getElementById('faelles-link');i.select();navigator.clipboard&&navigator.clipboard.writeText(i.value);this.textContent='Kopieret'">Kopiér</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" action="/admin/podcast/faelles" style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0">
|
||||||
|
<button class="wt-btn" type="submit" name="handling" value="skift"
|
||||||
|
onclick="return confirm('Det gamle fælles link holder op med at virke. Fortsæt?')">Skift nøgle</button>
|
||||||
|
{% if faelles.aktiv %}
|
||||||
|
<button class="wt-btn" type="submit" name="handling" value="fra">Slå fra</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="wt-btn wt-btn--primary" type="submit" name="handling" value="til">Slå til</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form method="post" action="/admin/podcast/faelles" style="margin:0">
|
||||||
|
<button class="wt-btn wt-btn--primary" type="submit" name="handling" value="skift">Lav fælles link</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 style="font-size:1.15rem;margin-top:2rem">Lytternes adresser</h2>
|
<h2 style="font-size:1.15rem;margin-top:2rem">Lytternes adresser</h2>
|
||||||
<table class="wt-table">
|
<table class="wt-table">
|
||||||
<thead><tr><th>Bruger</th><th>Oprettet</th><th>Sidst hentet</th><th>Hentninger</th><th>App</th><th></th></tr></thead>
|
<thead><tr><th>Bruger</th><th>Oprettet</th><th>Sidst hentet</th><th>Hentninger</th><th>App</th><th></th></tr></thead>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<link rel="stylesheet" href="https://assets.weircon.dk/css/weirsoe-theme.css" />
|
<link rel="stylesheet" href="https://assets.weircon.dk/css/weirsoe-theme.css" />
|
||||||
<link rel="stylesheet" href="/static/portal.css" />
|
<link rel="stylesheet" href="/static/portal.css" />
|
||||||
<script defer src="https://assets.weircon.dk/js/weirsoe-theme.js"></script>
|
<script defer src="https://assets.weircon.dk/js/weirsoe-theme.js"></script>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="wt-header">
|
<header class="wt-header">
|
||||||
|
|||||||
@@ -12,19 +12,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
{% if error %}<div class="err">{{ error }}</div>{% endif %}
|
{% if error %}<div class="err">{{ error }}</div>{% endif %}
|
||||||
{% if groups %}
|
|
||||||
<div class="toolbar">
|
|
||||||
<div class="filter"><span class="mag">⌕</span>
|
|
||||||
<input id="appfilter" type="search" placeholder="Filtrér…" autocomplete="off" aria-label="Filtrér tjenester"></div>
|
|
||||||
<div class="viewtoggle" role="group" aria-label="Visning">
|
|
||||||
<button type="button" data-view="grouped" class="{{ 'on' if view != 'alpha' }}">Grupperet</button>
|
|
||||||
<button type="button" data-view="alpha" class="{{ 'on' if view == 'alpha' }}">Alfabetisk</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<section id="favsec" class="appgroup"{% if not fav_apps %} style="display:none"{% endif %}>
|
|
||||||
<h2>★ Favoritter</h2>
|
|
||||||
<div class="grid" id="favgrid">{% for a in fav_apps %}{{ card(a) }}{% endfor %}</div>
|
|
||||||
</section>
|
|
||||||
{% if podcasts %}
|
{% if podcasts %}
|
||||||
<section class="appgroup" id="podsec">
|
<section class="appgroup" id="podsec">
|
||||||
<h2>🎙️ Familiens podcast</h2>
|
<h2>🎙️ Familiens podcast</h2>
|
||||||
@@ -40,6 +27,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if groups %}
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="filter"><span class="mag">⌕</span>
|
||||||
|
<input id="appfilter" type="search" placeholder="Filtrér…" autocomplete="off" aria-label="Filtrér tjenester"></div>
|
||||||
|
<div class="viewtoggle" role="group" aria-label="Visning">
|
||||||
|
<button type="button" data-view="grouped" class="{{ 'on' if view != 'alpha' }}">Grupperet</button>
|
||||||
|
<button type="button" data-view="alpha" class="{{ 'on' if view == 'alpha' }}">Alfabetisk</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section id="favsec" class="appgroup"{% if not fav_apps %} style="display:none"{% endif %}>
|
||||||
|
<h2>★ Favoritter</h2>
|
||||||
|
<div class="grid" id="favgrid">{% for a in fav_apps %}{{ card(a) }}{% endfor %}</div>
|
||||||
|
</section>
|
||||||
{% if mc_servers %}
|
{% if mc_servers %}
|
||||||
<section class="appgroup" id="mcsec">
|
<section class="appgroup" id="mcsec">
|
||||||
<h2>⛏️ Minecraft-servere</h2>
|
<h2>⛏️ Minecraft-servere</h2>
|
||||||
|
|||||||
+22
-6
@@ -1,5 +1,12 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Podcast{% endblock %}
|
{% block title %}{% if delt and udsendelser %}{{ udsendelser[0].titel }}{% else %}Podcast{% endif %}{% endblock %}
|
||||||
|
{% block head %}{% if delt %}<meta name="robots" content="noindex, nofollow, noarchive">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
{% if og %}<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="{{ og.titel }}">
|
||||||
|
<meta property="og:description" content="{{ og.beskrivelse }}">
|
||||||
|
<meta property="og:url" content="{{ og.url }}">
|
||||||
|
{% if og.billede %}<meta property="og:image" content="{{ og.billede }}">{% endif %}{% endif %}{% endif %}{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% if saved %}<div class="wt-flash">{{ saved }}</div>{% endif %}
|
{% if saved %}<div class="wt-flash">{{ saved }}</div>{% endif %}
|
||||||
{% if not udsendelser %}
|
{% if not udsendelser %}
|
||||||
@@ -10,7 +17,7 @@
|
|||||||
<section style="margin:1rem 0 2.5rem">
|
<section style="margin:1rem 0 2.5rem">
|
||||||
<div style="display:flex;gap:1.2rem;align-items:flex-start;flex-wrap:wrap">
|
<div style="display:flex;gap:1.2rem;align-items:flex-start;flex-wrap:wrap">
|
||||||
{% if u.har_cover %}
|
{% if u.har_cover %}
|
||||||
<img src="/podcast/{{ key }}/{{ u.slug }}/cover.jpg" alt="" width="160" height="160"
|
<img src="{{ rod }}/{{ u.slug }}/cover.jpg" alt="" width="160" height="160"
|
||||||
style="border-radius:14px;flex:0 0 auto;box-shadow:0 4px 18px rgba(0,0,0,.25)">
|
style="border-radius:14px;flex:0 0 auto;box-shadow:0 4px 18px rgba(0,0,0,.25)">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div style="flex:1 1 18rem;min-width:0">
|
<div style="flex:1 1 18rem;min-width:0">
|
||||||
@@ -21,7 +28,16 @@
|
|||||||
|
|
||||||
<div class="wt-card" style="margin:1.4rem 0;padding:1rem 1.2rem">
|
<div class="wt-card" style="margin:1.4rem 0;padding:1rem 1.2rem">
|
||||||
<h2 style="font-size:1.05rem;margin:0 0 .5rem">Abonnér i din podcast-app</h2>
|
<h2 style="font-size:1.05rem;margin:0 0 .5rem">Abonnér i din podcast-app</h2>
|
||||||
<p class="wt-muted" style="margin:.2rem 0 .8rem">Tryk på din app. Nye episoder kommer så af sig selv. Adressen er din egen, så del den ikke; familien får hver sin.</p>
|
{% if indbygget %}
|
||||||
|
<div class="wt-flash" style="margin:.4rem 0 .9rem">
|
||||||
|
Du har åbnet siden inde i Messenger, og den kan ikke åbne podcast-apps.
|
||||||
|
{% if indbygget.href %}Tryk her for at åbne siden i din almindelige browser, og abonnér derfra:
|
||||||
|
<div style="margin-top:.6rem"><a class="wt-btn wt-btn--primary" href="{{ indbygget.href }}">{{ indbygget.knap }}</a></div>
|
||||||
|
{% else %}Åbn siden i din almindelige browser (menuen ⋯ øverst), og abonnér derfra.{% endif %}
|
||||||
|
<div class="wt-muted" style="font-size:.85rem;margin-top:.5rem">Virker knappen ikke: tryk på ⋯ øverst og vælg "Åbn i ekstern browser". Du kan også bare lytte herunder.</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<p class="wt-muted" style="margin:.2rem 0 .8rem">{% if delt %}Tryk på din app, så kommer nye episoder af sig selv. Linket er familiens fælles, så del det kun inden for familien.{% else %}Tryk på din app. Nye episoder kommer så af sig selv. Adressen er din egen, så del den ikke; familien får hver sin.{% endif %}</p>
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:.5rem">
|
<div style="display:flex;flex-wrap:wrap;gap:.5rem">
|
||||||
<a class="wt-btn wt-btn--primary" href="podcast://{{ u.uden_skema }}">Apple Podcasts</a>
|
<a class="wt-btn wt-btn--primary" href="podcast://{{ u.uden_skema }}">Apple Podcasts</a>
|
||||||
<a class="wt-btn" href="overcast://x-callback-url/add?url={{ u.feed_enc }}">Overcast</a>
|
<a class="wt-btn" href="overcast://x-callback-url/add?url={{ u.feed_enc }}">Overcast</a>
|
||||||
@@ -48,9 +64,9 @@
|
|||||||
{% if e.kladde %}<strong>Kladde, kun du kan se den</strong> · {% endif %}{{ (e.varighed // 60) }} min
|
{% if e.kladde %}<strong>Kladde, kun du kan se den</strong> · {% endif %}{{ (e.varighed // 60) }} min
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% if e.beskrivelse %}<p style="margin:.5rem 0">{{ e.beskrivelse }}</p>{% endif %}
|
{% if e.beskrivelse %}<p style="margin:.5rem 0;white-space:pre-line">{{ e.beskrivelse }}</p>{% endif %}
|
||||||
<audio controls preload="none" style="width:100%;margin-top:.4rem"
|
<audio controls preload="none" style="width:100%;margin-top:.4rem"
|
||||||
src="/podcast/{{ key }}/{{ u.slug }}/{{ e.slug }}.mp3"></audio>
|
src="{{ rod }}/{{ u.slug }}/{{ e.slug }}.mp3"></audio>
|
||||||
</article>
|
</article>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="wt-muted">Ingen episoder endnu.</p>
|
<p class="wt-muted">Ingen episoder endnu.</p>
|
||||||
@@ -58,7 +74,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if udsendelser %}
|
{% if udsendelser and not delt %}
|
||||||
<form method="post" action="/podcast/ny-noegle" style="margin:2rem 0"
|
<form method="post" action="/podcast/ny-noegle" style="margin:2rem 0"
|
||||||
onsubmit="return confirm('Din gamle adresse holder op med at virke, og du skal abonnere igen i din app. Fortsæt?')">
|
onsubmit="return confirm('Din gamle adresse holder op med at virke, og du skal abonnere igen i din app. Fortsæt?')">
|
||||||
<button class="wt-btn" type="submit">Lav en ny adresse</button>
|
<button class="wt-btn" type="submit">Lav en ny adresse</button>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="da">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ og.titel }}</title>
|
||||||
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||||
|
<meta name="description" content="{{ og.beskrivelse }}">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="{{ og.titel }}">
|
||||||
|
<meta property="og:description" content="{{ og.beskrivelse }}">
|
||||||
|
<meta property="og:url" content="{{ og.url }}">
|
||||||
|
<meta property="og:locale" content="da_DK">
|
||||||
|
{% if og.billede %}<meta property="og:image" content="{{ og.billede }}">
|
||||||
|
<meta property="og:image:width" content="3000">
|
||||||
|
<meta property="og:image:height" content="3000">
|
||||||
|
<meta name="twitter:card" content="summary">{% endif %}
|
||||||
|
</head>
|
||||||
|
<body><h1>{{ og.titel }}</h1><p>{{ og.beskrivelse }}</p></body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user