Compare commits

..
2 Commits
Author SHA1 Message Date
agw 176859b15a fix(podcast): fælles link virker i Messenger
CD / deploy (push) Successful in 16s
CI / check (push) Successful in 23s
Messengers indbyggede browser kan ikke åbne podcast-apps (podcast://,
overcast:// osv.), så abonnér-knapperne gjorde ingenting. Siden genkender nu
Facebook-, Messenger- og Instagram-browseren og viser en knap, der åbner
siden i Safari (x-safari-https) eller på Android i den almindelige browser
(intent://), plus en vejledning til menuen. Afspilleren virker stadig direkte.

Link-forhåndsvisningen (facebookexternalhit m.fl.) får en minimal side med
titel, en neutral tekst og omslaget som og:-tags, aldrig episodelisten, og
den tælles ikke som en hentning. Den fulde side har de samme og:-tags.

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
2026-10-06 07:52:11 +02:00
agw 70bc700a11 feat(podcast): også gruppen Gramps må lytte
CD / deploy (push) Successful in 16s
CI / check (push) Successful in 22s
Slægten uden for den nære familie inviteres i gruppen Gramps, der kun giver
Gramps, podcasten og portalen. PODCAST_GROUP er nu kommasepareret
(standard "Familie,Gramps"). Podcast-kortet på forsiden vises også, selvom
brugeren ingen andre apps har.

Invitationssiden forklarer de kendte grupper med en linje, så det er tydeligt
hvad Familie, Gramps og Crawlkit-team giver.

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
2026-10-04 22:29:19 +02:00
6 changed files with 91 additions and 25 deletions
+3 -2
View File
@@ -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", "")
+35 -8
View File
@@ -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():
@@ -441,16 +442,42 @@ async def podcast_ny_noegle(request: Request):
# Det fælles familielink: ingen login, ingen kladder, noindex. Står før de # Det fælles familielink: ingen login, ingen kladder, noindex. Står før de
# personlige ruter, så "f" aldrig forveksles med en nøgle. # 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) @app.get("/podcast/f/{key}/", response_class=HTMLResponse)
async def podcast_faelles_side(request: Request, key: str): async def podcast_faelles_side(request: Request, key: str):
if podcast.blokeret(_klient_ip(request)) or not podcast.faelles_slaa_op(key, "side"): 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) _ikke_fundet(request)
rod = f"/podcast/f/{key}" 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", { return templates.TemplateResponse(request, "podcast.html", {
"request": request, "user": None, "is_admin": False, "rod": rod, "delt": True, "request": request, "user": None, "is_admin": False, "rod": rod, "delt": True, "og": og,
"udsendelser": _podcast_kontekst(request, rod, med_kladder=False), "saved": ""}, "indbygget": indbygget, "udsendelser": udsendelser, "saved": ""}, headers=headers)
headers={"X-Robots-Tag": "noindex, nofollow, noarchive", "Referrer-Policy": "no-referrer",
"Cache-Control": "private, no-store"})
@app.get("/podcast/f/{key}/{name}.xml") @app.get("/podcast/f/{key}/{name}.xml")
+6 -1
View File
@@ -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>
+13 -13
View File
@@ -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>
+15 -1
View File
@@ -1,7 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{% if delt and udsendelser %}{{ udsendelser[0].titel }}{% else %}Podcast{% endif %}{% 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"> {% block head %}{% if delt %}<meta name="robots" content="noindex, nofollow, noarchive">
<meta name="referrer" content="no-referrer">{% endif %}{% endblock %} <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 %}
@@ -23,6 +28,15 @@
<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>
{% 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> <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>
+19
View File
@@ -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>