Ein Video kann bei TikTok geloescht oder auf privat gestellt werden.
Bei uns blieb der Eintrag stehen -- mit Cover, Text und einem Knopf
auf eine Fehlerseite. Das faellt beim Bauen nicht auf und beim Testen
nicht; es faellt dem auf, der klickt, und das ist die Community.
DREI ANTWORTEN, NICHT ZWEI. Die naheliegende Fassung fragt TikTok und
setzt bei einem Fehlschlag "weg". Die waere an einem einzigen
schlechten Nachmittag von TikTok in der Lage, die halbe Galerie als
geloescht zu markieren -- und wer das hinterher sucht, sucht lange,
denn im Protokoll stuende sauber, dass gefragt wurde. Also:
Daten kommen an -> da. Zaehler zurueck, eine frueher gesetzte
Markierung faellt weg (privat gestellte
Videos kommen wieder).
"gibt es nicht" (404) -> ein Fehlversuch. Erst der DRITTE an drei
verschiedenen Tagen markiert.
niemand antwortet -> weiss nicht. Aendert gar nichts, nicht
einmal den Zeitstempel: Wer ihn setzte,
verschoebe die naechste Nachfrage um sieben
Tage, obwohl nichts gemessen wurde.
Drei Geschwindigkeiten beim Nachsehen: unauffaellig woechentlich,
verdaechtig taeglich (sonst dauerte eine Loeschung drei Wochen bis zur
Anzeige), schon markiert wieder woechentlich -- taeglich nachzufragen
waere Hammern fuer eine Auskunft, die wir schon haben. Gefunden hat
das eine rote Pruefung, nicht das Nachdenken davor.
Der Knopf verschwindet nicht, er sagt "Bei TikTok nicht mehr da".
Bewusst kein Rot: Es ist kein Fehler, sondern eine Auskunft -- Titel,
Text und Cover stimmen weiter, die liegen bei uns.
Geprueft: pruef-video 37 -> 51. Gegenprobe (Stoerung als "weg" werten
plus Markieren ab dem ersten Versuch) macht 8 rot, darunter woertlich
"eine Stoerung erhoeht den Zaehler NICHT (0 -> 1)".
Co-Authored-By: Claude Opus 5 <[email protected]>
463 lines
22 KiB
JavaScript
463 lines
22 KiB
JavaScript
/* =====================================================================
|
|
EIN VIDEO IN DIE APP HOLEN (17.09.2026) — Stufe A des Video-Plans
|
|
|
|
Filipe: „damit meine videos auch da auf der app rein kommen … sobald
|
|
ich ein video poste erscheint es sofort in der app für die ganze
|
|
community … dass man das Coverbild und den Text sieht … für meinen
|
|
Account, dann DogFather Clips und HasiDog."
|
|
|
|
---------------------------------------------------------------------
|
|
DER EIGENTLICHE PUNKT STEHT IN ABSCHNITT 6
|
|
|
|
TikToks Coverbild-Adresse ist signiert und läuft ab — gemessen am
|
|
17.09.2026 an einem echten Video: `x-expires` in 48 Stunden. Wer sie
|
|
nur speichert, hat übermorgen schwarze Kacheln. Beim Bauen merkt man
|
|
das nicht, beim Testen auch nicht: Es fällt erst am übernächsten Tag
|
|
auf, und dann sieht die ganze App kaputt aus.
|
|
|
|
Deshalb wird das Bild KOPIERT. Und genau das lässt sich beweisen:
|
|
Der nachgebaute TikTok-Dienst wird im letzten Abschnitt ABGESCHALTET.
|
|
Danach muss die alte Cover-Adresse ins Leere laufen und unsere
|
|
weiterhin ein Bild liefern. Wäre es nur verlinkt, wären beide tot —
|
|
genau wie übermorgen im Betrieb.
|
|
|
|
Deshalb steht das Abschalten auch GANZ HINTEN: Jeder Abschnitt davor
|
|
braucht den Dienst noch.
|
|
|
|
---------------------------------------------------------------------
|
|
OHNE ECHTES TIKTOK
|
|
|
|
Eine Prüfung, die das Netz braucht, ist langsam, hängt an fremder
|
|
Verfügbarkeit und wird irgendwann rot, ohne dass etwas kaputt ist —
|
|
und dann schaltet man sie ab. Hier läuft ein nachgebauter Dienst auf
|
|
127.0.0.1. Die Adresse dorthin kommt aus einer Umgebungsvariablen,
|
|
die es im Betrieb nicht gibt; steht sie nicht da, gilt TikTok.
|
|
|
|
Aufrufen mit: node server/pruef-video.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, existsSync, readdirSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createServer } from "node:http";
|
|
import { portMussFreiSein } from "./helfer-port.mjs";
|
|
|
|
const PORT = await portMussFreiSein(4499, "pruef-video");
|
|
const TIKTOK = await portMussFreiSein(4501, "pruef-video (nachgebautes TikTok)");
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-video-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
process.env.TIKTOK_OEMBED_BASIS = `http://127.0.0.1:${TIKTOK}/oembed`;
|
|
|
|
/* DER ABLAGEORT wird NICHT abgeschrieben, sondern abgeleitet: In
|
|
workspace.js steht `DATEN_ORDNER = dirname(DB_PFAD)`. Eine zweite
|
|
Rechnung hier wäre eine zweite Wahrheit -- und die veraltet. */
|
|
const ABLAGE = join(ordner, "dateien");
|
|
|
|
/* ---------- Das nachgebaute TikTok ----------------------------------- */
|
|
const PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
|
|
"base64");
|
|
let coverAbrufe = 0;
|
|
let oembedAbrufe = 0;
|
|
/* Welche Videos gerade „geloescht" sind und welche „stoeren".
|
|
Zwei getrennte Mengen, weil es zwei getrennte Faelle sind. */
|
|
const verschwunden = new Set();
|
|
const stoerung = new Set();
|
|
const coverAdresse = (was) => `http://127.0.0.1:${TIKTOK}/${was}`
|
|
+ `?x-expires=${Math.floor(Date.now() / 1000) + 172800}&x-signature=abc`;
|
|
|
|
const tiktok = createServer((q, a) => {
|
|
const u = new URL(q.url, `http://127.0.0.1:${TIKTOK}`);
|
|
if (u.pathname === "/oembed") {
|
|
oembedAbrufe++;
|
|
const ziel = u.searchParams.get("url") || "";
|
|
/* Welcher Account? Aus der angefragten Adresse -- beim echten
|
|
Dienst ist es genauso. */
|
|
const wer = (ziel.match(/@([^/?]+)/) || [])[1] || "";
|
|
if (!wer) { a.writeHead(404); a.end("{}"); return; }
|
|
/* UMSCHALTBAR FUER DIE WACHE (Abschnitt 5b): Ein Video kann
|
|
verschwinden und wiederkommen, und TikTok kann stoeren. Beides
|
|
sind verschiedene Dinge, und genau das muss messbar sein. */
|
|
if (verschwunden.has(ziel)) { a.writeHead(404); a.end("{}"); return; }
|
|
if (stoerung.has(ziel)) { a.writeHead(503); a.end("{}"); return; }
|
|
/* Welches Cover? Am Videonamen. So lassen sich die Sonderfälle
|
|
auslösen, ohne den Dienst umzubauen. */
|
|
let bild = coverAdresse("cover.jpg");
|
|
if (/kaputt/.test(ziel)) bild = coverAdresse("kein-bild.jpg");
|
|
if (/ohnecover/.test(ziel)) bild = null;
|
|
a.writeHead(200, { "content-type": "application/json" });
|
|
a.end(JSON.stringify({
|
|
version: "1.0", type: "video",
|
|
title: "Casper springt ins Bild \u{1F604} #fyp #hund #dogfather",
|
|
author_url: `https://www.tiktok.com/@${wer}`,
|
|
/* DER ANZEIGENAME ist bewusst bei allen derselbe -- wer ihn als
|
|
Schlüssel nimmt statt des Accounts, fällt in Abschnitt 3 auf. */
|
|
author_name: "Irgendein Anzeigename",
|
|
thumbnail_url: bild,
|
|
thumbnail_width: 576, thumbnail_height: 1024,
|
|
}));
|
|
return;
|
|
}
|
|
if (u.pathname === "/cover.jpg") {
|
|
coverAbrufe++;
|
|
a.writeHead(200, { "content-type": "image/jpeg" });
|
|
a.end(PNG);
|
|
return;
|
|
}
|
|
if (u.pathname === "/kein-bild.jpg") {
|
|
/* EINE HTML-SEITE, DIE SICH ALS BILD AUSGIBT. Genau der Fall, für
|
|
den die Byteprüfung gebaut wurde: Der Typ kommt hier von einem
|
|
FREMDEN Server, er ist also noch weniger wert als eine
|
|
Behauptung des eigenen Browsers. */
|
|
a.writeHead(200, { "content-type": "image/jpeg" });
|
|
a.end("<!DOCTYPE html><html><body>kein Bild, sondern HTML</body></html>");
|
|
return;
|
|
}
|
|
a.writeHead(404); a.end("{}");
|
|
});
|
|
await new Promise((r) => tiktok.listen(TIKTOK, "127.0.0.1", r));
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(180_000, "pruef-video");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const sch = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", sch, jetzt);
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync("CODE-DOGI-0001", salt, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen"
|
|
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
|
+ " VALUES (?,?,?,?,?,?,1,?)")
|
|
.run("Filipe", "admin", hash, salt, 32768,
|
|
createHmac("sha256", sch).update("CODE-DOGI-0001").digest("hex"), jetzt);
|
|
d.close();
|
|
|
|
const an = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
|
|
});
|
|
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
const kopf = { "Content-Type": "application/json", cookie: keks, origin: BASIS };
|
|
|
|
async function holen(url, bereich = "highlight") {
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/${bereich}/video`, {
|
|
method: "POST", headers: kopf, body: JSON.stringify({ url }),
|
|
});
|
|
return { status: a.status, daten: await a.json().catch(() => null) };
|
|
}
|
|
async function liste(bereich = "highlight") {
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/${bereich}`, { headers: { cookie: keks } });
|
|
return (await a.json().catch(() => null))?.eintraege || [];
|
|
}
|
|
const videoVon = (handle, nr = "123") =>
|
|
`http://127.0.0.1:${TIKTOK}/@${handle}/video/${nr}`;
|
|
const dateienImOrdner = () => {
|
|
try { return readdirSync(ABLAGE).length; } catch { return 0; }
|
|
};
|
|
|
|
/* =======================================================================
|
|
1. EIN LINK WIRD ZU EINEM EINTRAG
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Der Link ===");
|
|
let eintragId = 0, bildId = 0;
|
|
{
|
|
const a = await holen(videoVon("dogfather0804"));
|
|
ok(a.status === 201, `ein Video von DogFather wird eingelesen (${a.status} ${a.daten?.fehler || ""})`);
|
|
eintragId = a.daten?.id || 0;
|
|
ok(a.daten?.kanal === "dogfather", `und dem richtigen Kanal zugeordnet (${a.daten?.kanal})`);
|
|
|
|
const e = (await liste()).find((x) => x.id === eintragId);
|
|
ok(/Casper springt ins Bild/.test(e?.titel || ""),
|
|
`der Text des Videos steht da ("${(e?.titel || "").slice(0, 40)}…")`);
|
|
ok(e?.quelle_url === videoVon("dogfather0804"),
|
|
"die Adresse zum Video auch -- sonst käme niemand zum Liken hin");
|
|
ok(e?.quelle_kanal === "dogfather", `und der Kanal am Eintrag (${e?.quelle_kanal})`);
|
|
ok((e?.bilder || []).length === 1, `ein Coverbild hängt dran (${(e?.bilder || []).length})`);
|
|
bildId = e?.bilder?.[0]?.id || 0;
|
|
ok(coverAbrufe === 1, `das Cover wurde genau einmal geholt (${coverAbrufe})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. DAS COVER LIEGT BEI UNS
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Wo das Bild liegt ===");
|
|
{
|
|
const r = await fetch(`${BASIS}/workspace/api/dateien/${bildId}/bild`, { headers: { cookie: keks } });
|
|
ok(r.status === 200 && (r.headers.get("content-type") || "").startsWith("image/"),
|
|
`es kommt über unseren eigenen Weg (${r.status}, ${r.headers.get("content-type")})`);
|
|
const bytes = Buffer.from(await r.arrayBuffer());
|
|
ok(bytes.length === PNG.length && bytes.equals(PNG),
|
|
`Byte für Byte dasselbe Bild (${bytes.length} von ${PNG.length})`);
|
|
|
|
/* UND IN DER AUSKUNFT STEHT KEINE FREMDE ADRESSE. Ein Eintrag, dessen
|
|
Bildadresse nach draußen zeigt, wäre genau der Fehler, den
|
|
Abschnitt 6 misst -- nur fiele er dort dann nicht mehr auf, weil
|
|
die Kachel ja "ein Bild hat". */
|
|
const e = (await liste()).find((x) => x.id === eintragId);
|
|
const alsText = JSON.stringify(e?.bilder || []);
|
|
ok(!/tiktok|x-expires|127\.0\.0\.1/i.test(alsText),
|
|
`und keine abgelaufene Fremdadresse (${alsText.slice(0, 60)})`);
|
|
ok(dateienImOrdner() === 1, `eine Datei liegt in der Ablage (${dateienImOrdner()})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. ALLE DREI KANÄLE, JEDER AN SEINEN PLATZ
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Die drei Kanäle ===");
|
|
{
|
|
/* NICHT ABGESCHRIEBEN: Die Handles kommen aus KANAELE selbst. Eine
|
|
eigene Liste hier wäre eine zweite Wahrheit -- genau die Falle, die
|
|
in diesem Haus schon dreimal zugeschlagen hat. */
|
|
const { KANAELE } = await import("./workspace.js");
|
|
ok(KANAELE.length === 3, `es sind drei Kanäle (${KANAELE.map((k) => k.wert).join(", ")})`);
|
|
let n = 500;
|
|
for (const k of KANAELE.filter((x) => x.wert !== "dogfather")) {
|
|
const a = await holen(videoVon(k.handle.replace(/^@/, ""), String(++n)));
|
|
ok(a.status === 201 && a.daten?.kanal === k.wert,
|
|
`${k.handle} landet auf „${k.name}" (${a.status}, ${a.daten?.kanal})`);
|
|
}
|
|
/* UND DER ANZEIGENAME SPIELT KEINE ROLLE. Der nachgebaute Dienst
|
|
schickt bei allen denselben ("Irgendein Anzeigename"). Wäre er der
|
|
Schlüssel, lägen jetzt alle drei auf demselben Kanal. */
|
|
const kanaele = new Set((await liste()).map((e) => e.quelle_kanal).filter(Boolean));
|
|
ok(kanaele.size === 3,
|
|
`drei verschiedene Kanäle in der Liste (${[...kanaele].join(", ")})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. WAS NICHT HINEINKOMMT
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Die Gegenprobe ===");
|
|
{
|
|
const vorher = (await liste()).length;
|
|
|
|
/* EIN FREMDER ACCOUNT. Das ist nicht bloß falsch einsortiert -- es
|
|
wäre fremder Inhalt unter Filipes Namen. */
|
|
const f = await holen(videoVon("irgendwerAnders", "777"));
|
|
ok(f.status === 403, `ein fremder Account wird abgelehnt (${f.status})`);
|
|
ok(/keiner deiner Kan/i.test(f.daten?.fehler || ""),
|
|
`und es steht auch da, warum ("${(f.daten?.fehler || "").slice(0, 48)}…")`);
|
|
|
|
/* KEINE TIKTOK-ADRESSE. Geprüft wird der Rechnername, nicht die
|
|
Zeichenkette -- "tiktok.com.beispiel.de" gehört jemand anderem. */
|
|
for (const boese of [
|
|
"https://tiktok.com.beispiel.de/@dogfather0804/video/1",
|
|
"https://beispiel.de/@dogfather0804/video/1",
|
|
"nicht mal eine Adresse",
|
|
"",
|
|
]) {
|
|
const x = await holen(boese);
|
|
ok(x.status === 400, `abgelehnt: ${boese.slice(0, 44) || "(leer)"} (${x.status})`);
|
|
}
|
|
|
|
/* DERSELBE LINK ZWEIMAL. Wer den Knopf zweimal drückt, meint nicht
|
|
zwei Videos. */
|
|
const wieder = await holen(videoVon("dogfather0804"));
|
|
ok(wieder.status === 200 && wieder.daten?.schon === true,
|
|
`derselbe Link gibt denselben Eintrag zurück (${wieder.status}, schon=${wieder.daten?.schon})`);
|
|
ok(wieder.daten?.id === eintragId, `und zwar wirklich denselben (${wieder.daten?.id})`);
|
|
ok(coverAbrufe === 3,
|
|
`und holt das Cover kein zweites Mal (${coverAbrufe} Abrufe für 3 Videos)`);
|
|
|
|
const nachher = (await liste()).length;
|
|
ok(nachher === vorher, `kein Eintrag ist dazugekommen (${nachher} von ${vorher})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. WAS SICH ALS BILD AUSGIBT, ABER KEINS IST
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Die Byteprüfung ===");
|
|
{
|
|
const vorherDateien = dateienImOrdner();
|
|
|
|
/* Der Dienst schickt HTML mit `content-type: image/jpeg`. Ein
|
|
fremder Server darf den Typ behaupten -- geglaubt wird ihm nicht. */
|
|
const a = await holen(videoVon("dogfather0804", "kaputt"));
|
|
ok(a.status === 201, `das Video kommt trotzdem rein (${a.status})`);
|
|
const e = (await liste()).find((x) => x.id === a.daten?.id);
|
|
ok((e?.bilder || []).length === 0,
|
|
`aber ohne Cover -- die Bytes waren kein Bild (${(e?.bilder || []).length})`);
|
|
ok(dateienImOrdner() === vorherDateien,
|
|
`und nichts wurde abgelegt (${dateienImOrdner()} von ${vorherDateien})`);
|
|
ok(/Casper/.test(e?.titel || ""),
|
|
"der Text ist da -- ein fehlendes Cover wirft nicht das ganze Video weg");
|
|
|
|
/* UND EIN VIDEO GANZ OHNE COVER stürzt nicht ab. */
|
|
const b = await holen(videoVon("dogfather0804", "ohnecover"));
|
|
ok(b.status === 201, `ein Video ohne Coverbild geht auch (${b.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5b. FUEHRT DER KNOPF NOCH IRGENDWOHIN? (Stufe 5)
|
|
|
|
Der wichtigste Satz dieses Abschnitts ist nicht „geloeschte Videos
|
|
werden erkannt", sondern: EINE STOERUNG BEI TIKTOK AENDERT NICHTS.
|
|
Die naheliegende Fassung setzt bei jedem misslungenen Abruf „weg" --
|
|
und markiert an einem schlechten Nachmittag die halbe Galerie als
|
|
geloescht. Wer das hinterher sucht, sucht lange, denn im Protokoll
|
|
steht dann sauber, dass TikTok gefragt wurde.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5b. Wenn ein Video bei TikTok verschwindet ===");
|
|
{
|
|
const video = await import("./workspace-video.js");
|
|
const db2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const nimm = (id) => db2.prepare(
|
|
"SELECT quelle_geprueft_am, quelle_fehlt_seit,"
|
|
+ " COALESCE(quelle_fehlversuche,0) AS fehl FROM eintraege WHERE id = ?").get(id);
|
|
/* Stellt den naechsten Tag dar. Die Wache sieht verdaechtige Videos
|
|
TAEGLICH nach -- ohne Zurueckdatieren waeren alle drei Laeufe am
|
|
selben Tag, und nur der erste taete etwas. Genau so ist es
|
|
gewollt; die Pruefung muss deshalb die Uhr weiterstellen, nicht
|
|
die Regel umgehen. */
|
|
const morgen = (id, stunden = 36) => db2.prepare(
|
|
"UPDATE eintraege SET quelle_geprueft_am = ? WHERE id = ?")
|
|
.run(new Date(Date.now() - stunden * 3600_000).toISOString(), id);
|
|
|
|
/* Derselbe Account wie in Abschnitt 1 -- ein ERFUNDENER wird von der
|
|
Route zu Recht abgewiesen, und die Pruefung haette dann gemessen,
|
|
dass sie sich selbst vertippt hat. */
|
|
const adresse = videoVon("dogfather0804", "5b1");
|
|
const ein = await holen(adresse);
|
|
const id5b = ein.daten?.id;
|
|
ok(ein.status === 201 && !!id5b, `ein Video zum Beobachten ist da (#${id5b})`);
|
|
|
|
/* ---- Die drei Antworten einzeln ---------------------------------- */
|
|
ok((await video.videoNochDa(adresse)).da === true,
|
|
"ein vorhandenes Video meldet: da");
|
|
stoerung.add(adresse);
|
|
const beiStoerung = await video.videoNochDa(adresse);
|
|
ok(beiStoerung.da === null,
|
|
`eine Stoerung meldet WEISS NICHT, nicht „weg" (${beiStoerung.grund})`);
|
|
stoerung.delete(adresse);
|
|
verschwunden.add(adresse);
|
|
const beiWeg = await video.videoNochDa(adresse);
|
|
ok(beiWeg.da === false, `ein geloeschtes meldet: weg (${beiWeg.grund})`);
|
|
|
|
/* ---- Ein einzelner Fehlversuch markiert NICHT --------------------- */
|
|
const l1 = await video.videoWacheLauf();
|
|
const n1 = nimm(id5b);
|
|
ok(n1.fehl === 1 && !n1.quelle_fehlt_seit,
|
|
`nach dem ersten Fehlversuch ist es noch NICHT „weg" (Zaehler ${n1.fehl})`);
|
|
|
|
morgen(id5b);
|
|
await video.videoWacheLauf();
|
|
const n2 = nimm(id5b);
|
|
ok(n2.fehl === 2 && !n2.quelle_fehlt_seit,
|
|
`nach dem zweiten auch nicht (Zaehler ${n2.fehl})`);
|
|
|
|
morgen(id5b);
|
|
const l3 = await video.videoWacheLauf();
|
|
const n3 = nimm(id5b);
|
|
ok(n3.fehl === 3 && !!n3.quelle_fehlt_seit,
|
|
`erst der dritte markiert es (Zaehler ${n3.fehl}, seit ${String(n3.quelle_fehlt_seit).slice(0, 10)})`);
|
|
ok(l3.weg === 1, `und der Lauf sagt es auch (weg: ${l3.weg})`);
|
|
|
|
/* ---- Die Oberflaeche erfaehrt davon ------------------------------- */
|
|
const e5b = (await liste()).find((x) => x.id === id5b);
|
|
ok(!!e5b?.quelle_fehlt_seit,
|
|
"die Liste liefert den Zustand mit -- sonst zeigte die Kachel weiter den Knopf");
|
|
|
|
/* ---- DER KERN: eine Stoerung aendert NICHTS ----------------------- */
|
|
verschwunden.delete(adresse);
|
|
/* ACHT TAGE, nicht 36 Stunden: Ein bereits markiertes Video wird
|
|
absichtlich nur noch woechentlich nachgefragt (siehe die drei
|
|
Geschwindigkeiten in workspace-video.js). Die Pruefung muss die
|
|
echte Regel abbilden, nicht die bequemere. */
|
|
morgen(id5b, 8 * 24);
|
|
await video.videoWacheLauf(); /* wieder da -> Zaehler zurueck */
|
|
const zurueck = nimm(id5b);
|
|
ok(zurueck.fehl === 0 && !zurueck.quelle_fehlt_seit,
|
|
"kommt es zurueck (privat gestellt und wieder oeffentlich), faellt die Markierung weg");
|
|
|
|
/* Ebenfalls acht Tage: Nach der Rueckkehr steht der Zaehler auf
|
|
null, das Video gilt also wieder als unauffaellig -- und
|
|
unauffaellige werden nur woechentlich gefragt. */
|
|
morgen(id5b, 8 * 24);
|
|
stoerung.add(adresse);
|
|
const vorher5b = nimm(id5b);
|
|
const lS = await video.videoWacheLauf();
|
|
const nachher5b = nimm(id5b);
|
|
stoerung.delete(adresse);
|
|
ok(nachher5b.fehl === vorher5b.fehl && !nachher5b.quelle_fehlt_seit,
|
|
`eine Stoerung erhoeht den Zaehler NICHT (${vorher5b.fehl} -> ${nachher5b.fehl})`);
|
|
ok(nachher5b.quelle_geprueft_am === vorher5b.quelle_geprueft_am,
|
|
"und verschiebt auch den Zeitpunkt nicht -- sonst waere sieben Tage Ruhe,"
|
|
+ " obwohl nichts gemessen wurde");
|
|
ok(lS.unklar >= 1 && lS.geprueft === 0,
|
|
`der Lauf zaehlt sie als „weiss nicht" (unklar ${lS.unklar}, geprueft ${lS.geprueft})`);
|
|
|
|
/* ---- Und ein gesundes Video bleibt unberuehrt --------------------- */
|
|
const heil = (await liste()).find((x) => x.id === eintragId);
|
|
ok(!heil?.quelle_fehlt_seit,
|
|
"ein Video, das es noch gibt, wird nicht angefasst");
|
|
|
|
db2.close();
|
|
void l1;
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. UND ES ÜBERLEBT, WENN TIKTOK WEG IST ← der eigentliche Punkt
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6. Wenn die Quelle verschwindet ===");
|
|
{
|
|
/* Die Adresse, die TikTok uns gegeben hat -- mit ihrem Ablaufstempel. */
|
|
const fremd = coverAdresse("cover.jpg");
|
|
const vorher = await fetch(fremd).then((r) => r.ok).catch(() => false);
|
|
ok(vorher, "vorher liefert die TikTok-Adresse noch ein Bild");
|
|
|
|
/* JETZT WIRD ABGESCHALTET. Dasselbe passiert übermorgen im Betrieb,
|
|
wenn `x-expires` abläuft: Die Adresse antwortet nicht mehr. */
|
|
await new Promise((r) => tiktok.close(r));
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
|
|
const nachher = await fetch(fremd).then((r) => r.ok).catch(() => false);
|
|
ok(!nachher, "danach ist sie tot -- genau wie nach 48 Stunden");
|
|
|
|
const r = await fetch(`${BASIS}/workspace/api/dateien/${bildId}/bild`, { headers: { cookie: keks } });
|
|
ok(r.status === 200, `unser Cover ist trotzdem noch da (${r.status})`);
|
|
const bytes = Buffer.from(await r.arrayBuffer());
|
|
ok(bytes.equals(PNG), `und unverändert (${bytes.length} von ${PNG.length} Bytes)`);
|
|
ok(existsSync(ABLAGE), "es liegt als Datei in unserer eigenen Ablage");
|
|
|
|
/* UND DIE KACHEL ZEIGT IHN WEITERHIN: Der Eintrag hat sein Bild noch,
|
|
die Liste kommt ohne den fremden Dienst aus. */
|
|
const e = (await liste()).find((x) => x.id === eintragId);
|
|
ok((e?.bilder || []).length === 1,
|
|
`die Kachel hat ihr Bild noch (${(e?.bilder || []).length})`);
|
|
ok(oembedAbrufe >= 5,
|
|
`der fremde Dienst wurde beim Einlesen gefragt, nie beim Anzeigen (${oembedAbrufe})`);
|
|
}
|
|
|
|
melde("");
|
|
melde(`${geprueft} Prüfungen, ${fehler} Fehler`);
|
|
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
|
process.exit(fehler ? 1 : 0);
|