Tre fejl, der forstærkede hinanden:
- Hver tilføjelse startede sin egen kø-tømning, så flere hentninger kørte
samtidig (og natlig timer kunne køre oveni). Mange samtidige hentninger fra
samme IP er netop det YouTube svarer 403 på. Køen har nu en fil-lås
(yt_koe.lock), så kun én kørsel henter, også på tværs af processer.
- yt-dlp gentager ikke selv en 403. Midlertidige fejl (403/429/timeout) prøves
nu igen op til to gange med pause og en anden YouTube-klient
(tv,web_safari og mweb). Permanente fejl prøves ikke igen.
- Et job der stod som "running", da portalen genstartede, blev hængende for
altid. Med låsen ved vi, at et sådant job er afbrudt, og det lægges i kø
igen ved næste kørsel.
Fejlede job har en "Prøv igen"-knap (og "Prøv alle fejlede igen"), og den
natlige kørsel opdaterer yt-dlp, før den henter.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Hver række under "Hentede videoer" på /admin/yt har nu en Slet-knap (med
bekræftelse; nævner det, hvis videoen er favorit). Sletningen bruger samme
sikkerhedsnet som oprydningen: kun filer portalen selv har hentet, og
undertekster/miniature slettes med. Er det jobbets sidste fil, markeres jobbet
som slettet, og Emby-biblioteket opdateres. Video-id'et bliver i
abonnementets "seen", så en overvåget kanal ikke henter den igen.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
main.py brugte asyncio.create_task i YouTube-ruterne (tilføj video, tilføj
og kør overvågede kanaler), men importen på modulniveau forsvandt 2026-09-20
(9675a5b); kun to funktioner havde en lokal import. Siden da gav "tilføj
overvåget kanal" og "tilføj video" 500 (NameError).
CI kører nu pyflakes og fejler på udefinerede navne, så det fanges før drift.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Messenger på iPhone blokerer også x-safari-https://, så "Åbn i Safari" gjorde
ingenting. I stedet vises to veje, der virker: menuen ⋯ → "Åbn i ekstern
browser", eller "Kopiér adressen" og tilføj feedet i Apple Podcasts
(Bibliotek → ⋯ → Følg en serie via URL). Kopieringen falder tilbage til
execCommand/prompt, hvis clipboard-API'et er spærret. Android beholder
intent://-knappen.
Siden kunne skubbes til siden på iPhone. Afspiller og felter må nu aldrig
blive bredere end kortet, felter har 16 px skrift (ellers zoomer iOS ind),
lange ord brydes, og html/body skjuler vandret overløb.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Med det store omslag (3000 px) viste Messenger et kvadratisk billede i hele
beskedens bredde. Forhåndsvisningen bruger nu cover-lille.jpg (300 px), når
den findes, så den vises som en miniature ved teksten.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
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
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
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
/podcast viser familiens podcasts (Slægtens Stemmer) med afspiller og en
abonnér-knap til Apple Podcasts, Overcast, Pocket Casts og AntennaPod. Podcast-
apps kan ikke logge ind med Authentik, så hver lytter får sin egen hemmelige
nøgle i adressen (/podcast/<nøgle>/<show>.xml); mp3-filer og omslag ligger
under samme nøgle. En nøgle kan trækkes tilbage i /admin/podcast uden at de
andre mærker det, og admin ser hvem der henter hvad.
Episoder kan ligge som kladde: så ses de kun af administratorer, også i deres
eget feed, indtil de udgives fra admin-siden. Adgang: gruppen PODCAST_GROUP
(standard Familie) og admins. Forsiden får et kort, når der er episoder.
Data ligger i DATA/podcast/ og kommer aldrig i git. mutagen læser varigheden.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Efter et tryk på Start kunne siden nå at læse den gamle hvile-status (fx
"Logget ind"), før serveren havde skrevet "starter". Så sprang den tilbage
til knappen og målte først igen efter 30 sekunder, og login-knappen kom
aldrig frem. Nu venter siden i op til et minut på en status, serveren har
skrevet efter trykket, og måler hvert 2. sekund imens.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Et link til fx /admin/claude-login (fra en ntfy) i en browser uden
portal-session gav "403 Kun for administratorer". Tre af dem på /admin fik
CrowdSec til at bandlyse IP'en som admin-probing, midt i et login.
Sidevisninger uden session sendes nu til /auth/login?next=<sti> og efter
login tilbage dertil; next accepteres kun som en sti på denne side.
Fetch- og POST-kald uden session får 401. Logget ind uden admin er
stadig 403. claude-login skriver nu brugernavnet i request-filen.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Portalen blev udrullet ved at kopiere filer ind i drift, og drift og git gled
fra hinanden. Nu er appmappen i drift en git-checkout, og et v*-tag udruller
via Gitea Actions: CD logger ind som deploy-brugeren og kører
deploy/deploy.sh fra tagget. Scriptet afbryder hvis drift afviger fra git,
installerer requirements kun når de er ændret, og ruller tilbage hvis
/healthz ikke svarer efter genstart. Værtsadressen ligger i en repo-variabel.
CI kompilerer Python, parser alle templates og tjekker deploy.sh.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Claude Codes login på værten holder 28 dage, og et langtidstoken kan ikke
bruges til Remote Control, så der skal logges ind igen hver 4. uge. Hidtil
krævede det en terminal for at kopiere en adresse ud og en kode ind igen.
/admin/claude-login viser hvornår login udløber og har en "Start login"-knap.
Værten (claude-login-agent, som spørger portalen, ikke omvendt) starter
`claude auth login`, og siden viser login-adressen; koden fra Claudes side
indsættes på samme side og hentes af værten et par sekunder senere. Portalen
gemmer kun den korte kode, indtil den er hentet; den virker kun sammen med
login-processen på værten (PKCE).
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
I drift blev admin-oversigten 23/6 sat tilbage fra wt-tile-klasserne til
portalens egne .grid/.card-klasser, og de ubrugte .wt-tile-link/.skel-regler
blev fjernet fra portal.css. Repoet fik aldrig den ændring med, så git og
drift har afveget siden. Nu er de ens; udseendet er uændret.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Kommentarer og docstrings nævnte containernumre, værtsplatformen og
edge-hostens adresse og sti. Eksempel-env'en bruger nu en pladsholder.
Ingen adfærd er ændret.
Claude-Session: https://claude.ai/code/session_01GGe7kUhooMFuzY6GaDb2w9
/admin/secrets tager imod et token eller kodeord og krypterer det med det
samme med PVE-værtens offentlige X25519-nøgle (sealed box: HKDF-SHA256 +
ChaCha20-Poly1305, navnet som associated data). Kun ciphertext gemmes;
portalen kan ikke læse det igen. På værten henter secret-drop get <navn>
det én gang og sletter kopien her.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Selvstaendig under-app med egen Authentik-provider (hovedhus-mcp, gruppe
Hovedhus), egen OIDCProxy/noegle/lager og backends fra
data/hovedhus_gateway.json, aldrig fra mcp_registry.json. Ingen statiske
noegler; UserGate kraever navngiven bruger og fejler lukket.
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Udrullet direkte til prod 2026-09-29; importeret her så repo == prod.
Bruger Authentik-flowet weircon-invitation (grupper og engangs via
fixed_data og politikker i flowet).
Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
Fem nye værktøjer i portal-navnerummet: yt_status, yt_download, yt_watch,
yt_unwatch, yt_check_subs. Gatewayen monterer mcp_server under namespace
"portal", så de arver portal_-præfikset og de scopes nøglerne allerede har —
ingen ekstra registrering.
Download og abonnement-tjek returnerer med det samme og arbejder videre i
baggrunden, så et MCP-kald ikke blokerer i minutter på en lang video.
Importerer samtidig mcp_server.py, mcp_gateway.py, mcpkeys.py og
templates/admin_mcp_keys.html, som kun har eksisteret i prod (CT151) og aldrig
i repoet.
ÅRSAG TIL IMPORTEN: den manglende viden i repoet kostede nedetid. Da jeg
udrullede repoets main.py og config.py til prod i går, overskrev jeg
MCP-monteringen (mcp_app.lifespan + route/middleware-overførsel), hele
/admin/mcp-keys-modulet og konstanterne MCP_REGISTRY/MCP_SELF_URL.
weircon.dk/mcp svarede 404 i ~13 timer. Rettet ved at genopbygge begge filer
fra prod-backuppen plus YouTube-ændringerne, ikke omvendt.
Repo og prod er nu identiske bortset fra static/portal.css og
templates/admin.html, hvor repoet er FORAN (wt-*-temaklasser, aldrig udrullet).
Claude-Session: https://claude.ai/code/session_011WxG8pbLj61jYU1MMaD7VC
Playliste-abonnement henter nye videoer efterhånden som de lægges i playlisten.
Kanal-abonnement holder en rullende buffer: cycle X = de X nyeste videoer ligger
klar, og kommer der en ny, ryger den ældste ud.
X tæller kun kanal-abonnementets egne hentninger. Manuelle hentninger,
playliste-abonnementer og favoritter tæller ikke med — og favoritter slettes
aldrig, så en favoritering frigør en plads i cyklussen til næste video.
Tre ting der måtte designes udenom:
1. `seen` husker hvert video-id abonnementet nogensinde har hentet, også efter
videoen er slettet igen. Uden det ville set-reglen slette en video efter 7
dage, hvorefter kanalen stadig har den blandt sine nyeste og henter den igen
— i en uendelig løkke.
2. Playliste og kanal primer forskelligt ved oprettelse. En playliste kan have
hundredvis af videoer, og ønsket var NYE videoer, så første tjek markerer det
eksisterende som kendt uden at hente. En kanal henter derimod de X nyeste med
det samme; det er hele pointen med cycle X. Resten af bagkataloget markeres
som kendt, så den aldrig begynder at hente gammelt indhold.
3. enforce_cycles rører intet hvis Emby ikke svarer. Uden favorit-data kunne den
komme til at slette noget fredet — samme konservative holdning som mediaclean.
Downloads gemmer nu video-id og upload-dato, så cyklussen kan sortere efter hvor
ny videoen ER, ikke hvornår vi tilfældigvis hentede den.
Timeren kører i rækkefølgen: tjek abonnementer -> hent -> håndhæv cyklus ->
slet sete. Cyklussen skal håndhæves efter hentningen, ellers ser den et halvt
bibliotek.
Claude-Session: https://claude.ai/code/session_011WxG8pbLj61jYU1MMaD7VC
Nyt admin-modul /admin/yt: indsæt en YouTube-URL, og videoen hentes med yt-dlp
til Emby-biblioteket "YouTube" (/media/yt-offline) — klar til offline-brug.
Oprydningen er bevidst modsat mediaclean.py: dér tæller selv delvis afspilning
som "set" for at frede alt der er rørt, her slettes kun videoer der er eksplicit
markeret som set (Emby UserData.Played). Halvt sete videoer bliver liggende.
Favoritter slettes aldrig, og en favoritering i karensperioden freder igen.
Sikkerhedsnet: oprydningen rører kun filer portalen selv har hentet (registreret
med fuld sti i yt_state.json). Det eksisterende YT-bibliotek er urørligt, og
download-træet er lagt uden for det, så Emby ikke indekserer dobbelt.
Standardvalg:
- H.264/AAC foretrækkes frem for AV1. Pascal-GPU'en i CT100 kan ikke afkode AV1,
og mange afspillere kan det slet ikke offline. Koster ~1,8x diskplads.
- SponsorBlock klipper sponsorater ud og beholder resten som kapitler.
- Undertekster hentes hvis de findes, men er aldrig et krav: YouTube rate-limiter
undertekst-endpointet uafhængigt af videoen, så en 429 dér ville ellers vælte
hele hentningen. Fald-tilbage henter videoen uden undertekster.
- Metadata, miniature og kapitler indlejres, så Emby har noget at vise.
Alt kan redigeres i UI'et. Natlig timer (yt-offline.timer) tømmer køen og rydder op.
Claude-Session: https://claude.ai/code/session_011WxG8pbLj61jYU1MMaD7VC