Der Anlass ist eine Beobachtung aus dem Betrieb: der Bot kam mit voller
Aussteuerung in den Kanal, und wer schon drin sass, ist erschrocken. Dass
sich jeder den Bot fuer sich leiser drehen kann, hilft erst *nachdem* es zu
laut war. Also liegt die Vorgabe jetzt bei 50 %, und lauter machen kostet
zwei Knopfdruecke.
Dafuer musste die Tonkette umgebaut werden. Bisher lief Opus unveraendert
durch (ffmpeg -> Ogg/Opus -> Discord). Das war eine Stufe sparsamer, aber an
fertigen Opus-Paketen laesst sich die Lautstaerke nicht drehen -- dafuer muss
man an die Abtastwerte. Jetzt: ffmpeg gibt PCM aus, die Sprachbibliothek
regelt und kodiert.
Der alte Einwand im Dateikopf ("sonst muesste @discordjs/opus mit ins Image,
und das will gebaut werden") stimmt weiterhin -- fuer @discordjs/opus.
Nachgemessen am 14.08.2026 in node:22-slim:
@discordjs/opus npm i bricht ab, kein Prebuild fuer node-v127/glibc-2.36,
node-gyp will bauen, im Image fehlen die Werkzeuge
opusscript reines JavaScript, installiert in 740 ms, kein Compiler
0,3 % eines Kerns fuers Kodieren eines Dauerstroms
Der Umweg ueber Opus-Dekodieren entfaellt dabei ganz, ffmpeg liefert ja schon
Abtastwerte. Deshalb 0,3 % und nicht 2,9 %, was Dekodieren und Kodieren
zusammen gekostet haetten.
Die Kette ist mit echtem ffmpeg gegen einen Sinuston geprueft: 200
Opus-Pakete fuer 4,0 s Ton, kein Versatz, und die Lautstaerke laesst sich
mitten im Strom aendern. Genau darum geht der Weg ueber PCM und nicht ueber
ffmpegs volume-Filter -- der haette bei jedem Klick einen Neustart des
Prozesses gebraucht, also eine hoerbare Luecke.
Am Panel zwei Knoepfe in der bestehenden Reihe, Schritte von 10, Bereich 10
bis 200. Am Anschlag graut der Knopf aus, statt sich druecken zu lassen und
nichts zu tun. Die Rueckmeldung ist die neu gezeichnete Tafel mit dem neuen
Prozentwert.
Der Wert steht in den Einstellungen, nicht in radio_state: der Merker dort
wird beim Stoppen geloescht, die Lautstaerke soll das ueberleben.
Dazu der Kanal-Status: was laeuft, steht jetzt auch in der Statuszeile des
Sprachkanals und wird mit dem Titel nachgefuehrt.
ACHTUNG, das ist ein UNDOKUMENTIERTER Endpunkt.
PUT /channels/{id}/voice-status -- Discord hat ihn nie in die API-Doku
aufgenommen, die Pull Requests dazu liegen seit 2023 offen, und discord.js
hat die Umsetzung als "not planned" geschlossen. Roher Aufruf ueber
client.rest also, und er kann ohne Ankuendigung verschwinden.
Deshalb ist er als Beiwerk gebaut: faellt er aus, sagt er einmal warum und
schweigt dann -- ein Fehler alle 30 Sekunden waere kein Hinweis mehr,
sondern Rauschen. Das Radio laeuft in jedem Fall weiter. Der naechste
Sender-Start versucht es erneut, falls das Recht inzwischen erteilt wurde.
Das Recht heisst "Kanalstatus festlegen" und sitzt auf Bit 48. discord.js
kennt es nicht einmal als Konstante, deshalb steht die Zahl im Quelltext.
Ungeprueft, weil dafuer ein echter Sprachkanal noetig ist: ob das Recht dem
Bot hier tatsaechlich erteilt ist. Faellt es aus, steht der Grund im Log.
Beim Stoppen wird die Zeile geleert -- sonst behauptet sie stundenlang, es
liefe ein Lied, das laengst vorbei ist.
Nebenbei: prism-media wurde direkt importiert, stand aber nie in der
package.json. Der Import ist mit dem Ogg-Auspacker weggefallen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Mitschnitt von 12:33 hat es benannt:
Netzteil: Websocket auf -> Anmeldung -> zu -> Websocket zu (4017) -> zu
4017 heisst DAVE. Discord erzwingt seit dem 01.03.2026 Ende-zu-Ende-
Verschluesselung in allen Nicht-Stage-Sprachkanaelen und weist Gegenstellen,
die das nicht koennen, sofort nach der Anmeldung ab -- lange bevor irgendein
UDP-Paket fliegt. Nachgeschlagen bei Discord, nicht gedeutet.
Gemessen, warum es uns traf: @discordjs/voice 0.18.0 kennt DAVE ueberhaupt
nicht (null Fundstellen im Bundle) und spricht Sprach-Gateway v4. Es meldet
deshalb max_dave_protocol_version: 0. 0.19.2 spricht v8, bringt
@snazzah/davey als feste Abhaengigkeit mit und hat 48 Fundstellen.
Also hochgezogen. Im gebauten Abbild gegengeprueft, weil das native Modul
plattformabhaengig ist und der Lock auf Windows entstanden ist:
DAVE Libraries
- @snazzah/davey: 0.1.12
@snazzah/davey-linux-x64-gnu steht im Lock, node:22-slim ist Debian/glibc,
das Modul laedt im Abbild. engines steht jetzt auf >=22.12.0 -- das verlangt
0.19.2, und ">=20" waere ab jetzt gelogen.
Und ein Fehler in der frischen Diagnose selbst, den erst der Ernstfall zeigte:
sie hat den Schritt nicht benannt, obwohl der Mitschnitt ihn enthielt. Das
Netzteil schliesst sich erst und meldet dann den Code, also endet jeder
Mitschnitt auf "zu" -- und genau den nahm die Auswertung als letzten Schritt.
Damit lief alles in "Ursache offen". Jetzt wird "zu" uebersprungen; die
Gegenprobe mit 4009 landet wieder beim UDP-Handschlag statt im Nichts.
Nebenbei stand "zu" zweimal im Mitschnitt, einmal vor und einmal nach dem
Schliesscode: die Entdopplung verglich mit dem letzten Eintrag statt mit dem
letzten Schritt.
Der Weg hierher gehoert zur Sache: erst hiess es "ausgehendes UDP dicht" --
geraten. Dann wurde gemessen, dass UDP rauskommt, und die Meldung sagte
ehrlich "Ursache offen". Diese eine ehrliche Meldung hat den Schliesscode
sichtbar gemacht, und der war die Antwort. Eine Pruefung, die nichts sehen
kann, meldet nicht "sauber" und auch nicht "UDP".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/radio holt den Bot in den Sprachkanal und postet eine Tafel: Auswahlmenue
mit allen Sendern, Stopp, und ein Embed mit dem laufenden Titel, das sich
alle 30 Sekunden selbst nachfuehrt.
Der Titel kommt aus dem Icecast-Strom selbst (Icy-MetaData). Bewusst so und
nicht ueber eine Sender-API: das funktioniert bei jedem Strom, den jemand
eintraegt, und nicht nur bei den drei, die ich kenne. Leere Titel-Bloecke
heissen "unveraendert", deshalb werden bis zu drei gelesen.
Die Tonkette braucht keine Opus-Bibliothek: ffmpeg holt das MP3 und gibt
direkt Ogg/Opus aus, prism-media packt nur noch aus. Gemessen: 600 Pakete in
fuenf Sekunden. Sonst muesste @discordjs/opus mit ins Image, und das will
gebaut werden. Neu im Image ist nur ffmpeg.
44 RauteMusik-Sender sind ab Werk eingetragen — dieselben, die im LS25 und
ETS als Bordradio laufen. Die Liste ist nicht abgeschrieben, sondern
gemessen: jede Adresse einzeln angefragt, der Name kommt aus icy-name.
Doppelgaenger sind raus (deutschrap-charts und wackenradio liefern denselben
Strom wie deutschrap und metal). Eingetragen wird einmal, solange die Liste
leer ist — wer loescht, behaelt es geloescht.
Ein Auswahlmenue fasst nur 25 Eintraege. Bei 44 Sendern haette ein Deckel
die letzten 19 stillschweigend verschluckt; stattdessen mehrere Menues,
benannt nach ihrem ersten und letzten Sender.
Der Bot geht raus, wenn der Kanal leer ist, und kommt nach einem Neustart in
den Kanal zurueck — sonst beendet jeder Deploy die Musik endgueltig.
Kein YouTube. Genau daran sind Groovy und Rythm gestorben, und was davon
uebrig ist, ist ein Dauerlauf gegen kaputte Extraktoren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Willkommens-Karten: gerendertes PNG (SVG → sharp) mit Avatar im
Neon-Ring, Willkommens-Schriftzug und Member-Nummer; Fallback aufs
bisherige Embed wenn das Rendering scheitert; Dockerfile installiert
fonts-dejavu-core für die Text-Darstellung
- Geburtstags-System: /geburtstag setzen|entfernen (birthdays-Tabelle),
tägliche Runde ab 09:00 Europe/Berlin (Doppel-Post-Schutz über
last_birthday_run), Gratulations-Embed + Tages-Rolle (wird am
nächsten Morgen wieder abgeräumt); Kanal + Rolle im Community-Tab
- Devlog-Permalinks: /devlogs/:id als eigene Seite (GET /api/devlogs/:id),
Link-Symbol an jeder Karte, Link-kopieren-Button; der Server injiziert
Open-Graph-Tags ins SPA-HTML — Devlog-Links zeigen Titel, Anriss und
Bild, alle anderen Seiten bekommen Default-Tags (auch die Startseite)
- Auto-Publish: maybeCrosspost() veröffentlicht Devlog-, Release-,
Composer- und geplante Posts automatisch in Ankündigungs-Kanälen
- brandEmbed: leere Avatar-URL crasht nicht mehr die Footer-Validierung
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- POST /webhooks/devlog/<secret> — Discord-Webhook-kompatibel (JSON + Multipart),
devlog.py braucht nur die neue URL in tools/.devlog_webhook
- Bot postet Embed in Brand-Gelb mit 'D4RKST3R // DEVLOG'-Footer, bis zu 4 Bilder
als Grid (Embed-Gruppierung über gemeinsame URL)
- Direkt-Archivierung inkl. Bilder (kein Umweg über den Live-Listener)
- Neue Env-Var DEVLOG_POST_SECRET, README-Abschnitt neu geschrieben
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Discord-OAuth2-Login (identify-Scope, CSRF-State, signierte Session-Cookies, keine Token-Speicherung)
- REST-API: /api/devlogs (öffentlich), /api/commits (nur ADMIN_DISCORD_ID), /api/me
- React + Vite Frontend: Devlog-Archiv mit Mini-Markdown-Renderer, Commit-Tabelle, dunkles EcoGame-Theme
- Fastify liefert frontend/dist mit SPA-Fallback aus; Vite-Dev-Proxy für lokale Entwicklung
- Multi-Stage-Dockerfile (Frontend-Build im Image), neue Env-Vars in Compose + .env.example
- README: OAuth2-Setup (Redirect-URLs, Client Secret) und Frontend-Workflow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fastify-Webserver mit /health und /webhooks/gitea (HMAC-SHA256-Signaturprüfung, timing-safe)
- Push-Commits werden in SQLite gespeichert (Dedupe per SHA) und als Embed gepostet
- Dockerfile auf node:22-slim (glibc-Prebuilds für better-sqlite3), Port 3080 published
- README: Anleitung für Cloudflare-DNS, Nginx Proxy Manager (bot.d4rkst3r.de) und Gitea-Webhook
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- discord.js v14 Bot mit Slash-Command-Registry (Guild oder global)
- Konfiguration über .env mit Validierung (config.js)
- Dockerfile + docker-compose.yml für Portainer-Deployment
- README mit Schritt-für-Schritt-Anleitung (Discord Developer Portal)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>