- Neues Modul lib/tiktok-live.js liest den Live-Status direkt von der oeffentlichen TikTok-Live-Seite (SIGI_STATE -> liveRoom.status, 2 = live, alles andere = offline; bestaetigt durch yt-dlp) - Cron-Trigger im Worker prueft jede Minute und schreibt das Ergebnis nach app_settings; schlaegt ein Abruf fehl, bleibt der letzte gute Stand stehen statt faelschlich auf offline zu springen - /live-status liefert jetzt Quelle, Titel und Zeitpunkt der Pruefung - Punkt ist rot wenn offline, gruen+pulsierend wenn live, grau solange der Status laedt; Startseite aktualisiert alle 30 s ohne Neuladen - Handschaltung bleibt als Notfall-Vorrang mit 3-Stunden-Ablauf, plus Knopf "Zurueck zur Automatik" im Postfach Co-Authored-By: Claude Opus 5 <[email protected]>
94 lines
3.5 KiB
JavaScript
94 lines
3.5 KiB
JavaScript
/* =====================================================================
|
|
lib/tiktok-live.js — Automatische Live-Erkennung für TikTok
|
|
|
|
TikTok bietet KEINE offizielle API, die einem sagt "ist Nutzer X gerade
|
|
live?". Was aber öffentlich funktioniert: die Seite
|
|
https://www.tiktok.com/@<name>/live liefert im HTML ein JSON-Objekt
|
|
(<script id="SIGI_STATE">), in dem der Raum-Status steht.
|
|
|
|
Bedeutung von liveRoom.status (aus Beobachtung, nicht offiziell):
|
|
2 = läuft gerade (LIVE)
|
|
4 = beendet / offline
|
|
Alles andere behandeln wir sicherheitshalber als "offline".
|
|
|
|
Weil das eine inoffizielle Quelle ist, kann TikTok sie jederzeit ändern.
|
|
Deshalb: jeder Fehler wird sauber gemeldet (ok:false + reason) und der
|
|
zuletzt bekannte Status bleibt stehen, statt fälschlich auf offline zu
|
|
springen. Zusätzlich gibt es im Postfach immer den manuellen Schalter.
|
|
===================================================================== */
|
|
|
|
const LIVE_STATUS_RUNNING = 2;
|
|
|
|
const BROWSER_HEADERS = {
|
|
"User-Agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
"Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
|
|
"Cache-Control": "no-cache",
|
|
Pragma: "no-cache",
|
|
};
|
|
|
|
/** Schneidet den Inhalt eines <script id="..."> aus dem HTML heraus. */
|
|
function extractScriptJson(html, id) {
|
|
const marker = `<script id="${id}"`;
|
|
const start = html.indexOf(marker);
|
|
if (start === -1) return null;
|
|
const open = html.indexOf(">", start);
|
|
if (open === -1) return null;
|
|
const end = html.indexOf("</script>", open);
|
|
if (end === -1) return null;
|
|
try {
|
|
return JSON.parse(html.slice(open + 1, end));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fragt bei TikTok nach, ob der Kanal gerade live ist.
|
|
* @returns {Promise<{ok:boolean, live?:boolean, title?:string, roomId?:string,
|
|
* followers?:number, rawStatus?:number, reason?:string}>}
|
|
*/
|
|
export async function checkTikTokLive(username) {
|
|
const handle = String(username || "").replace(/^@/, "").trim();
|
|
if (!handle) return { ok: false, reason: "kein-benutzername" };
|
|
|
|
let res;
|
|
try {
|
|
res = await fetch(`https://www.tiktok.com/@${encodeURIComponent(handle)}/live`, {
|
|
headers: BROWSER_HEADERS,
|
|
redirect: "follow",
|
|
cf: { cacheTtl: 0, cacheEverything: false },
|
|
});
|
|
} catch (err) {
|
|
return { ok: false, reason: `netzwerkfehler: ${String(err).slice(0, 120)}` };
|
|
}
|
|
|
|
if (!res.ok) return { ok: false, reason: `http-${res.status}` };
|
|
|
|
const html = await res.text();
|
|
if (html.length < 5000) return { ok: false, reason: "seite-zu-klein-vermutlich-blockiert" };
|
|
|
|
// Bevorzugt SIGI_STATE, ersatzweise das neuere Rehydration-Objekt.
|
|
const sigi = extractScriptJson(html, "SIGI_STATE");
|
|
let info = sigi?.LiveRoom?.liveRoomUserInfo;
|
|
|
|
if (!info) {
|
|
const universal = extractScriptJson(html, "__UNIVERSAL_DATA_FOR_REHYDRATION__");
|
|
info = universal?.__DEFAULT_SCOPE__?.["webapp.live-detail"]?.liveRoomUserInfo;
|
|
}
|
|
if (!info) return { ok: false, reason: "struktur-nicht-gefunden" };
|
|
|
|
const status = info.liveRoom?.status;
|
|
if (typeof status !== "number") return { ok: false, reason: "kein-status-feld" };
|
|
|
|
return {
|
|
ok: true,
|
|
live: status === LIVE_STATUS_RUNNING,
|
|
rawStatus: status,
|
|
title: info.liveRoom?.title || null,
|
|
roomId: info.user?.roomId ? String(info.user.roomId) : null,
|
|
followers: info.stats?.followerCount ?? null,
|
|
};
|
|
}
|