diff --git a/server/workspace-video.js b/server/workspace-video.js index 96155788..0dda8119 100644 --- a/server/workspace-video.js +++ b/server/workspace-video.js @@ -140,14 +140,81 @@ async function holeMitFrist(adresse, kopf = {}) { const OEMBED_BASIS = process.env.TIKTOK_OEMBED_BASIS || "https://www.tiktok.com/oembed"; /** Die Auskunft zu einem Video: Titel, Cover-Adresse, Account. */ -export async function videoAuskunft(url) { +/** Wohin fuehrt ein Kurzlink wirklich -- ohne die Seite zu laden. + * + * `vm.tiktok.com/ZN8kdxP9N/` verraet von aussen nicht, ob dahinter + * ein Video oder ein Fotopost steckt. Nur die Weiterleitung sagt es. + * Gelesen wird deshalb NUR der Ort-Kopf (`redirect: "manual"`); die + * Seite selbst wird nie geholt -- sie ist ein Megabyte gross und + * enthaelt nichts, was wir brauchen. */ +async function endgueltigeAdresse(url) { + let jetzt = String(url); + for (let i = 0; i < 5; i++) { + const abbruch = new AbortController(); + const uhr = setTimeout(() => abbruch.abort(), FRIST_MS); + let a; + try { + a = await fetch(jetzt, { method: "GET", redirect: "manual", signal: abbruch.signal }); + } catch { return jetzt; } finally { clearTimeout(uhr); } + /* Den Koerper wegwerfen, nicht lesen -- sonst bleibt die + Verbindung offen und wir laden die ganze Seite umsonst. */ + try { await a.body?.cancel(); } catch { /* egal */ } + const ort = a.headers.get("location"); + if (!ort) return jetzt; + try { jetzt = new URL(ort, jetzt).toString(); } catch { return jetzt; } + } + return jetzt; +} + +/** Ein Fotopost unter derselben Nummer als Video angefragt. + * + * WARUM DAS NOETIG IST (05.10.2026, gemessen am echten Konto): + * + * .../photo/7693113561568136470 -> 400 "Something went wrong" + * .../video/7693113561568136470 -> 200 mit Titel + * + * TikToks Auskunft kennt Fotoposts (Slideshows) nicht, dieselbe + * Nummer unter /video/ aber schon. Filipe postet seit kurzem solche + * Beitraege -- deshalb "es ging doch immer bis jetzt": Es lag weder + * an einer Auslieferung noch am Haus, sondern an der Art des Posts. + * + * Gibt es nichts zu drehen, kommt `null` zurueck -- dann bleibt es + * bei der ersten Antwort und ihrer Fehlermeldung. */ +function alsVideoFassung(url) { + try { + const u = new URL(String(url)); + if (!/\/photo\/\d+/.test(u.pathname)) return null; + u.pathname = u.pathname.replace("/photo/", "/video/"); + return u.toString(); + } catch { return null; } +} + +async function auskunftHolen(url) { const a = await holeMitFrist( `${OEMBED_BASIS}?url=${encodeURIComponent(url)}`, { accept: "application/json" }); - if (!a.ok) return { fehler: `TikTok antwortet nicht (${a.status}).` }; + if (!a.ok) return { status: a.status, daten: 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." }; + return { status: a.status, daten: j }; +} + +export async function videoAuskunft(url) { + let versuch = await auskunftHolen(url); + + /* ZWEITER ANLAUF FUER FOTOPOSTS. Nur wenn der erste scheitert -- + bei einem gewoehnlichen Video kostet das keine einzige zusaetzliche + Anfrage. Erst den Kurzlink aufloesen (er verraet von aussen nicht, + was dahinter steckt), dann /photo/ auf /video/ drehen. */ + if (!versuch.daten) { + const echt = await endgueltigeAdresse(url); + const gedreht = alsVideoFassung(echt); + if (gedreht) versuch = await auskunftHolen(gedreht); + } + + const j = versuch.daten; + if (!j) return { fehler: `TikTok antwortet nicht (${versuch.status}).` }; + if (!j.title) return { fehler: "TikTok hat nichts zu diesem Video geschickt." }; /* Der Account steht in `author_url` (…/@name). `author_name` ist der ANZEIGENAME und aendert sich, wenn jemand ihn umstellt -- als Schluessel waere er die falsche Wahl. */