Link einfuegen, Video steht in der App -- 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 fuer die ganze
community ... dass man das Coverbild und den Text sieht ... fuer meinen
Account, dann DogFather Clips und HasiDog."

Der Weg, der HEUTE funktioniert -- ohne Freigabe, ohne Geheimnis, ohne
Wartezeit: Adresse einfuegen, alles andere geht von allein.

  Link -> oEmbed (Titel, Cover-Adresse, Account)
       -> Cover HERUNTERLADEN und bei uns ablegen
       -> Eintrag mit Bild, Text und Knopf zum Video

DAS COVER WIRD KOPIERT, NICHT VERLINKT -- der wichtigste Satz.
TikToks Bildadresse ist signiert und laeuft ab. Heute an einem echten
Video nachgemessen: x-expires = 1789812000, also in 48 Stunden. Wer sie
nur speichert, hat uebermorgen schwarze Kacheln. Beim Bauen merkt man
das nicht und beim Testen auch nicht -- es faellt erst am uebernaechsten
Tag auf, und dann sieht die ganze App kaputt aus.

Deshalb wandert das Bild in dieselbe Ablage wie jeder Anhang und geht
durch dieselbe Bytepruefung. Ein heruntergeladenes Cover ist eine
FREMDE Datei; dass der Typ hier von einem fremden Server behauptet wird
statt vom eigenen Browser, macht ihn nicht glaubwuerdiger.

NUR SEINE EIGENEN KANAELE. Der Account aus der Antwort wird gegen
KANAELE geprueft (kanalVonHandle). Genommen wird author_url (.../@name),
nicht author_name -- der Anzeigename aendert sich, wenn jemand ihn
umstellt. Ein fremdes Video in Filipes Highlights waere nicht bloss
falsch einsortiert, es waere fremder Inhalt unter seinem Namen: 403.

DIE PRUEFUNG SCHALTET TIKTOK AB (server/pruef-video.mjs, 37 Pruefungen)
Ein nachgebauter Dienst auf 127.0.0.1 antwortet wie TikTok, mitsamt
Ablaufstempel. Im letzten Abschnitt wird er ABGESCHALTET. Danach muss
die alte Cover-Adresse ins Leere laufen und unsere weiterhin ein Bild
liefern -- beides wird gemessen, nebeneinander. Waere das Bild nur
verlinkt, waeren beide tot, genau wie uebermorgen im Betrieb.

Die Adresse des Dienstes kommt aus TIKTOK_OEMBED_BASIS. Steht sie nicht
da, gilt TikTok -- kein Verhalten, das sich still aendert. Eine
Pruefung, die das echte TikTok braucht, misst fremde Verfuegbarkeit
statt unseren Code und wird irgendwann rot, ohne dass etwas kaputt ist.

VIER GEGENPROBEN, weil 37 von 37 im ersten Lauf kein Beweis ist:
  a) Cover nicht mehr kopieren      -> 9 Pruefungen rot
  b) author_name statt author_url   -> 22 rot
  c) Bytepruefung entfernen         -> 2 rot (genau Abschnitt 5)
  d) Dublettenpruefung entfernen    -> 4 rot (genau Abschnitt 4)
Jede schlaegt dort an, wo sie soll. Und eine fuenfte Gegenprobe ist ins
Leere gelaufen: Der Patch lief ueber python3, das es hier nicht gibt --
die Datei blieb unveraendert und der Lauf war gruen. Haette ich die
Ausgabe nicht gelesen, haette ich behauptet, die Pruefung koenne rot
werden, ohne sie je rot gesehen zu haben. Der dritte Ausgang, an mir
selbst.

DAZU:
- eintraege.quelle_url / quelle_kanal / quelle_geholt_am (Umstellung)
- KANAELE bekommt handle, dazu kanalVonHandle()
- videoRouter steht VOR bereicheRouter, sonst schluckt /:bereich ihn
- Eingabefeld und Kanal-Knopf in bereich.html/.js, Farben wie bei den
  Aufgaben-Kanaelen
- Stempel 202609171249

Nicht darin: die Kanal-Filterzeile in der Galerie, die Handy-Verknuepfung
(Teilen -> Workspace) und der Zustand "nicht mehr da" fuer geloeschte
Videos. Stufe B (Display API) braucht Filipes Entwickler-Freigabe.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 12:50:42 +02:00
co-authored by Claude Opus 5
parent 4a440e42d8
commit f05783ebc8
37 changed files with 1124 additions and 363 deletions
+78
View File
@@ -56,6 +56,12 @@
/* Wie ein Brett in einem Satz heisst. Kurz, weil es mitten in einer
Zeile steht: „Aus Wunschliste: …" liest sich, „Aus dem Bereich
Wunschliste (wunsch): …" nicht. */
/* Wie ein Kanal auf der Karte heisst. Kurz -- er steht neben einem
Knopf, nicht in einer Ueberschrift. */
const KANAL_WORT = {
dogfather: 'DogFather', clips: 'DogFather Clips', hasidog: 'HasiDog',
};
const BRETT_WORT = {
anschlag: 'Anschlagbrett', ansteht: 'Was ansteht', treff: 'Treff',
wunsch: 'Wunschliste', highlight: 'Highlights', regeln: 'Regeln',
@@ -388,6 +394,29 @@
k.append(kopf);
/* ---- DAS VIDEO (17.09.2026), Stufe A des Video-Plans ----------
Der Knopf ist die Antwort auf "und dann liken": Er fuehrt
dorthin, wo das Liken zaehlt. Ein Herz bei uns, das bei TikTok
nichts bewirkt, waere eine Attrappe.
`rel="noopener"` ist Pflicht: Ohne das kann die geoeffnete Seite
ueber `window.opener` auf unsere zugreifen. */
if (e.quelle_url) {
const z = el('div', 'videozeile');
if (e.quelle_kanal) {
const k = el('span', 'videozeile__kanal',
KANAL_WORT[e.quelle_kanal] || e.quelle_kanal);
k.dataset.kanal = e.quelle_kanal;
z.append(k);
}
const a = el('a', 'schritt videozeile__weg', 'Auf TikTok ansehen →');
a.href = e.quelle_url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
z.append(a);
k.append(z);
}
/* ---- DIE KETTE (17.09.2026), Stufe 8 des Plans -----------------
Woher kam das hier, und was ist daraus geworden? Beides steht
auf der Karte, nicht in einem Detail: Der Zusammenhang ist die
@@ -914,6 +943,53 @@
} catch { uebernahmen = {}; uebernahmeMoeglich = false; }
}
/* ---- EIN VIDEO EINLESEN ---------------------------------------
Link einfuegen, den Rest macht der Server. Nur bei den Highlights:
Dort gehoeren Clips hin, und ein Feld an jedem anderen Brett waere
eine Einladung, sie ueberall hinzuschreiben. */
function videoFeldZeigen() {
const ziel = $('videoholen');
if (!ziel) return;
ziel.hidden = !(bereich === 'highlight' && darfEintragen());
}
function videoFeldBedienen() {
const form = $('video-form');
if (!form || form.dataset.bereit === 'ja') return;
form.dataset.bereit = 'ja';
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
const feld = $('video-url');
const knopf = $('video-holen');
const url = feld.value.trim();
if (!url) return;
knopf.disabled = true;
/* "Wird geholt" statt eines stillen Wartens: Der Server fragt
TikTok, und das dauert einen Moment. Ein Knopf, der nichts tut
und nichts sagt, wird ein zweites Mal gedrueckt. */
const hinweis = $('video-hinweis');
const alterText = hinweis?.textContent || '';
if (hinweis) hinweis.textContent = 'Wird geholt …';
try {
const a = await hole(`/workspace/api/bereich/${bereich}/video`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
});
const r = await a.json().catch(() => ({}));
if (!a.ok) {
melde(r.fehler || 'Ging nicht.');
if (hinweis) hinweis.textContent = alterText;
knopf.disabled = false;
return;
}
feld.value = '';
if (hinweis) hinweis.textContent = r.schon
? 'Das Video war schon da.' : alterText;
await laden();
} finally { knopf.disabled = false; }
});
}
/* ---- DER STARTKATALOG ------------------------------------------------
Fertige Texte, die jemand aus dem Team uebernimmt. Nicht
automatisch: Ein Text, den niemand gelesen hat, stuende sonst als
@@ -1017,6 +1093,8 @@
alles uebernommen ist -- und das haengt an dem, was gerade
geladen wurde. */
startkatalogZeigen();
videoFeldZeigen();
videoFeldBedienen();
maxStimmen = (daten.eintraege || [])
.reduce((n, e) => Math.max(n, Number(e.stimmen) || 0), 0);