Die TikTok-Datei des Creators -- ohne Entwicklerkonto

Filipe: "tiktok entwicklerkonto hab ich nicht und werd ich nicht
bekommen."

Damit sind zwei der drei Wege aus dem Plan vom 10.09. erledigt: Login
Kit und die Datenportabilitaets-API haengen beide an einer App auf
developers.tiktok.com. Sie sind nicht verschoben, sie sind weg.

DER DRITTE BRAUCHT KEINES. Jeder TikTok-Nutzer kann seine eigenen Daten
selbst herunterladen (Profil -> Einstellungen -> Konto -> Deine Daten
herunterladen, Format JSON). Die Datei enthaelt laut TikToks eigener
Datentypen-Liste einen Abschnitt "TikTok LIVE" mit dem Go-LIVE-Verlauf.
Der Creator gibt sie her, mit eigenen Haenden -- legitimer wird es
nicht, und es ist jetzt der einzige verbliebene Weg an TikTok-Daten.

ICH KENNE DAS FORMAT NICHT -- UND BAUE TROTZDEM.
TikTok dokumentiert, DASS es den Verlauf gibt, nicht wie die Schluessel
heissen. Der naheliegende Weg waere, Namen zu raten. Das waere die
schlechteste Loesung: Er fiele bei der ersten echten Datei auseinander,
und zwar STILL -- "0 Eintraege gefunden" sieht aus wie "war nicht live".

Deshalb wird gesucht statt geraten. Der Leser geht die Datei durch,
sammelt ALLE Listen, deren Eintraege ein Datum tragen (gemessen am
INHALT, nicht am Feldnamen), schlaegt eine Zuordnung vor und legt sie
zur Auswahl vor -- mit Anzahl, Zeitraum, Beispielzeile und dem, was
dabei herauskaeme. Bestaetigt wird von Hand. Damit ist der Leser
unabhaengig davon, wie die Felder heissen.

DIE EINHEIT IST DIE FALLE. "83" kann Sekunden, Minuten oder Stunden
sein. Geraten wird NICHT aus der Zahl, sondern aus dem Schluesselnamen
-- und wo der schweigt, aus der Form ("01:23:45" ist eindeutig).
Schweigen beide, kommt null zurueck. Eine Dauer, die um Faktor 60
danebenliegt, sieht richtig aus und ist es nicht.

MEHRERE LIVES AN EINEM TAG SIND EIN TAG: Dauer und Diamanten addiert,
bei den Zuschauern gewinnt die hoehere Spitze -- ein Durchschnitt aus
zwei Streams waere eine Zahl, die es nie gegeben hat.

DIE DATEI UEBERSCHREIBT NICHTS. Anders als der Backstage-Import, der
die Wahrheit der Agentur bringt, ist diese Datei die ZWEITE Quelle. Wo
schon eine Zahl steht -- von Hand oder aus Backstage --, bleibt sie
stehen (COALESCE statt REPLACE). Sonst wuerde ein Dateiupload
stillschweigend die offiziellen Zahlen ersetzen, und niemand wuesste
hinterher, welche gilt.

UND SIE WIRD NICHT GESPEICHERT. Gelesen, ausgewertet, verworfen. In
derselben Datei stehen Direktnachrichten, Such- und Ansehverlauf; die
haben auf diesem Server nichts zu suchen. "Income and Wallet" bleibt
ebenfalls liegen: Diamanten sind Leistung, Auszahlungen sind Gehalt.

GEPRUEFT OHNE ECHTE DATEI -- MIT DREI ERFUNDENEN (47 Pruefungen):
  A  englisch, flach, Dauer in Sekunden
  B  deutsch, verschachtelt, Dauer als "01:23:45"
  C  Sekundenstempel, Dauer in Minuten, andere Namen
  +  eine Datei ganz ohne Verlauf -> muss abgelehnt werden

Eine Pruefung gegen EINE ausgedachte Form wuerde nur beweisen, dass
mein Leser meine eigene Erfindung liest. Drei verschiedene zeigen, dass
er das Prinzip kann und nicht eine Form.

EIN EIGENER FEHLER, von der Pruefung gefunden: Der Nachrichtenverlauf
in Form A hatte nur EINEN Eintrag und fiel damit unter die
Mindestgroesse von zwei. Die Pruefung "der LIVE-Verlauf steht oben"
hatte danach nur einen Kandidaten und bewies gar nichts -- eine
Rangfolge laesst sich nur an mindestens zwei Dingen zeigen. Jetzt sind
es drei Nachrichten, und die Rangfolge ist wirklich gemessen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-11 12:01:49 +02:00
co-authored by Claude Opus 5
parent 017375d333
commit 027c372afd
29 changed files with 1166 additions and 285 deletions
+32
View File
@@ -316,3 +316,35 @@
.import-vorschau__zeile[data-art="still"] {
color: var(--text-still); font-size: .8rem;
}
/* ---- Die Auswahl in der TikTok-Datei (11.09.2026) -------------------
Das Format der Datei ist nicht dokumentiert. Der Server schlägt vor,
der Mensch entscheidet -- und eine Entscheidung gehört als sichtbare
Auswahl auf die Seite, nicht in ein Aufklappfeld, in dem man die
erste Zeile für gesetzt hält. */
.tt-wahl {
display: flex; align-items: flex-start; gap: 10px;
margin: 6px 0;
padding: 9px 11px;
border: 1px solid var(--rand); border-radius: 11px;
background: rgba(255, 255, 255, .02);
cursor: pointer;
}
.tt-wahl:hover { border-color: var(--rand-hell); }
.tt-wahl:has(input:checked) {
border-color: color-mix(in srgb, var(--akzent) 55%, transparent);
background: color-mix(in srgb, var(--akzent) 10%, transparent);
}
.tt-wahl input { margin-top: 3px; flex: none; }
.tt-wahl__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.tt-wahl__text strong {
font-family: ui-monospace, "Cascadia Code", "Consolas", monospace;
font-size: .82rem; color: var(--text);
overflow-wrap: anywhere;
}
.tt-wahl__zahl { font-size: .8rem; color: var(--text-leise); }
.tt-wahl__felder {
font-size: .76rem; color: var(--text-still);
font-family: ui-monospace, "Cascadia Code", "Consolas", monospace;
overflow-wrap: anywhere;
}
+114
View File
@@ -181,6 +181,9 @@
darum, niemandem einen Knopf hinzustellen, der eine Absage
bringt. */
$('netz-oeffnen').hidden = !LEITUNG.has(ich?.rolle);
/* Die Datei gehoert EINEM Creator -- deshalb sichtbar fuer alle,
die fuer ihn eintragen duerfen, also auch fuer seinen Scout. */
$('tt-oeffnen').hidden = !darfEintragen();
if (!z || (!z.diamanten && !z.tage && !z.verweildauer)) {
ziel.append(el('p', 'leistung__leer',
@@ -548,6 +551,109 @@
$('tag-form').addEventListener('submit', tagSpeichern);
$('tag-zu').addEventListener('click', () => $('tag-dialog').close());
$('tag-loeschen').addEventListener('click', tagLoeschen);
/* =====================================================================
DIE TIKTOK-DATEI DES CREATORS
Das Format ist nicht dokumentiert. Deshalb wird hier nichts
geraten: Der Server durchsucht die Datei nach Listen mit Datum und
schickt sie zur Auswahl zurück -- mit Anzahl, Zeitraum und dem,
was dabei herauskäme. Bestätigt wird von Hand.
===================================================================== */
let ttInhalt = null; /* die gelesene Datei -- bleibt im Browser */
let ttWahl = null; /* der bestätigte Kandidat */
async function ttDateiGewaehlt(ereignis) {
const datei = ereignis.target.files?.[0];
$('tt-fehler').textContent = '';
$('tt-vorschau').hidden = true;
$('tt-los').disabled = true;
ttInhalt = null; ttWahl = null;
if (!datei) return;
/* Ein Vollarchiv kann zweistellige Megabyte haben. Das Lesen
passiert im Browser; auf den Server geht es erst danach -- und
nur einmal, nicht bei jedem Klick. */
$('tt-fehler').textContent = 'Datei wird gelesen …';
let text;
try { text = await datei.text(); } catch { $('tt-fehler').textContent = 'Die Datei ließ sich nicht lesen.'; return; }
try { ttInhalt = JSON.parse(text); } catch {
$('tt-fehler').textContent = 'Das ist keine JSON-Datei. Beim Herunterladen bei TikTok '
+ 'muss als Format JSON gewählt werden, nicht TXT.';
return;
}
$('tt-fehler').textContent = 'Datei wird durchsucht …';
const a = await hole(`/workspace/api/leistung/${offenerCreator}/tiktok-datei/vorschau`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ inhalt: ttInhalt }),
}).catch(() => null);
if (!a) { $('tt-fehler').textContent = 'Die Datei konnte nicht geprüft werden.'; return; }
const d = await a.json().catch(() => ({}));
if (!a.ok) { $('tt-fehler').textContent = d.fehler || 'Die Datei wurde nicht verstanden.'; return; }
$('tt-fehler').textContent = '';
const v = $('tt-vorschau');
v.textContent = ''; v.hidden = false;
const zeile = (text2, art) => {
const p = el('p', 'import-vorschau__zeile');
if (art) p.dataset.art = art;
p.textContent = text2;
v.append(p);
return p;
};
zeile(`${d.kandidaten.length} Liste(n) mit Datum gefunden. Welche ist der LIVE-Verlauf?`);
/* Die Auswahl als echte Radioknöpfe: Es ist eine Entscheidung, und
eine Entscheidung gehört nicht in ein Aufklappfeld, in dem man
die erste Zeile für gesetzt hält. */
d.kandidaten.forEach((k, i) => {
const box = el('label', 'tt-wahl');
const r = document.createElement('input');
r.type = 'radio'; r.name = 'tt-kandidat'; r.value = String(i);
if (i === 0) { r.checked = true; ttWahl = k; }
r.addEventListener('change', () => { ttWahl = k; $('tt-los').disabled = false; });
const t = el('span', 'tt-wahl__text');
t.append(el('strong', null, k.pfad || '(oberste Ebene)'));
t.append(el('span', 'tt-wahl__zahl',
` ${k.anzahl} Einträge · ${k.tage_gesamt} Tage`
+ (k.von ? ` · ${k.von} bis ${k.bis}` : '')));
const felder = ['tag', 'dauer', 'diamanten', 'zuschauer']
.filter((f) => k.vorschlag[f])
.map((f) => `${f}: ${k.vorschlag[f]}`).join(' · ');
t.append(el('span', 'tt-wahl__felder', felder || 'nur ein Datum erkannt'));
box.append(r, t);
v.append(box);
});
if (ttWahl) $('tt-los').disabled = false;
}
async function ttUebernehmen(ereignis) {
ereignis.preventDefault();
if (!ttInhalt || !ttWahl) return;
$('tt-los').disabled = true;
const a = await hole(`/workspace/api/leistung/${offenerCreator}/tiktok-datei`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ inhalt: ttInhalt, pfad: ttWahl.pfad, zuordnung: ttWahl.vorschlag }),
}).catch(() => null);
if (!a) { $('tt-los').disabled = false; return; }
const d = await a.json().catch(() => ({}));
if (!a.ok) {
$('tt-fehler').textContent = d.fehler === 'keine_brauchbaren_tage'
? 'In dieser Liste stehen keine Tage mit Zahlen.'
: (d.fehler || 'Übernehmen ging nicht.');
$('tt-los').disabled = false;
return;
}
/* Die Datei loslassen, sobald sie nicht mehr gebraucht wird. */
ttInhalt = null; ttWahl = null;
$('tt-dialog').close();
melde(`${d.geschrieben} Tage neu, ${d.ergaenzt} ergänzt`);
setTimeout(() => melde(''), 6000);
await laden();
}
/* =====================================================================
DIE BACKSTAGE-TABELLE — Vorschau und Übernahme
===================================================================== */
@@ -660,6 +766,14 @@
$('netz-text').focus();
});
$('netz-zu').addEventListener('click', () => $('netz-dialog').close());
$('tt-oeffnen').addEventListener('click', () => {
$('tt-vorschau').hidden = true; $('tt-los').disabled = true;
$('tt-fehler').textContent = ''; $('tt-datei').value = '';
$('tt-dialog').showModal();
});
$('tt-zu').addEventListener('click', () => $('tt-dialog').close());
$('tt-datei').addEventListener('change', ttDateiGewaehlt);
$('tt-form').addEventListener('submit', ttUebernehmen);
/* Beim EINFÜGEN sofort nachsehen -- nicht erst auf einen Knopf warten.
Wer eine Tabelle einfügt, will wissen, ob sie erkannt wurde; ein
zweiter Klick dazwischen ist nur eine Gelegenheit, es nicht zu tun. */