# Datenmodell SQLite, Datei `data/db/abdulive.db`. WAL-Modus, damit mehrere Prozesse gleichzeitig lesen können, während der Ingest schreibt. Migrationen als nummerierte SQL-Dateien in `packages/db/migrations/`. ## Kerntabellen ### `events` — der Rohstrom Append-only. Alles landet hier zuerst. Alle Auswertungen sind ableitbar. ```sql CREATE TABLE events ( id TEXT PRIMARY KEY, -- ULID ts TEXT NOT NULL, -- ISO-8601 UTC type TEXT NOT NULL, stream_id TEXT, -- FK streams.id, NULL wenn offline user_id TEXT, -- Twitch user id payload TEXT NOT NULL -- JSON ); CREATE INDEX idx_events_ts ON events(ts); CREATE INDEX idx_events_type ON events(type, ts); CREATE INDEX idx_events_strm ON events(stream_id, ts); ``` ### `streams` — eine Zeile pro Sendung ```sql CREATE TABLE streams ( id TEXT PRIMARY KEY, -- Twitch stream id started_at TEXT NOT NULL, ended_at TEXT, vod_id TEXT, peak_viewers INTEGER DEFAULT 0, avg_viewers REAL, unique_chatters INTEGER DEFAULT 0, new_followers INTEGER DEFAULT 0 ); ``` ### `stream_segments` — Titel/Kategorie über die Zeit Basis für "welcher Titel hat was gebracht" und für VOD-Kapitel. ```sql CREATE TABLE stream_segments ( id INTEGER PRIMARY KEY, stream_id TEXT NOT NULL REFERENCES streams(id), started_at TEXT NOT NULL, ended_at TEXT, title TEXT, category_id TEXT, category TEXT, obs_scene TEXT ); ``` ### `viewer_samples` — Viewerzahl im Zeitverlauf Alle 60 s ein Sample aus `GET /helix/streams`. ```sql CREATE TABLE viewer_samples ( stream_id TEXT NOT NULL REFERENCES streams(id), ts TEXT NOT NULL, viewers INTEGER NOT NULL, PRIMARY KEY (stream_id, ts) ); ``` ### `chat_messages` — jede Nachricht ```sql CREATE TABLE chat_messages ( id TEXT PRIMARY KEY, -- msg-id aus IRC stream_id TEXT REFERENCES streams(id), ts TEXT NOT NULL, user_id TEXT NOT NULL REFERENCES users(id), text TEXT NOT NULL, emotes TEXT, -- JSON: [{id,name,count}] is_first INTEGER DEFAULT 0, -- first-msg Tag is_command INTEGER DEFAULT 0, bits INTEGER DEFAULT 0 ); CREATE INDEX idx_chat_stream_ts ON chat_messages(stream_id, ts); CREATE INDEX idx_chat_user ON chat_messages(user_id, ts); ``` ### `users` — das Community-Gedächtnis ```sql CREATE TABLE users ( id TEXT PRIMARY KEY, -- Twitch user id (stabil, nicht der Login!) login TEXT NOT NULL, display_name TEXT NOT NULL, first_seen_at TEXT NOT NULL, last_seen_at TEXT NOT NULL, streams_present INTEGER DEFAULT 0, -- an wie vielen Streams anwesend streams_chatted INTEGER DEFAULT 0, -- an wie vielen geschrieben message_count INTEGER DEFAULT 0, is_follower INTEGER DEFAULT 0, followed_at TEXT, is_sub INTEGER DEFAULT 0, sub_tier TEXT, points INTEGER DEFAULT 0, watch_minutes INTEGER DEFAULT 0, greeting TEXT, -- individuelle Begrüßung notes TEXT, -- freie Notizen des Streamers pronouns TEXT, muted_greeting INTEGER DEFAULT 0 ); ``` > `login` ändert sich bei Namensänderung, `id` nie. Überall wird über `id` > verknüpft, `login`/`display_name` bei jedem Kontakt aktualisiert. ### `user_streams` — Anwesenheit pro Stream Grundlage für "Nummer 15 für dich" und für neue-vs.-wiederkehrende Chatter. ```sql CREATE TABLE user_streams ( user_id TEXT NOT NULL REFERENCES users(id), stream_id TEXT NOT NULL REFERENCES streams(id), first_ts TEXT NOT NULL, last_ts TEXT NOT NULL, messages INTEGER DEFAULT 0, minutes INTEGER DEFAULT 0, PRIMARY KEY (user_id, stream_id) ); ``` ## Tool-Tabellen ### `clips` ```sql CREATE TABLE clips ( id TEXT PRIMARY KEY, -- interne ULID twitch_clip_id TEXT, stream_id TEXT REFERENCES streams(id), created_at TEXT NOT NULL, trigger TEXT NOT NULL, -- 'spike'|'command'|'manual'|'hotkey' trigger_score REAL, source TEXT NOT NULL, -- 'twitch'|'obs-replay' raw_path TEXT, vertical_path TEXT, subtitle_path TEXT, duration_s REAL, title TEXT, transcript TEXT, status TEXT NOT NULL, -- 'pending'|'rendered'|'approved'|'uploaded'|'rejected' score REAL, -- Ranking für Upload-Kandidaten uploaded_to TEXT -- JSON: {"tiktok": "...", "yt": "..."} ); ``` ### `raids` ```sql CREATE TABLE raids ( id INTEGER PRIMARY KEY, direction TEXT NOT NULL, -- 'in'|'out' ts TEXT NOT NULL, channel_id TEXT NOT NULL, channel_login TEXT NOT NULL, viewers INTEGER, category TEXT, reciprocated INTEGER DEFAULT 0, -- hat der zurückgeraidet? notes TEXT ); ``` ### `raid_candidates` — Scout-Ergebnisse ```sql CREATE TABLE raid_candidates ( id INTEGER PRIMARY KEY, scanned_at TEXT NOT NULL, channel_id TEXT NOT NULL, channel_login TEXT NOT NULL, viewers INTEGER NOT NULL, category TEXT, language TEXT, title TEXT, uptime_min INTEGER, score REAL, history_raids INTEGER DEFAULT 0 -- schon mal Kontakt gehabt? ); ``` ### `shop_items` / `redemptions` ```sql CREATE TABLE shop_items ( id TEXT PRIMARY KEY, label TEXT NOT NULL, cost INTEGER NOT NULL, action TEXT NOT NULL, -- 'set_title'|'obs_hotkey'|'sound'|'emote_rain'|'challenge' params TEXT, -- JSON cooldown_s INTEGER DEFAULT 0, per_stream_limit INTEGER, enabled INTEGER DEFAULT 1, requires_approval INTEGER DEFAULT 0 ); CREATE TABLE redemptions ( id TEXT PRIMARY KEY, ts TEXT NOT NULL, stream_id TEXT REFERENCES streams(id), user_id TEXT NOT NULL REFERENCES users(id), item_id TEXT NOT NULL REFERENCES shop_items(id), cost INTEGER NOT NULL, status TEXT NOT NULL, -- 'pending'|'executed'|'refunded' result TEXT ); ``` ### `posts` — ausgehende Social-Posts ```sql CREATE TABLE posts ( id TEXT PRIMARY KEY, created_at TEXT NOT NULL, kind TEXT NOT NULL, -- 'golive'|'recap'|'clip' platform TEXT NOT NULL, -- 'discord'|'bluesky'|'instagram' status TEXT NOT NULL, -- 'draft'|'sent'|'failed' body TEXT, media_path TEXT, remote_url TEXT, error TEXT ); ``` ## Event-Typen ``` stream.online stream.offline stream.segment_changed chat.message chat.first_message chat.cleared chat.spike ← vom Ingest berechnet user.arrived user.returning follow subscribe subgift cheer raid.in raid.out points.redeemed shop.redeemed poll.begin poll.end obs.scene_changed obs.replay_saved clip.requested clip.created clip.rendered ``` ## Aufbewahrung Nichts wird gelöscht. Die DB wächst bei ~10 Zuschauern um wenige MB pro Monat. Nächtliches Backup nach `data/db/backup/` per `scripts/backup.ps1` (SQLite `VACUUM INTO`, 30 Generationen).