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) ----------------
|
# ---------------- 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 +439,60 @@ 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.
|
||||||
|
@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")
|
@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 +500,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 +521,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 +534,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)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{% 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">{% 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 +12,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 +23,7 @@
|
|||||||
|
|
||||||
<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>
|
<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 +50,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 +60,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>
|
||||||
|
|||||||
Reference in New Issue
Block a user