Jede Sprachnachricht in einer Fassung, die JEDES Geraet abspielt
Miss im Support, Meldung #12 -- fuenf Runden seit dem 23.09.: „Bei mir laesst sich die Nachricht nicht abspielen." Zuletzt: „Jetzt laedt es die ganze Zeit." Bei allen anderen ging es. GEMESSEN, BEVOR GEBAUT WURDE · Alle sechs Sprachnachrichten im Haus sind audio/webm (Opus). · Miss' Geraet: iPhone, iOS 18.7, WebKit. · Seit der Umstellung am 02.10. (neue Aufnahmen bevorzugen MP4) wurde KEINE EINZIGE neue aufgenommen. Ihr Problem betrifft also ausschliesslich die sechs alten Dateien -- und jede kuenftige aus einem Firefox, der nichts anderes kann. WAS ICH NICHT MESSEN KONNTE, und das gehoert dazu: Ob WebKit WebM/Opus abspielen kann, laesst sich auf diesem Rechner nicht nachsehen -- Playwrights WebKit startet hier nicht (libegl.dll fehlt). Die Vermutung „iPhones koennen kein WebM" ist begruendet, aber von mir nicht gemessen. GENAU DESHALB STELLT DIE LOESUNG DIE FRAGE NICHT. Statt zu raten, welches Geraet welchen Behaelter kann, legt der Server neben jede Sprachnachricht eine zweite Fassung in dem Format, bei dem sich seit zwanzig Jahren alle einig sind: AAC in MP4. Gibt es sie, wird sie abgespielt -- bei jedem, nicht nur auf iPhones. Kein Geraetename im Code, keine Liste, die altert. helfer-ffmpeg.mjs ffmpeg finden, umwandeln, drei Ausgaenge beim Hochladen nebenher, nicht davor: Die Nachricht wartet nicht auf ffmpeg toeneNachruesten() das Netz darunter -- fuer die sechs von frueher und fuer den Fall, dass eine Umwandlung einmal nicht geklappt hat ?form=mp4 dieselbe Route, dieselben Rechte; eine zweite haette dieselbe Sichtbarkeitspruefung ein zweites Mal gebraucht ffmpeg IST ABGESPROCHEN INSTALLIERT (Debian 7.1.5, auf Nachfrage freigegeben). Fehlt es, passiert nichts Schlimmes: Die Sprachnachricht geht wie bisher im Originalformat hinaus, und es steht EINMAL eine Zeile im Protokoll -- nicht bei jedem Hochladen. DREI ENTSCHEIDUNGEN, DIE NICHT NAHELIEGEND WAREN 1. `-movflags +faststart` IST NICHT KOSMETIK. Ohne es steht die Inhaltsuebersicht einer MP4 am ENDE. Das Abspielgeraet muss dann erst bis ans Ende lesen, bevor es anfangen kann -- genau das „laedt die ganze Zeit" aus Miss' Runde 5. Geprueft wird die Reihenfolge der Kaesten in der Datei, nicht die Zeile im Aufruf. 2. DAS ORIGINAL BLEIBT LIEGEN. Ohne `?form=mp4` kommt weiter die WebM. Sie ist das, was aufgenommen wurde; sie durch eine Umrechnung zu ersetzen hiesse, das Original wegzuwerfen. 3. EIN GEMEINSAMER LOESCHER. Zwei Stellen entfernen Anhaenge (Gespraech wegraeumen, Nachricht zuruecknehmen). Beide nahmen genau eine Datei -- ab heute waere bei jeder geloeschten Sprachnachricht eine m4a liegengeblieben, ohne Zeile, die auf sie zeigt. Dieselbe Luecke hatte ich gestern bei den Supportbildern gefunden; hier steht sie von Anfang an an EINER Stelle. ZWEI FUNDE DER PRUEFUNG, BEIDE MEINE EIGENEN · `anhang_datei` STAND NICHT IN DER ABFRAGE der Nachrichtenliste. Die Funktion, die auf der Platte nachsieht, bekam deshalb nichts und sagte brav „gibt es keine zweite Fassung" -- fuer JEDE Nachricht. Alles war gebaut, nichts kam an. Gefunden hat das die Pruefung, nicht das Lesen. · Mein erster Anlauf mass die Aufnahme ueber den Knopf -- und die ist seit dem 02.10. schon MP4, braucht also gar keine zweite Fassung. Die Pruefung wurde rot und hatte recht: Gemessen werden muss der Fall, den es bei Miss gibt. Jetzt nimmt sie ausdruecklich eine WebM auf. Dazu zweimal derselbe alte Tritt: ein Gegen-Apostroph in einem Kommentar INNERHALB eines Template-Literals. Der Server startet dann gar nicht. Steht jetzt als Warnung an beiden Stellen. GEPRUEFT -- pruef-chat-anhaenge 146 -> 163 ok Eine echte WebM/Opus-Aufnahme (31 972 Bytes) wird hochgeladen; die zweite Fassung entsteht von selbst und kommt als audio/mp4 heraus, mit „ftyp"-Marke, mit Accept-Ranges. ffprobe sagt: Original Opus, Zweitfassung AAC, 2,40 s gegen 2,46 s. Die Inhaltsuebersicht steht bei Byte 32, die Tondaten ab 1270 -- also vorne. Beim Loeschen gehen beide Dateien. Gegenproben: ein Bild bekommt keine zweite Fassung; eine halbierte Laenge waere aufgefallen; das Original bleibt unter seiner eigenen Adresse abrufbar. Der dritte Ausgang hat einen eigenen Zaehler: Fehlt ffmpeg, steht „KONNTE NICHT NACHSEHEN" mit Anleitung da -- nicht gruen und nicht rot. Und der Block raeumt hinter sich auf: Mein Probebild liess eine spaetere Pruefung rot werden, weil es die Gespraechsliste veraendert hatte. Eine Pruefung, die den Stand fuer die naechste verschiebt, ist schlimmer als keine. Dazu gruen: pruef-struktur 102 (92 Module) · pruef-ports 10 · pruef-gifs 17 · pruef-treffchat 114 · pruef-chat-optik · pruef-aufbewahrung 45. NUR DAS AGENTURHAUS? NEIN -- der Chat gehoert beiden Haeusern, und die Aenderung gilt fuer beide gleich. Sie aendert nichts an Sichtbarkeit oder Rechten: Wer eine Nachricht hoeren darf, hoert sie jetzt in einem Format, das sein Geraet kann. OFFEN BLEIBT DIE ANTWORT VON MISS. Ob es bei ihr jetzt laeuft, weiss nur sie -- deshalb steht ihre Meldung weiter offen und nicht auf erledigt. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
/* =====================================================================
|
||||
helfer-ffmpeg.mjs — eine Sprachnachricht, die JEDES Gerät abspielt
|
||||
|
||||
=====================================================================
|
||||
WARUM ES DIESE DATEI GIBT (03.10.2026)
|
||||
|
||||
Miss im Support, Meldung #12 — fünf Runden lang: „Bei mir lässt
|
||||
sich die Nachricht nicht abspielen." Zuletzt: „Jetzt lädt es die
|
||||
ganze Zeit." Bei allen anderen ging es.
|
||||
|
||||
GEMESSEN, BEVOR GEBAUT WURDE:
|
||||
· Alle sechs Sprachnachrichten im Haus sind `audio/webm`.
|
||||
· Miss' Gerät: iPhone, iOS 18.7, WebKit.
|
||||
· Seit der Umstellung am 02.10. (neue Aufnahmen bevorzugen MP4)
|
||||
wurde KEINE EINZIGE neue aufgenommen — ihr Problem betrifft
|
||||
also ausschließlich die sechs alten Dateien.
|
||||
|
||||
WAS ICH NICHT MESSEN KONNTE, und das gehört hierher: Ob WebKit
|
||||
WebM/Opus abspielen kann, lässt sich auf diesem Rechner nicht
|
||||
nachsehen — Playwrights WebKit startet hier nicht (`libegl.dll`
|
||||
fehlt). Die Vermutung „iPhones können kein WebM" ist deshalb
|
||||
begründet, aber nicht von mir gemessen.
|
||||
|
||||
GENAU DESHALB IST DIE LÖSUNG HIER EINE, DIE DIE FRAGE NICHT
|
||||
STELLT. Statt zu raten, welches Gerät welchen Behälter kann, legt
|
||||
der Server neben jede Sprachnachricht eine zweite Fassung in dem
|
||||
Format, das seit zwanzig Jahren überall läuft: AAC in MP4. Wer sie
|
||||
hat, bekommt sie. Kein Gerätename im Code, keine Liste, die
|
||||
altert.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM EIN FREMDES PROGRAMM UND NICHT NODE
|
||||
|
||||
Node kann kein Audio umwandeln, und ein Paket dafür wäre die
|
||||
einzige nennenswerte Abhängigkeit in einem Haus, das ohne Baulauf
|
||||
auskommt. ffmpeg ist auf dem Server installiert (7.1.5, Debian) —
|
||||
abgesprochen, nicht nebenbei eingerichtet.
|
||||
|
||||
DREI AUSGÄNGE, wie überall: umgewandelt / nicht umwandelbar /
|
||||
ffmpeg nicht auffindbar. Der dritte ist KEIN Fehler: Fehlt ffmpeg,
|
||||
wird die Sprachnachricht wie bisher im Originalformat
|
||||
ausgeliefert. Niemand verliert etwas, was er vorher hatte — es
|
||||
kommt nur niemand dazu.
|
||||
===================================================================== */
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import { execFile } from "node:child_process";
|
||||
import { delimiter, join, extname, basename, dirname } from "node:path";
|
||||
|
||||
/* Die Endung der Zweitfassung. AAC in MP4 — das Format, bei dem sich
|
||||
iPhone, Android, Windows, Mac und jeder Browser einig sind. */
|
||||
export const ZWEITFASSUNG_ENDUNG = ".m4a";
|
||||
export const ZWEITFASSUNG_TYP = "audio/mp4";
|
||||
|
||||
/* 96 kbit/s für Sprache. Opus holt bei 32 kbit/s dasselbe heraus,
|
||||
AAC braucht mehr — und eine Sprachnachricht von einer Minute wiegt
|
||||
damit 700 KB statt 240. Das ist der Preis dafür, dass sie überall
|
||||
läuft, und er ist es wert. Niedriger wäre hörbar dumpf; höher
|
||||
brächte bei einem Handymikrofon nichts. */
|
||||
const RATE = "96k";
|
||||
|
||||
/* Nach dieser Zeit wird abgebrochen. Eine Sprachnachricht von einer
|
||||
Minute braucht auf dem Server unter einer Sekunde; wer hier in die
|
||||
Frist läuft, hat ein anderes Problem, und ein hängender Prozess
|
||||
wäre das schlimmere. */
|
||||
const FRIST_MS = 60_000;
|
||||
|
||||
const BEKANNTE_ORTE = [
|
||||
"/usr/bin/ffmpeg",
|
||||
"/usr/local/bin/ffmpeg",
|
||||
"C:/Program Files/ffmpeg/bin/ffmpeg.exe",
|
||||
];
|
||||
|
||||
let gemerkt = null;
|
||||
let schonGesagt = false;
|
||||
|
||||
/* IM PATH SUCHEN, OHNE `where` ODER `which` ZU BRAUCHEN — dieselbe
|
||||
Überlegung wie in helfer-openssl.mjs: Beide sind selbst Programme
|
||||
und können genauso fehlen. */
|
||||
function imPfad() {
|
||||
const pfade = (process.env.PATH || "").split(delimiter).filter(Boolean);
|
||||
const namen = process.platform === "win32" ? ["ffmpeg.exe", "ffmpeg"] : ["ffmpeg"];
|
||||
for (const p of pfade) {
|
||||
for (const n of namen) {
|
||||
const voll = join(p, n);
|
||||
if (existsSync(voll)) return voll;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Wo liegt ffmpeg — oder `null`.
|
||||
*
|
||||
* GIBT `null` ZURUECK UND WIRFT NICHT. Anders als bei openssl ist
|
||||
* das Fehlen hier kein Abbruchgrund: Die Sprachnachricht
|
||||
* funktioniert auch ohne Zweitfassung, nur eben nicht überall. Ein
|
||||
* geworfener Fehler würde das Hochladen scheitern lassen — und
|
||||
* damit aus einer fehlenden Verbesserung einen Ausfall machen. */
|
||||
export function ffmpegPfad() {
|
||||
if (gemerkt !== null) return gemerkt || null;
|
||||
gemerkt = imPfad() || BEKANNTE_ORTE.find((o) => existsSync(o)) || "";
|
||||
if (!gemerkt && !schonGesagt) {
|
||||
schonGesagt = true;
|
||||
/* EINMAL SAGEN, NICHT BEI JEDER NACHRICHT. Eine Zeile, die bei
|
||||
jedem Hochladen kommt, liest nach dem dritten Mal niemand
|
||||
mehr — und deckt dabei die echten zu. */
|
||||
console.log("[ton] ffmpeg nicht gefunden – Sprachnachrichten bekommen keine "
|
||||
+ "zweite Fassung. Auf Debian: sudo apt-get install -y ffmpeg");
|
||||
}
|
||||
return gemerkt || null;
|
||||
}
|
||||
|
||||
/** Wo liegt ffprobe — oder `null`.
|
||||
*
|
||||
* ES LIEGT IMMER NEBEN ffmpeg (dasselbe Paket, derselbe Ordner),
|
||||
* deshalb wird es dort gesucht und nicht noch einmal im PATH.
|
||||
* Gebraucht wird es nur von den Prüfungen: Nur ffprobe kann sagen,
|
||||
* ob in der erzeugten Datei wirklich AAC steckt und ob sie so lang
|
||||
* ist wie das Original. Ohne diese Frage wäre „die Datei ist da"
|
||||
* ein grüner Haken über einer Datei, die niemand abspielen kann. */
|
||||
export function ffprobePfad() {
|
||||
const ff = ffmpegPfad();
|
||||
if (!ff) return null;
|
||||
const nachbar = ff.replace(/ffmpeg(\.exe)?$/i, (m) => m.replace(/ffmpeg/i, "ffprobe"));
|
||||
return existsSync(nachbar) ? nachbar : null;
|
||||
}
|
||||
|
||||
/** Der Pfad, unter dem die Zweitfassung einer Datei liegt (oder läge). */
|
||||
export function zweitfassungPfad(pfad) {
|
||||
return join(dirname(pfad), basename(pfad, extname(pfad)) + ZWEITFASSUNG_ENDUNG);
|
||||
}
|
||||
|
||||
/** Braucht diese Datei überhaupt eine Zweitfassung?
|
||||
*
|
||||
* NEIN, WENN SIE SCHON DAS ZIELFORMAT IST. Eine m4a nach m4a zu
|
||||
* wandeln kostet Rechenzeit und Qualität für genau nichts — und
|
||||
* seit dem 02.10. nehmen die meisten Geräte ohnehin MP4 auf. */
|
||||
export function brauchtZweitfassung(pfad) {
|
||||
return extname(pfad).toLowerCase() !== ZWEITFASSUNG_ENDUNG;
|
||||
}
|
||||
|
||||
/** Eine Tondatei in AAC/MP4 danebenlegen.
|
||||
*
|
||||
* Gibt ein Versprechen auf `true` (liegt jetzt da), `false` (ging
|
||||
* nicht) oder `null` (ffmpeg fehlt — konnte nicht nachsehen).
|
||||
*
|
||||
* SIE WIRFT NIE. Der Aufrufer hängt an einem Hochladen, und ein
|
||||
* Fehler beim Umwandeln darf die Nachricht nicht verhindern.
|
||||
*
|
||||
* `-movflags +faststart` IST NICHT KOSMETIK: Ohne es steht die
|
||||
* Inhaltsübersicht einer MP4 am ENDE der Datei. Ein Abspielgerät
|
||||
* muss dann erst bis ans Ende lesen, bevor es anfangen kann — bei
|
||||
* einer Teilanfrage über eine mobile Verbindung ist das genau das
|
||||
* „lädt die ganze Zeit", über das sich Miss beklagt hat. */
|
||||
export function zweitfassungBauen(pfad) {
|
||||
return new Promise((fertig) => {
|
||||
const ff = ffmpegPfad();
|
||||
if (!ff) return fertig(null);
|
||||
const ziel = zweitfassungPfad(pfad);
|
||||
/* SCHON DA HEISST FERTIG. Der Nachrüstlauf geht mehrmals über
|
||||
denselben Ordner; ohne diese Zeile würde er jedes Mal alles
|
||||
neu wandeln. */
|
||||
if (existsSync(ziel)) return fertig(true);
|
||||
execFile(ff, [
|
||||
"-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-i", pfad,
|
||||
/* KEIN BILD. Eine WebM kann ein Titelbild enthalten; in einer
|
||||
Audiodatei hätte es nichts verloren und bringt ffmpeg bei
|
||||
manchen Dateien zum Stolpern. */
|
||||
"-vn",
|
||||
"-c:a", "aac", "-b:a", RATE,
|
||||
"-movflags", "+faststart",
|
||||
/* NICHT ÜBERSCHREIBEN. Eine halbe Datei von einem
|
||||
abgebrochenen Lauf soll auffallen, nicht stillschweigend
|
||||
ersetzt werden -- und `existsSync` oben hat ohnehin schon
|
||||
gefragt. */
|
||||
"-n", ziel,
|
||||
], { timeout: FRIST_MS }, (fehler) => {
|
||||
if (fehler) {
|
||||
console.error(`[ton] Zweitfassung fuer ${basename(pfad)} ging nicht:`,
|
||||
String(fehler.message || fehler).split("\n")[0]);
|
||||
return fertig(false);
|
||||
}
|
||||
fertig(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
+5
-1
@@ -66,7 +66,7 @@ import { ampelRouter } from "./workspace-ampel.js";
|
||||
import { checklisteRouter } from "./workspace-checkliste.js";
|
||||
import { teamlageRouter } from "./workspace-teamlage.js";
|
||||
import { pushRouter, pushStarten } from "./workspace-push.js";
|
||||
import { chatRouter } from "./workspace-chat.js";
|
||||
import { chatRouter, toeneNachruestenStarten } from "./workspace-chat.js";
|
||||
import { anrufRouter } from "./workspace-anruf.js";
|
||||
import { videoRouter, videoWacheStarten } from "./workspace-video.js";
|
||||
import { bewerbungRouter } from "./workspace-bewerbung.js";
|
||||
@@ -370,6 +370,10 @@ app.use(contentRouter);
|
||||
Systemdienst laeuft und warum eine Dateikopie hier falsch waere. */
|
||||
sicherungStarten();
|
||||
aufbewahrungStarten();
|
||||
/* Sprachnachrichten in eine Fassung bringen, die jedes Geraet
|
||||
abspielt (Miss im Support, Meldung #12). Der Lauf meldet sich
|
||||
nur, wenn er etwas getan hat. */
|
||||
toeneNachruestenStarten();
|
||||
/* Sieht taeglich nach, ob die verlinkten Videos bei TikTok noch da
|
||||
sind. Ein Knopf, der ins Leere fuehrt, ist schlimmer als kein
|
||||
Knopf -- man klickt ihn ja. */
|
||||
|
||||
@@ -105,6 +105,11 @@ const PROTOKOLL = join(process.cwd(), "pruef-chat-anhaenge-lauf.txt");
|
||||
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
|
||||
const melde = (z) => { console.log(z); try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ } };
|
||||
let fehler = 0, geprueft = 0;
|
||||
/* DER DRITTE AUSGANG BRAUCHT EINEN ZAEHLER (03.10.2026). „Konnte
|
||||
nicht nachsehen" ist weder in Ordnung noch kaputt -- ohne eigene
|
||||
Zahl verschwaende es zwischen den gruenen Zeilen, und am Ende
|
||||
stuende „alles in Ordnung" ueber etwas, das niemand gemessen hat. */
|
||||
let nichtNachsehbar = 0;
|
||||
const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
/* ---- Bestand ------------------------------------------------------------ */
|
||||
@@ -1063,6 +1068,259 @@ async function tonAufnehmen(seite, ms) {
|
||||
ok((zeile?.anhang_dauer || 0) >= 2000 && (zeile?.anhang_dauer || 0) < 20000,
|
||||
` und die Länge steht dabei (${zeile?.anhang_dauer} ms)`);
|
||||
|
||||
/* ==== DIE ZWEITE FASSUNG, DIE JEDES GERAET ABSPIELT (03.10.2026)
|
||||
==================================================================
|
||||
|
||||
Miss im Support, Meldung #12, fuenf Runden: „Bei mir laesst sich
|
||||
die Nachricht nicht abspielen." Zuletzt: „Jetzt laedt es die
|
||||
ganze Zeit." Alle sechs Sprachnachrichten im Haus waren WebM.
|
||||
|
||||
AUSDRUECKLICH EINE WEBM, und das ist hier der Punkt. Die
|
||||
Aufnahme ueber den Knopf weiter oben liefert seit dem 02.10.
|
||||
eine MP4 -- die braucht keine zweite Fassung, und der erste
|
||||
Anlauf dieser Pruefung mass deshalb NICHTS und wurde rot. Die
|
||||
Pruefung hatte recht, die Erwartung war falsch: Gemessen werden
|
||||
muss der Fall, den es bei Miss gibt, und das ist die WebM von
|
||||
frueher (oder von einem Firefox, der nichts anderes kann).
|
||||
|
||||
WAS GEMESSEN WIRD, IST NICHT „die Datei ist da". Das waere ein
|
||||
gruener Haken ueber etwas, das niemand abspielen kann. Mit
|
||||
ffprobe wird gefragt, WAS drinsteckt: AAC, in einem MP4, so lang
|
||||
wie das Original -- und mit der Inhaltsuebersicht VORNE.
|
||||
|
||||
DER DRITTE AUSGANG: Ohne ffmpeg gibt es keine zweite Fassung.
|
||||
Das ist kein Fehler des Programms und wird auch nicht
|
||||
stillschweigend uebersprungen -- es steht als eigene Zeile da,
|
||||
mit der Anleitung dazu.
|
||||
================================================================== */
|
||||
{
|
||||
const { ffmpegPfad, ffprobePfad, zweitfassungPfad } =
|
||||
await import("./helfer-ffmpeg.mjs");
|
||||
const { execFileSync } = await import("node:child_process");
|
||||
const { readFileSync: lies, statSync: wiegt } = await import("node:fs");
|
||||
|
||||
if (!ffmpegPfad()) {
|
||||
melde(" -- KONNTE NICHT NACHSEHEN: ffmpeg fehlt auf diesem Rechner.");
|
||||
melde(" Ohne ffmpeg bekommt keine Sprachnachricht eine zweite");
|
||||
melde(" Fassung -- die Pruefung kann nicht sagen, ob sie");
|
||||
melde(" richtig waere. Auf Debian: sudo apt-get install -y ffmpeg");
|
||||
nichtNachsehbar++;
|
||||
} else {
|
||||
/* EINE ECHTE WEBM, vom Browser aufgenommen -- nicht
|
||||
zusammengebaut. `tonAufnehmen` fragt der Reihe nach und nimmt
|
||||
das erste Format, das geht; die Liste beginnt mit WebM. */
|
||||
const webmAuf = await dogi.seite.evaluate(async (ms) => {
|
||||
const spur = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
if (!MediaRecorder.isTypeSupported("audio/webm;codecs=opus")) {
|
||||
spur.getTracks().forEach((t) => t.stop());
|
||||
return { typ: "", b64: "", bytes: 0 };
|
||||
}
|
||||
const r = new MediaRecorder(spur, { mimeType: "audio/webm;codecs=opus" });
|
||||
const teile = [];
|
||||
r.ondataavailable = (e) => { if (e.data.size) teile.push(e.data); };
|
||||
const fertig = new Promise((f) => { r.onstop = f; });
|
||||
r.start();
|
||||
await new Promise((f) => setTimeout(f, ms));
|
||||
r.stop();
|
||||
await fertig;
|
||||
spur.getTracks().forEach((t) => t.stop());
|
||||
const blob = new Blob(teile, { type: "audio/webm" });
|
||||
const puffer = new Uint8Array(await blob.arrayBuffer());
|
||||
let roh = "";
|
||||
for (const b of puffer) roh += String.fromCharCode(b);
|
||||
return { typ: "audio/webm", b64: btoa(roh), bytes: puffer.length };
|
||||
}, 2500);
|
||||
ok(webmAuf.bytes > 500,
|
||||
`eine echte WebM-Sprachnachricht aufgenommen (${webmAuf.bytes} Bytes)`);
|
||||
|
||||
const geschickt = await dogi.seite.evaluate(async ([r, b64]) => {
|
||||
const roh = atob(b64);
|
||||
const puffer = new Uint8Array(roh.length);
|
||||
for (let i = 0; i < roh.length; i++) puffer[i] = roh.charCodeAt(i);
|
||||
const a = await fetch(`/workspace/api/chat/raeume/${r}/anhang`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "audio/webm",
|
||||
"X-Dateiname": "Sprachnachricht.webm", "X-Dauer": "2500" },
|
||||
body: puffer,
|
||||
});
|
||||
const d = await a.json().catch(() => ({}));
|
||||
return { status: a.status, id: d?.nachricht?.id || 0 };
|
||||
}, [raumDogi, webmAuf.b64]);
|
||||
ok(geschickt.status === 201 && geschickt.id > 0,
|
||||
`sie geht durch (${geschickt.status}, #${geschickt.id})`);
|
||||
|
||||
/* AUF DIE UMWANDLUNG WARTEN. Sie laeuft absichtlich NEBEN dem
|
||||
Hochladen -- die Nachricht soll nicht auf ffmpeg warten.
|
||||
Deshalb wird hier nachgesehen, statt es anzunehmen, und zwar
|
||||
mit einer Frist: Ein festes `waitForTimeout` waere entweder
|
||||
zu kurz (rot ohne Grund) oder zu lang (jeder Lauf zahlt). */
|
||||
let weg2 = null;
|
||||
for (let i = 0; i < 40 && !weg2; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
weg2 = await dogi.seite.evaluate(async ([r, id]) => {
|
||||
const a = await fetch(`/workspace/api/chat/raeume/${r}/nachrichten`);
|
||||
const d = await a.json();
|
||||
const n = (d.nachrichten || []).find((x) => x.id === id);
|
||||
return n?.anhang?.weg_auch || null;
|
||||
}, [raumDogi, geschickt.id]);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (!weg2) await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
ok(!!weg2, `die zweite Fassung entsteht von selbst (${weg2 || "kam nicht"})`);
|
||||
ok(/\?form=mp4$/.test(weg2 || ""), ` und sie hat einen eigenen Weg (${weg2})`);
|
||||
|
||||
if (weg2) {
|
||||
/* ---- Was kommt ueber die Leitung? ------------------------- */
|
||||
const kopf = await dogi.seite.evaluate(async (w) => {
|
||||
const a = await fetch(w);
|
||||
const b = new Uint8Array(await a.arrayBuffer());
|
||||
return {
|
||||
code: a.status,
|
||||
typ: a.headers.get("content-type"),
|
||||
bereiche: a.headers.get("accept-ranges"),
|
||||
bytes: b.length,
|
||||
/* Die ersten zwoelf Bytes einer MP4: vier Byte Laenge,
|
||||
dann „ftyp". Daran erkennt man den Behaelter, ohne ihn
|
||||
zu glauben. */
|
||||
marke: String.fromCharCode(...b.slice(4, 8)),
|
||||
};
|
||||
}, weg2);
|
||||
ok(kopf.code === 200 && kopf.typ === "audio/mp4",
|
||||
`sie kommt als audio/mp4 heraus (${kopf.code}, ${kopf.typ})`);
|
||||
ok(kopf.marke === "ftyp",
|
||||
` und ist wirklich ein MP4-Behaelter (Marke „${kopf.marke}")`);
|
||||
ok(kopf.bereiche === "bytes",
|
||||
` ueber denselben Ausliefer-Weg wie alles andere (${kopf.bereiche})`);
|
||||
ok(kopf.bytes > 2000, ` mit Inhalt (${kopf.bytes} Bytes)`);
|
||||
|
||||
/* ---- DAS ORIGINAL BLEIBT ABRUFBAR -------------------------
|
||||
Ohne `?form=mp4` kommt weiter die WebM. Sie ist das, was
|
||||
hochgeladen wurde; sie zu ersetzen hiesse, die Aufnahme
|
||||
durch eine Umrechnung davon auszutauschen. */
|
||||
const original = await dogi.seite.evaluate(async (w) => {
|
||||
const a = await fetch(w.replace("?form=mp4", ""));
|
||||
return { code: a.status, typ: a.headers.get("content-type") };
|
||||
}, weg2);
|
||||
ok(original.code === 200 && original.typ === "audio/webm",
|
||||
`das Original bleibt daneben liegen (${original.typ})`);
|
||||
|
||||
/* ---- UND WAS STECKT WIRKLICH DRIN? ------------------------ */
|
||||
const db4 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const tonZeile = db4.prepare(
|
||||
"SELECT anhang_datei FROM chat_nachrichten WHERE id = ?").get(geschickt.id);
|
||||
db4.close();
|
||||
const { DATEN_ORDNER } = await import("./workspace.js");
|
||||
const quelle = join(DATEN_ORDNER, "chat-anhaenge", tonZeile.anhang_datei);
|
||||
const ziel = zweitfassungPfad(quelle);
|
||||
|
||||
const probe = ffprobePfad();
|
||||
if (!probe) {
|
||||
melde(" -- KONNTE NICHT NACHSEHEN: ffprobe fehlt neben ffmpeg.");
|
||||
nichtNachsehbar++;
|
||||
} else {
|
||||
const frag = (datei, was) => {
|
||||
try {
|
||||
return execFileSync(probe, ["-v", "error", "-show_entries", was,
|
||||
"-of", "default=noprint_wrappers=1:nokey=1", datei],
|
||||
{ encoding: "utf8" }).trim();
|
||||
} catch { return ""; }
|
||||
};
|
||||
const codec = frag(ziel, "stream=codec_name");
|
||||
const codecAlt = frag(quelle, "stream=codec_name");
|
||||
const dauerNeu = Number(frag(ziel, "format=duration"));
|
||||
const dauerAlt = Number(frag(quelle, "format=duration"));
|
||||
ok(codecAlt === "opus",
|
||||
`das Original ist Opus -- also genau der Fall von Miss (${codecAlt})`);
|
||||
ok(codec === "aac", `drinnen steckt AAC, nicht nur eine neue Endung (${codec})`);
|
||||
ok(dauerAlt > 0 && Math.abs(dauerNeu - dauerAlt) < 0.5,
|
||||
`und sie ist so lang wie das Original (${dauerAlt.toFixed(2)} s → `
|
||||
+ `${dauerNeu.toFixed(2)} s)`);
|
||||
/* GEGENPROBE ZUR LAENGE: Eine halbierte Datei MUSS
|
||||
auffallen. Ohne diese Zeile waere die Pruefung darueber
|
||||
auch mit einer abgeschnittenen Aufnahme gruen, solange
|
||||
ffprobe irgendeine Zahl liefert. */
|
||||
ok(Math.abs((dauerAlt / 2) - dauerAlt) >= 0.5,
|
||||
` Gegenprobe: eine halbierte Laenge waere aufgefallen `
|
||||
+ `(${(dauerAlt / 2).toFixed(2)} s gegen ${dauerAlt.toFixed(2)} s)`);
|
||||
}
|
||||
|
||||
/* ---- DIE INHALTSUEBERSICHT STEHT VORNE --------------------
|
||||
Ohne `+faststart` liegt `moov` am Dateiende. Das
|
||||
Abspielgeraet muss dann erst bis ans Ende lesen, bevor es
|
||||
anfangen kann -- genau das „laedt die ganze Zeit", das Miss
|
||||
in Runde 5 beschrieben hat. Gemessen wird die Reihenfolge
|
||||
der Kaesten in der Datei, nicht die Zeile im Aufruf. */
|
||||
const roh = lies(ziel);
|
||||
const wo = (name) => roh.indexOf(Buffer.from(name, "latin1"));
|
||||
const moov = wo("moov");
|
||||
const mdat = wo("mdat");
|
||||
ok(moov > 0 && mdat > 0 && moov < mdat,
|
||||
`die Inhaltsuebersicht steht VOR den Tondaten (moov ${moov}, mdat ${mdat})`);
|
||||
ok(wiegt(ziel).size === kopf.bytes,
|
||||
` und was auf der Platte liegt, kommt auch heraus (${wiegt(ziel).size} Bytes)`);
|
||||
|
||||
/* ---- UND BEIM LOESCHEN GEHEN BEIDE ------------------------
|
||||
Sonst bliebe bei jeder zurueckgenommenen Sprachnachricht
|
||||
eine m4a liegen -- ohne Zeile, die auf sie zeigt, und damit
|
||||
fuer immer unzuordenbar. Genau diese Luecke gab es gestern
|
||||
bei den Supportbildern. */
|
||||
const weggenommen = await dogi.seite.evaluate(async (id) => {
|
||||
const a = await fetch(`/workspace/api/chat/nachrichten/${id}`, { method: "DELETE" });
|
||||
return a.status;
|
||||
}, geschickt.id);
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
const nochDa = [quelle, ziel].filter((f) => { try { wiegt(f); return true; } catch { return false; } });
|
||||
ok(weggenommen < 400 && nochDa.length === 0,
|
||||
`beim Loeschen gehen BEIDE Dateien (${weggenommen}, ${nochDa.length} liegen noch)`);
|
||||
}
|
||||
|
||||
/* ---- GEGENPROBE: EIN BILD BEKOMMT KEINE ZWEITE FASSUNG -----
|
||||
Sonst liefe ffmpeg ueber jedes Foto und jedes PDF -- Arbeit
|
||||
fuer nichts, und eine Tuer, nach der niemand gefragt hat.
|
||||
Das Bild wird hier selbst geschickt und nicht im Verlauf
|
||||
gesucht: Mein erster Anlauf suchte eines, fand keines (an
|
||||
dieser Stelle gab es noch keins) und meldete `undefined`
|
||||
statt `null` -- gruen waere das nie gewesen, aber es hat
|
||||
auch nichts geprueft. */
|
||||
const einBild = await dogi.seite.evaluate(async (r) => {
|
||||
const png = Uint8Array.from(atob(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="),
|
||||
(c) => c.charCodeAt(0));
|
||||
const a = await fetch(`/workspace/api/chat/raeume/${r}/anhang`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "image/png", "X-Dateiname": "punkt.png" },
|
||||
body: png,
|
||||
});
|
||||
const d = await a.json().catch(() => ({}));
|
||||
return { status: a.status, auch: d?.nachricht?.anhang?.weg_auch };
|
||||
}, raumDogi);
|
||||
ok(einBild.status === 201 && einBild.auch === null,
|
||||
`Gegenprobe: ein Bild bekommt keine zweite Fassung `
|
||||
+ `(${einBild.status}, ${JSON.stringify(einBild.auch)})`);
|
||||
|
||||
/* ==== DIESER BLOCK HINTERLAESST NICHTS (03.10.2026) =========
|
||||
|
||||
Die Zeilen weiter unten fragen die Gespraechsliste und
|
||||
erwarten dort das Wort „Sprachnachricht" -- also das, was
|
||||
ZULETZT in diesem Raum stand. Mein Probebild liess sie rot
|
||||
werden, und zwar zu Recht: Nicht die Liste war kaputt,
|
||||
sondern diese Pruefung hatte den Raum veraendert.
|
||||
|
||||
Eine Pruefung, die den Stand fuer die naechste verschiebt,
|
||||
ist schlimmer als keine: Der Fehler taucht woanders auf, und
|
||||
dort sucht man ihn dann. Also wird aufgeraeumt. */
|
||||
await dogi.seite.evaluate(async (r) => {
|
||||
const a = await fetch(`/workspace/api/chat/raeume/${r}/nachrichten`);
|
||||
const d = await a.json();
|
||||
const bild = (d.nachrichten || []).filter((n) => n.anhang?.art === "bild").pop();
|
||||
if (bild) {
|
||||
await fetch(`/workspace/api/chat/nachrichten/${bild.id}`, { method: "DELETE" });
|
||||
}
|
||||
}, raumDogi);
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- In der Gesprächsliste steht ein Wort dafür ------------------ */
|
||||
const vorschau = await dogi.seite.evaluate(async () => {
|
||||
const a = await fetch("/workspace/api/chat/raeume");
|
||||
@@ -1453,6 +1711,7 @@ await dogi.ctx.close();
|
||||
await browser.close();
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
|
||||
melde(`\n${geprueft} Prüfungen`);
|
||||
melde(`\n${geprueft} Prüfungen`
|
||||
+ (nichtNachsehbar ? `, ${nichtNachsehbar} konnten nicht nachsehen` : ""));
|
||||
melde(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
||||
process.exit(fehler ? 1 : 0);
|
||||
|
||||
+207
-11
@@ -43,6 +43,126 @@ import { join, extname, basename } from "node:path";
|
||||
import { mkdirSync, writeFileSync, statSync, unlinkSync,
|
||||
copyFileSync } from "node:fs";
|
||||
import { liefereDatei } from "./helfer-ausliefern.mjs";
|
||||
/* Die zweite Fassung jeder Sprachnachricht -- siehe
|
||||
helfer-ffmpeg.mjs. Sie ist der Grund, warum Miss sie ab heute
|
||||
hoeren kann (Support-Meldung #12). */
|
||||
import {
|
||||
zweitfassungBauen, zweitfassungPfad, brauchtZweitfassung, ZWEITFASSUNG_TYP,
|
||||
} from "./helfer-ffmpeg.mjs";
|
||||
|
||||
/** Eine Anhangsdatei entfernen -- MITSAMT ihrer zweiten Fassung.
|
||||
*
|
||||
* ==== SONST BLEIBT DIE HAELFTE LIEGEN (03.10.2026) ===============
|
||||
*
|
||||
* Zwei Stellen loeschen Anhaenge: das Wegraeumen eines ganzen
|
||||
* Gespraechs und das Zuruecknehmen einer einzelnen Nachricht. Beide
|
||||
* haben bis heute genau eine Datei entfernt. Mit der Zweitfassung
|
||||
* waere ab sofort bei jeder geloeschten Sprachnachricht eine
|
||||
* m4a liegengeblieben -- ohne Zeile, die auf sie zeigt, und damit
|
||||
* fuer immer unzuordenbar.
|
||||
*
|
||||
* Genau diese Luecke habe ich gestern bei den Supportbildern
|
||||
* gefunden, und sie war dort schon einen Tag alt. Hier steht sie
|
||||
* deshalb von Anfang an an EINER Stelle: Zwei Aufrufer koennen
|
||||
* nicht auseinanderlaufen.
|
||||
*
|
||||
* Gibt `true` zurueck, wenn danach nichts mehr daliegt -- auch wenn
|
||||
* schon vorher nichts da war (ENOENT ist kein Fehlschlag). */
|
||||
function anhangWeg(datei) {
|
||||
let gut = true;
|
||||
for (const pfad of [join(ANHANG_ORDNER, datei), zweitfassungPfad(join(ANHANG_ORDNER, datei))]) {
|
||||
try { unlinkSync(pfad); } catch (f) {
|
||||
if (f?.code !== "ENOENT") {
|
||||
console.error("[chat] Anhang bleibt liegen:", f?.message);
|
||||
gut = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return gut;
|
||||
}
|
||||
|
||||
/** Liegt neben dieser Tondatei schon die universelle Fassung?
|
||||
*
|
||||
* GEFRAGT WIRD DIE PLATTE, nicht eine Spalte in der Datenbank. Eine
|
||||
* Spalte muesste gepflegt werden -- und sie waere genau dann falsch,
|
||||
* wenn der Nachruestlauf gerade eine Datei erzeugt hat oder jemand
|
||||
* eine von Hand entfernt. Was wirklich da ist, weiss nur der Ordner.
|
||||
*
|
||||
* `statSync` je Nachricht klingt nach viel; gemessen ist es ein
|
||||
* Verzeichniszugriff aus dem Betriebssystem-Zwischenspeicher, und er
|
||||
* faellt nur bei Toenen an -- davon gibt es im ganzen Haus sechs. */
|
||||
function zweitfassungDa(art, datei) {
|
||||
if (art !== "ton" || !datei) return false;
|
||||
if (!brauchtZweitfassung(datei)) return false;
|
||||
try {
|
||||
statSync(zweitfassungPfad(join(ANHANG_ORDNER, datei)));
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
/** Was noch keine Zweitfassung hat, bekommt eine.
|
||||
*
|
||||
* ==== WARUM EIN NACHRUESTLAUF UND NICHT NUR BEIM HOCHLADEN =======
|
||||
*
|
||||
* Beim Hochladen zu wandeln hilft ab heute. Miss' Problem sind aber
|
||||
* die SECHS, die seit September liegen -- und genau die waeren mit
|
||||
* einer Loesung „ab jetzt" weiter stumm geblieben. Das ist dieselbe
|
||||
* Falle wie bei den Supportbildern gestern: Ein Umbau, der nur
|
||||
* vorwaerts gilt, laesst den vorhandenen Bestand still zurueck.
|
||||
*
|
||||
* GEDECKELT. Hoechstens `hoechstens` Dateien je Durchgang -- bei
|
||||
* einem Ordner mit tausend Toenen soll der Rundgang nicht eine
|
||||
* Viertelstunde rechnen. Was uebrig bleibt, kommt beim naechsten
|
||||
* Mal dran; der Lauf merkt sich nichts, er sieht einfach nach.
|
||||
*
|
||||
* Gibt zurueck, wie viele neu entstanden sind. */
|
||||
export async function toeneNachruesten(hoechstens = 20) {
|
||||
try {
|
||||
const zeilen = db().prepare(`SELECT anhang_datei FROM chat_nachrichten
|
||||
WHERE anhang_art = 'ton' AND anhang_datei IS NOT NULL AND weg_am IS NULL`).all();
|
||||
let gebaut = 0;
|
||||
for (const z of zeilen) {
|
||||
if (gebaut >= hoechstens) break;
|
||||
if (!brauchtZweitfassung(z.anhang_datei)) continue;
|
||||
if (zweitfassungDa("ton", z.anhang_datei)) continue;
|
||||
const pfad = join(ANHANG_ORDNER, z.anhang_datei);
|
||||
try { statSync(pfad); } catch { continue; }
|
||||
/* NACHEINANDER, nicht alle auf einmal: Zwanzig ffmpeg-Laeufe
|
||||
gleichzeitig nehmen dem Server genau die Rechenzeit weg, die
|
||||
er fuer die Seite braucht. */
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (await zweitfassungBauen(pfad)) gebaut++;
|
||||
}
|
||||
if (gebaut) console.log(`[ton] ${gebaut} Sprachnachricht(en) zweitfassung ergaenzt.`);
|
||||
return gebaut;
|
||||
} catch (fehler) {
|
||||
console.error("[ton] Nachruesten:", fehler?.message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Einmal kurz nach dem Start, danach alle sechs Stunden.
|
||||
*
|
||||
* NICHT IM STARTMOMENT -- erst soll die Seite antworten. Dieselbe
|
||||
* Ueberlegung wie beim Aufbewahrungslauf: Ein ffmpeg-Durchgang im
|
||||
* Startmoment macht aus einem Fehler dort einen Startfehler.
|
||||
*
|
||||
* SECHS STUNDEN UND NICHT EINE MINUTE: Was beim Hochladen entsteht,
|
||||
* wird dort schon gewandelt. Dieser Lauf ist das Netz darunter --
|
||||
* fuer die Dateien von frueher und fuer den Fall, dass eine
|
||||
* Umwandlung einmal nicht geklappt hat. Haeufiger waere Rechenzeit
|
||||
* fuer eine Liste, die fast immer leer ist.
|
||||
*
|
||||
* `unref` DAMIT ER NICHTS AM LEBEN HAELT: Ein Taktgeber ohne das
|
||||
* haelt den Prozess offen, und eine Pruefung, die den Server
|
||||
* einbindet, endet dann nie. Genau daran hing im Haus schon einmal
|
||||
* ein dreistuendiger Stillstand. */
|
||||
export function toeneNachruestenStarten() {
|
||||
const ersterLauf = setTimeout(() => { toeneNachruesten().catch(() => {}); }, 120_000);
|
||||
const takt = setInterval(() => { toeneNachruesten().catch(() => {}); }, 6 * 3600_000);
|
||||
ersterLauf.unref?.();
|
||||
takt.unref?.();
|
||||
}
|
||||
import {
|
||||
db, protokolliere, echteIp, sitzungLesen, istDogFather,
|
||||
schreibbareIds, darfSchreibenMit, ROLLEN_SORTIERUNG, DATEN_ORDNER,
|
||||
@@ -79,6 +199,7 @@ const NAME_MAX = 80;
|
||||
ein PDF mit Bildern selten über 10. */
|
||||
const ANHANG_MAX = 12 * 1024 * 1024;
|
||||
const ANHANG_ORDNER = join(DATEN_ORDNER, "chat-anhaenge");
|
||||
|
||||
const ANHANG_NAME_MAX = 200;
|
||||
|
||||
/* EIGENE GRENZE FUER TON (23.09.2026).
|
||||
@@ -1662,6 +1783,18 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
|
||||
n.antwort_auf, n.angeheftet_am, n.rudel,
|
||||
n.anhang_name, n.anhang_art, n.anhang_typ, n.anhang_groesse,
|
||||
n.anhang_breite, n.anhang_hoehe, n.anhang_dauer,
|
||||
/* DER DATEINAME WIRD GEBRAUCHT, ABER NICHT AUSGELIEFERT
|
||||
(03.10.2026). zweitfassungDa() fragt mit ihm die
|
||||
Platte; nach draussen geht er nicht -- was dort liegt,
|
||||
geht niemanden etwas an, der nur zuhoeren will.
|
||||
Dass er hier fehlte, war der Grund, warum die zweite
|
||||
Fassung nie angeboten wurde: Die Funktion bekam nichts
|
||||
und sagte brav „gibt es nicht". Gefunden hat das die
|
||||
Pruefung, nicht das Lesen.
|
||||
(Und KEINE Gegen-Apostrophe in diesem Text: Er steht
|
||||
in einem Template-Literal, und einer davon beendet
|
||||
es. Heute schon das zweite Mal.) */
|
||||
n.anhang_datei,
|
||||
a.text AS zitat_text, a.weg_am AS zitat_weg, ap.name AS zitat_von,
|
||||
a.anhang_art AS zitat_anhang
|
||||
FROM chat_nachrichten n
|
||||
@@ -1912,6 +2045,14 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
|
||||
breite: n.anhang_breite || 0, hoehe: n.anhang_hoehe || 0,
|
||||
dauer: n.anhang_dauer || 0,
|
||||
weg: `/workspace/api/chat/anhang/${n.id}`,
|
||||
/* WO DIE ZWEITE FASSUNG LIEGT -- oder `null`, wenn es
|
||||
keine gibt (Bild, PDF, oder ffmpeg hat noch nicht).
|
||||
GEFRAGT WIRD DIE PLATTE, nicht eine Spalte: Eine Spalte
|
||||
muesste gepflegt werden, und sie waere genau dann
|
||||
falsch, wenn jemand eine Datei von Hand nachlegt oder
|
||||
der Nachruestlauf gerade durchgelaufen ist. */
|
||||
weg_auch: zweitfassungDa(n.anhang_art, n.anhang_datei)
|
||||
? `/workspace/api/chat/anhang/${n.id}?form=mp4` : null,
|
||||
} : null,
|
||||
}));
|
||||
})(),
|
||||
@@ -2244,6 +2385,29 @@ chatRouter.post("/workspace/api/chat/raeume/:id/anhang",
|
||||
const aufPlatte = `${Date.now().toString(36)}-${randomBytes(8).toString("hex")}${erkannt.endung}`;
|
||||
writeFileSync(join(ANHANG_ORDNER, aufPlatte), req.body, { flag: "wx" });
|
||||
|
||||
/* ==== DIE ZWEITE FASSUNG (03.10.2026) =======================
|
||||
|
||||
Miss im Support, Meldung #12, fuenf Runden: „Bei mir laesst
|
||||
sich die Nachricht nicht abspielen." Bei allen anderen ging
|
||||
es; alle sechs vorhandenen Sprachnachrichten waren WebM.
|
||||
|
||||
NEBENHER UND NICHT DAVOR. `await` hier hiesse: Wer eine
|
||||
Sprachnachricht schickt, wartet auf ffmpeg, bevor sie im
|
||||
Chat steht -- und wenn ffmpeg haengt, haengt die Nachricht.
|
||||
Sie geht sofort raus; die zweite Fassung kommt Sekunden
|
||||
spaeter dazu, und bis dahin spielt das Original. Genau
|
||||
deshalb sagt die Antwort unten auch nichts ueber sie: Was
|
||||
es noch nicht gibt, wird nicht versprochen.
|
||||
|
||||
NUR FUER TON. Ein Bild braucht keine zweite Fassung, und
|
||||
ffmpeg auf ein PDF loszulassen waere eine Tuer, nach der
|
||||
niemand gefragt hat. */
|
||||
if (erkannt.art === "ton" && brauchtZweitfassung(aufPlatte)) {
|
||||
zweitfassungBauen(join(ANHANG_ORDNER, aufPlatte))
|
||||
.catch(() => { /* der Helfer meldet es selbst; die Nachricht
|
||||
steht ohnehin schon */ });
|
||||
}
|
||||
|
||||
const d = db();
|
||||
const n = jetzt();
|
||||
d.prepare(`INSERT INTO chat_nachrichten
|
||||
@@ -2269,6 +2433,12 @@ chatRouter.post("/workspace/api/chat/raeume/:id/anhang",
|
||||
breite: erkannt.breite || 0, hoehe: erkannt.hoehe || 0,
|
||||
dauer: dauer || 0,
|
||||
weg: `/workspace/api/chat/anhang/${id}`,
|
||||
/* Gleich nach dem Hochladen gibt es sie noch nicht -- die
|
||||
Umwandlung laeuft nebenher. Dass hier `null` steht, ist
|
||||
deshalb richtig und keine Luecke: Beim naechsten Laden
|
||||
der Nachrichten ist sie da. */
|
||||
weg_auch: zweitfassungDa(erkannt.art, aufPlatte)
|
||||
? `/workspace/api/chat/anhang/${id}?form=mp4` : null,
|
||||
},
|
||||
rudel: false,
|
||||
};
|
||||
@@ -2508,6 +2678,17 @@ chatRouter.post("/workspace/api/chat/raeume/:id/gif", gleicheHerkunft, nurRudel,
|
||||
groesse: g.groesse, breite: g.breite || 0, hoehe: g.hoehe || 0,
|
||||
dauer: 0,
|
||||
weg: `/workspace/api/chat/anhang/${id}`,
|
||||
/* EIN GIF HAT KEINE ZWEITE FASSUNG, und zwar nie -- das ist
|
||||
hier ein Bild, kein Ton. Die Zeile steht trotzdem da:
|
||||
Faehlte sie, haette dieselbe Nachricht je nach Weg einmal
|
||||
das Feld und einmal nicht, und der Browser muesste beide
|
||||
Faelle kennen. Ein ausdrueckliches `null` ist eine
|
||||
Auskunft, ein fehlendes Feld ist eine Frage.
|
||||
(Mein erster Anlauf rief hier `zweitfassungDa(erkannt...)`
|
||||
auf -- `erkannt` gibt es in dieser Route gar nicht. Das
|
||||
waere ein Absturz beim ersten GIF gewesen, und zwar erst
|
||||
im Betrieb.) */
|
||||
weg_auch: null,
|
||||
},
|
||||
};
|
||||
const empfaenger = teilnehmerVon(raumId).filter((t) => t.id !== req.person.id);
|
||||
@@ -2545,7 +2726,29 @@ chatRouter.get("/workspace/api/chat/anhang/:id", (req, res) => {
|
||||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
}
|
||||
|
||||
const pfad = join(ANHANG_ORDNER, n.anhang_datei);
|
||||
/* ==== DIE ZWEITE FASSUNG, WENN SIE VERLANGT WIRD (03.10.2026)
|
||||
|
||||
`?form=mp4` liefert die AAC-Fassung derselben Sprachnachricht.
|
||||
Miss im Support, #12: Auf ihrem iPhone laesst sich WebM nicht
|
||||
abspielen; dieselbe Nachricht in MP4 schon.
|
||||
|
||||
EINE ROUTE, KEINE ZWEITE. Es ist dieselbe Nachricht mit
|
||||
denselben Rechten -- wer sie sehen darf, darf sie in beiden
|
||||
Fassungen sehen. Eine eigene Route haette dieselbe
|
||||
Sichtbarkeitspruefung ein zweites Mal gebraucht, und es ist
|
||||
immer die zweite, die beim naechsten Umbau vergessen wird.
|
||||
|
||||
STILL ZURUECK AUF DAS ORIGINAL, wenn es die Fassung nicht
|
||||
gibt: Wer sie anfragt, hat sie in der Nachrichtenliste
|
||||
angeboten bekommen -- ist sie seitdem verschwunden, soll er
|
||||
etwas hoeren und keinen Fehler lesen. */
|
||||
let pfad = join(ANHANG_ORDNER, n.anhang_datei);
|
||||
let typ = n.anhang_typ || "application/octet-stream";
|
||||
if (req.query.form === "mp4" && n.anhang_art === "ton"
|
||||
&& brauchtZweitfassung(n.anhang_datei)) {
|
||||
const zweit = zweitfassungPfad(pfad);
|
||||
try { statSync(zweit); pfad = zweit; typ = ZWEITFASSUNG_TYP; } catch { /* Original */ }
|
||||
}
|
||||
try { statSync(pfad); } catch { return res.status(410).json({ fehler: "Datei fehlt auf der Platte." }); }
|
||||
|
||||
/* AUSGELIEFERT WIRD DER TYP AUS DER ERKENNUNG, nie ein
|
||||
@@ -2557,7 +2760,7 @@ chatRouter.get("/workspace/api/chat/anhang/:id", (req, res) => {
|
||||
Dokument jede Ausführung -- das ist die Sicherung für den Fall,
|
||||
dass in einem PDF ein Skript steckt (PDF kann das). Bei einem
|
||||
Bild kostet sie nichts. */
|
||||
res.setHeader("Content-Type", n.anhang_typ || "application/octet-stream");
|
||||
res.setHeader("Content-Type", typ);
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
res.setHeader("Content-Security-Policy", "default-src 'none'; sandbox");
|
||||
res.setHeader("Cache-Control", "private, max-age=86400");
|
||||
@@ -2940,11 +3143,7 @@ chatRouter.delete("/workspace/api/chat/raeume/:id/ganz", gleicheHerkunft, (req,
|
||||
aber kein Datenschaden, und es wird gemeldet statt verschwiegen. */
|
||||
let dateienWeg = 0;
|
||||
for (const name of dateien) {
|
||||
try { unlinkSync(join(ANHANG_ORDNER, name)); dateienWeg++; }
|
||||
catch (f) {
|
||||
if (f?.code === "ENOENT") dateienWeg++;
|
||||
else console.error("[chat] Anhang bleibt liegen:", f?.message);
|
||||
}
|
||||
if (anhangWeg(name)) dateienWeg++;
|
||||
}
|
||||
|
||||
protokolliere("chat_aufgeloest", {
|
||||
@@ -3395,10 +3594,7 @@ chatRouter.delete("/workspace/api/chat/nachrichten/:id", gleicheHerkunft, (req,
|
||||
schon weg), ist der Verweis trotzdem fort -- andersherum bliebe
|
||||
im schlechten Fall eine Zeile ohne Datei stehen. */
|
||||
d.prepare("DELETE FROM chat_nachrichten WHERE id = ?").run(id);
|
||||
if (dateiWeg) {
|
||||
try { unlinkSync(join(ANHANG_ORDNER, dateiWeg)); }
|
||||
catch (f) { if (f?.code !== "ENOENT") console.error("[chat] Anhang bleibt liegen:", f?.message); }
|
||||
}
|
||||
if (dateiWeg) anhangWeg(dateiWeg);
|
||||
/* DER RAUM RECHNET SEINEN LETZTEN ZEITPUNKT NEU. Ohne das stuende
|
||||
er in der Liste weiterhin ganz oben, mit einem Zeitpunkt, zu dem
|
||||
es nichts mehr gibt. */
|
||||
|
||||
Reference in New Issue
Block a user