Video einlesen: die Frist galt nur fuer den Anfang -- und kein Fehlschlag stand im Protokoll
Filipe: "ES GING DOCH IMMER BIS JETZT." Das war der nuetzlichste Satz
des Tages, denn er stimmt: Zwischen seinem letzten Erfolg (04.10.
13:44) und der Meldung wurde NICHTS ausgeliefert -- gemessen am
Server: ein einziger Neustart seit dem 04.10. 13:00, meiner von
heute 12:19. Am Haus hat sich nichts geaendert.
Ein Fehler, der ohne Aenderung anfaengt, haengt an etwas von
draussen. Und genau dafuer war die Frist zu kurz gebaut:
holeMitFrist() gab die Antwort zurueck, sobald die KOPFZEILEN da
waren, und raeumte im finally die Uhr weg. Was danach kam --
a.json() fuer die Videodaten, a.arrayBuffer() fuer das
Vorschaubild -- lief OHNE jede Frist.
Bleibt TikToks Bildserver mitten im Koerper stehen, wartet unsere
Anfrage fuer immer. Von aussen sieht das genau so aus, wie Filipe es
beschrieben hat: "der Knopf bleibt auf `wird geholt ...` stehen" --
keine Meldung, kein Eintrag, kein Protokolleintrag, weil der Vorgang
nie endet. Das Vorschaubild ist dabei kein Nebending: das letzte war
1,29 MB.
Der Koerper wird jetzt gelesen, SOLANGE die Uhr laeuft. Acht
Sekunden gelten fuer alles zusammen.
NACHGEMESSEN, nicht vermutet: oembed antwortet dem Server in 0,2 s,
das Bild laedt in 0,1 s -- heute haengt es also nicht. Der Fehler
ist trotzdem echt und erklaert genau dieses Bild; er kommt und geht
mit dem fremden Netz.
UND DER ZWEITE TEIL, der die Suche so teuer gemacht hat: Im
Protokoll stand nur der ERFOLG (video_eingelesen). Blieb TikTok eine
Antwort schuldig, gab es hinterher nichts zum Nachsehen -- auf die
Frage "ist der Versuch ueberhaupt angekommen?" liess sich nichts
sagen. Dieselbe Reihenfolge wie am 06.09. im Shop ("erst fragen, OB
gesendet wurde") und wie heute frueh beim Push.
Jetzt steht jeder Fehlschlag als `video_gescheitert` im Protokoll,
mit Grund: keine TikTok-Adresse, TikTok antwortet nicht, oder
Abbruch mit Meldung.
Gegengemessen: mess-teilen weiterhin 201 mit Eintrag und
Vorschaubild, pruef-video 74, pruef-teilen 27, pruef-kanalzeile 14 --
alle 0 Fehler.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -94,11 +94,36 @@ export function istTikTok(roh) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Mit Frist holen. Ohne sie haengt die Anfrage am fremden Dienst. */
|
/** Mit Frist holen. Ohne sie haengt die Anfrage am fremden Dienst. */
|
||||||
|
/** Holt etwas von draussen -- MIT Frist ueber den ganzen Vorgang.
|
||||||
|
*
|
||||||
|
* DIE ALTE FASSUNG HAT NUR DEN ANFANG BEWACHT (behoben 05.10.2026).
|
||||||
|
*
|
||||||
|
* Sie gab die Antwort zurueck, sobald die Kopfzeilen da waren, und
|
||||||
|
* raeumte im `finally` die Uhr weg. Was danach kam -- `a.json()` beim
|
||||||
|
* Abruf der Videodaten, `a.arrayBuffer()` beim Vorschaubild -- lief
|
||||||
|
* OHNE jede Frist. Bleibt der fremde Server mitten im Koerper
|
||||||
|
* stehen, wartet unsere Anfrage fuer immer.
|
||||||
|
*
|
||||||
|
* Genau das sieht man als "der Knopf bleibt auf `wird geholt ...`
|
||||||
|
* stehen": keine Fehlermeldung, kein Eintrag, kein Protokolleintrag
|
||||||
|
* -- weil der Vorgang nie endet. Und es braucht keine Aenderung bei
|
||||||
|
* uns, um anzufangen: Es haengt am fremden Netz. "Es ging doch immer
|
||||||
|
* bis jetzt" ist bei so einem Fehler die Regel, nicht die Ausnahme.
|
||||||
|
*
|
||||||
|
* Der Koerper wird deshalb GELESEN, solange die Uhr laeuft, und erst
|
||||||
|
* danach abgeraeumt. 8 Sekunden gelten damit fuer alles zusammen. */
|
||||||
async function holeMitFrist(adresse, kopf = {}) {
|
async function holeMitFrist(adresse, kopf = {}) {
|
||||||
const abbruch = new AbortController();
|
const abbruch = new AbortController();
|
||||||
const uhr = setTimeout(() => abbruch.abort(), FRIST_MS);
|
const uhr = setTimeout(() => abbruch.abort(), FRIST_MS);
|
||||||
try {
|
try {
|
||||||
return await fetch(adresse, { signal: abbruch.signal, headers: kopf, redirect: "follow" });
|
const a = await fetch(adresse, { signal: abbruch.signal, headers: kopf, redirect: "follow" });
|
||||||
|
/* Bei einem Fehlschlag interessiert der Koerper nicht -- aber er
|
||||||
|
muss trotzdem weg, sonst bleibt die Verbindung offen. */
|
||||||
|
const roh = Buffer.from(await a.arrayBuffer());
|
||||||
|
return {
|
||||||
|
ok: a.ok, status: a.status, headers: a.headers, roh,
|
||||||
|
text: () => roh.toString("utf8"),
|
||||||
|
};
|
||||||
} finally { clearTimeout(uhr); }
|
} finally { clearTimeout(uhr); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +145,8 @@ export async function videoAuskunft(url) {
|
|||||||
`${OEMBED_BASIS}?url=${encodeURIComponent(url)}`,
|
`${OEMBED_BASIS}?url=${encodeURIComponent(url)}`,
|
||||||
{ accept: "application/json" });
|
{ accept: "application/json" });
|
||||||
if (!a.ok) return { fehler: `TikTok antwortet nicht (${a.status}).` };
|
if (!a.ok) return { fehler: `TikTok antwortet nicht (${a.status}).` };
|
||||||
const j = await a.json().catch(() => null);
|
let j = null;
|
||||||
|
try { j = JSON.parse(a.text()); } catch { j = null; }
|
||||||
if (!j || !j.title) return { fehler: "TikTok hat nichts zu diesem Video geschickt." };
|
if (!j || !j.title) return { fehler: "TikTok hat nichts zu diesem Video geschickt." };
|
||||||
/* Der Account steht in `author_url` (…/@name). `author_name` ist der
|
/* Der Account steht in `author_url` (…/@name). `author_name` ist der
|
||||||
ANZEIGENAME und aendert sich, wenn jemand ihn umstellt -- als
|
ANZEIGENAME und aendert sich, wenn jemand ihn umstellt -- als
|
||||||
@@ -141,9 +167,7 @@ async function coverAblegen(adresse) {
|
|||||||
let a;
|
let a;
|
||||||
try { a = await holeMitFrist(adresse); } catch { return null; }
|
try { a = await holeMitFrist(adresse); } catch { return null; }
|
||||||
if (!a.ok) return null;
|
if (!a.ok) return null;
|
||||||
const lang = Number(a.headers.get("content-length") || 0);
|
const roh = a.roh;
|
||||||
if (lang > COVER_MAX) return null;
|
|
||||||
const roh = Buffer.from(await a.arrayBuffer());
|
|
||||||
if (!roh.length || roh.length > COVER_MAX) return null;
|
if (!roh.length || roh.length > COVER_MAX) return null;
|
||||||
/* DIESELBE BYTEPRUEFUNG WIE BEI HOCHGELADENEN DATEIEN. Was von
|
/* DIESELBE BYTEPRUEFUNG WIE BEI HOCHGELADENEN DATEIEN. Was von
|
||||||
draussen kommt, ist von draussen -- egal ob jemand es hochlaedt
|
draussen kommt, ist von draussen -- egal ob jemand es hochlaedt
|
||||||
@@ -177,6 +201,10 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => {
|
|||||||
|
|
||||||
const url = String(req.body?.url ?? "").trim();
|
const url = String(req.body?.url ?? "").trim();
|
||||||
if (!istTikTok(url)) {
|
if (!istTikTok(url)) {
|
||||||
|
protokolliere("video_gescheitert", {
|
||||||
|
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||||
|
detail: `${bereich} ${String(url).slice(0, 120)} -- keine TikTok-Adresse`,
|
||||||
|
});
|
||||||
return res.status(400).json({ fehler: "Das ist keine TikTok-Adresse." });
|
return res.status(400).json({ fehler: "Das ist keine TikTok-Adresse." });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +262,20 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auskunft = await videoAuskunft(url);
|
const auskunft = await videoAuskunft(url);
|
||||||
if (auskunft.fehler) return res.status(502).json({ fehler: auskunft.fehler });
|
if (auskunft.fehler) {
|
||||||
|
/* NICHT STILL SCHEITERN. Bis heute stand im Protokoll nur der
|
||||||
|
Erfolg (`video_eingelesen`). Blieb TikTok eine Antwort
|
||||||
|
schuldig, gab es hinterher nichts zum Nachsehen -- und auf
|
||||||
|
die Frage "warum kam mein Video nicht an?" liess sich nicht
|
||||||
|
einmal sagen, OB der Versuch den Server erreicht hat. Genau
|
||||||
|
diese Reihenfolge hat am 06.09. im Shop zwei Stunden
|
||||||
|
gekostet, und heute noch einmal einen halben Tag. */
|
||||||
|
protokolliere("video_gescheitert", {
|
||||||
|
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||||
|
detail: `${bereich} ${url} -- ${auskunft.fehler}`.slice(0, 180),
|
||||||
|
});
|
||||||
|
return res.status(502).json({ fehler: auskunft.fehler });
|
||||||
|
}
|
||||||
|
|
||||||
/* NUR SEINE EIGENEN KANAELE. Ein fremdes Video in Filipes
|
/* NUR SEINE EIGENEN KANAELE. Ein fremdes Video in Filipes
|
||||||
Highlights waere nicht bloss falsch einsortiert -- es waere ein
|
Highlights waere nicht bloss falsch einsortiert -- es waere ein
|
||||||
@@ -344,6 +385,14 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => {
|
|||||||
Datei ohne Eintrag findet niemand wieder und niemand loescht sie
|
Datei ohne Eintrag findet niemand wieder und niemand loescht sie
|
||||||
-- sie liegt einfach fuer immer da. */
|
-- sie liegt einfach fuer immer da. */
|
||||||
if (abgelegt) { try { unlinkSync(join(ORDNER, abgelegt.name)); } catch { /* egal */ } }
|
if (abgelegt) { try { unlinkSync(join(ORDNER, abgelegt.name)); } catch { /* egal */ } }
|
||||||
|
/* Auch der Absturz gehoert ins Protokoll -- er ist der Fall, bei
|
||||||
|
dem sonst GAR nichts dasteht. */
|
||||||
|
try {
|
||||||
|
protokolliere("video_gescheitert", {
|
||||||
|
personId: req.person?.id, rolle: req.person?.rolle, ip: echteIp(req),
|
||||||
|
detail: `Abbruch: ${String(fehler?.message || fehler)}`.slice(0, 180),
|
||||||
|
});
|
||||||
|
} catch { /* das Protokoll darf die Antwort nicht aufhalten */ }
|
||||||
console.error("[workspace] Video einlesen:", fehler?.message);
|
console.error("[workspace] Video einlesen:", fehler?.message);
|
||||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||||
}
|
}
|
||||||
@@ -429,7 +478,8 @@ export async function videoNochDa(url) {
|
|||||||
return { da: false, grund: `TikTok kennt es nicht (${a.status})` };
|
return { da: false, grund: `TikTok kennt es nicht (${a.status})` };
|
||||||
}
|
}
|
||||||
if (!a.ok) return { da: null, grund: `unerwartet (${a.status})` };
|
if (!a.ok) return { da: null, grund: `unerwartet (${a.status})` };
|
||||||
const j = await a.json().catch(() => null);
|
let j = null;
|
||||||
|
try { j = JSON.parse(a.text()); } catch { j = null; }
|
||||||
/* Antwort ohne Titel: TikTok liefert das auch fuer entfernte Videos
|
/* Antwort ohne Titel: TikTok liefert das auch fuer entfernte Videos
|
||||||
mit Status 200. Ein leerer Titel ist deshalb eine Aussage. */
|
mit Status 200. Ein leerer Titel ist deshalb eine Aussage. */
|
||||||
if (!j || !j.title) return { da: false, grund: "Antwort ohne Titel" };
|
if (!j || !j.title) return { da: false, grund: "Antwort ohne Titel" };
|
||||||
|
|||||||
Reference in New Issue
Block a user