@pixl-app/sdk 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1
4
+
5
+ Das Script nimmt die eigene Adresse nur noch als Server, wenn es von einer pixl-Instanz kommt (`/sdk/pixl.js`). Von jsDelivr, unpkg oder einem lokalen Dev-Server geladen, verbindet es sich mit pixl.lennox.zip statt mit dem CDN. `baseUrl` geht weiter vor, an `https://pixl.lennox.zip/sdk/pixl.js` ändert sich nichts.
6
+
7
+ ## 0.1.0
8
+
9
+ Erste Version im eigenen Repo: Live-Events per Stream, `feed()` mit Verlauf und Live-Events ohne Doppelte, Chat mit Emotes, Spotify, StreamElements, TTS, persistente States und Einstellungen für eigene Widgets.
10
+
11
+ Neu gegenüber dem `/sdk/pixl.js`, das pixl.lennox.zip gerade ausliefert: `pixl.nowPlaying()` und `Pixl.progress()`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lennox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # pixl SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Holt die Events eines Kanals aus [pixl](https://pixl.lennox.zip) in eigene Widgets: Follows, Subs, Cheers, Raids, Kanalpunkte, Chat mit 7TV-, BTTV- und FFZ-Emotes, Spotify, StreamElements-Spenden und TTS. Läuft in StreamElements Custom Widgets, OBS-Browserquellen und jedem aktuellen Browser.
4
+
5
+ Die ausführliche Doku steht unter [docs.pixl.lennox.zip/sdk](https://docs.pixl.lennox.zip/sdk).
6
+
7
+ ## Einbinden
8
+
9
+ Per Script-Tag, ohne Installation:
10
+
11
+ ```html
12
+ <script src="https://pixl.lennox.zip/sdk/pixl.js"></script>
13
+ <script>
14
+ const pixl = new Pixl({ key: "pixl_DEIN_SCHLÜSSEL" });
15
+ pixl.on("twitch.follow", (data) => console.log(`${data.username} folgt jetzt`));
16
+ </script>
17
+ ```
18
+
19
+ Mit fester Version über jsDelivr. Unter `pixl.lennox.zip/sdk/pixl.js` kommt immer die Version, die pixl gerade ausliefert:
20
+
21
+ ```html
22
+ <script src="https://cdn.jsdelivr.net/npm/@pixl-app/sdk@0.1.1/dist/pixl.js"></script>
23
+ ```
24
+
25
+ Oder als Paket mit Bundler und TypeScript:
26
+
27
+ ```bash
28
+ bun add @pixl-app/sdk # oder: npm i @pixl-app/sdk
29
+ ```
30
+
31
+ ```ts
32
+ import { Pixl } from "@pixl-app/sdk";
33
+
34
+ const pixl = new Pixl({ key: "pixl_DEIN_SCHLÜSSEL" });
35
+ pixl.on("twitch.follow", (data) => console.log(`${data.username} folgt jetzt`));
36
+ ```
37
+
38
+ Den API-Schlüssel legst du in pixl unter „API & SDK“ an. Welche Felder jedes Event mitbringt, steht in der Doku unter [Events](https://docs.pixl.lennox.zip/sdk/events).
39
+
40
+ ## Entwickeln
41
+
42
+ ```bash
43
+ bun install
44
+ bun run dev # Dev-SDK unter http://localhost:4400/pixl.js, baut bei jeder Änderung neu
45
+ bun run check # Typen
46
+ bun run lint # Formatierung
47
+ bun test # Tests
48
+ bun run build # dist/pixl.js (Script), dazu dist/index.js und .d.ts
49
+ ```
50
+
51
+ Ein Overlay mit dem Dev-SDK testen: `<script src="http://localhost:4400/pixl.js"></script>` einbinden. Es spricht dann mit pixl.lennox.zip, gegen einen lokalen pixl-Server zusätzlich `baseUrl` setzen. In einer OBS-Browserquelle oder einer lokalen HTML-Datei klappt das direkt, eine https-Seite wie StreamElements sperrt `http://localhost` unter Umständen.
52
+
53
+ Die Typen der pixl-API liegen als Kopie aus dem pixl-Monorepo in `src/api-types/`, bis es dafür ein npm-Paket gibt. Geändert werden sie dort, nicht hier.
54
+
55
+ Gegen eine lokale pixl-Instanz: `new Pixl({ key, baseUrl: "http://localhost:5173" })`.
56
+
57
+ ## Versionen und Releases
58
+
59
+ Semver. Bricht eine Änderung bestehende Widgets, steigt die Hauptversion. Was sich geändert hat, steht im [CHANGELOG](./CHANGELOG.md).
60
+
61
+ Jede Version landet auf [npm](https://www.npmjs.com/package/@pixl-app/sdk) und als [GitHub-Release](https://github.com/pixl-app/sdk/releases) mit `pixl.js` (Script für `<script src>`) und `pixl-app-sdk-<version>.tgz` (Paket mit ESM und `.d.ts`). So entsteht eine:
62
+
63
+ 1. Version in `package.json` anheben, in `CHANGELOG.md` einen Abschnitt `## 0.2.0` anlegen. Daraus werden die Release-Notizen.
64
+ 2. Tag `v0.2.0` pushen. Die CI prüft Version, Typen und Formatierung, baut, veröffentlicht auf npm (Trusted Publishing, ohne gespeicherten Token) und hängt beide Dateien an das Release.
65
+
66
+ ## Fehler melden
67
+
68
+ Unter [Issues](https://github.com/pixl-app/sdk/issues), am besten mit SDK-Version, Browser oder OBS-Version und dem, was in der Konsole steht.
69
+
70
+ ## Lizenz
71
+
72
+ MIT
@@ -0,0 +1,4 @@
1
+ export declare const API_KEY_SCOPES: readonly ["events:read", "states:read", "streamelements:read", "tts:read", "tts:write", "subs:read", "spotify:read"];
2
+ export type ApiKeyScope = (typeof API_KEY_SCOPES)[number];
3
+ export declare function isApiKeyScope(value: unknown): value is ApiKeyScope;
4
+ export declare function apiScopeForEvent(event: string): ApiKeyScope | null;
@@ -0,0 +1,60 @@
1
+ export type ChatEmoteProvider = "twitch" | "7tv" | "bttv" | "ffz";
2
+ export type ChatEmote = {
3
+ provider: ChatEmoteProvider;
4
+ id: string;
5
+ name: string;
6
+ animated?: boolean;
7
+ /** Legt sich über das vorige Emote (z.B. 7TV-Hüte) */
8
+ zeroWidth?: boolean;
9
+ };
10
+ /** Badge vor dem Namen: von Twitch (Mod, Sub, ...) oder von 7TV */
11
+ export type ChatBadge = {
12
+ provider: "twitch" | "7tv";
13
+ /** Twitch: Badge-Set, z.B. `subscriber`, `moderator` */
14
+ set?: string;
15
+ /** Twitch: Version im Set (bei Subs die Monatsstufe), 7TV: Badge-ID */
16
+ id: string;
17
+ title: string;
18
+ /** Bild in doppelter Größe (36 px bei Twitch) */
19
+ url: string;
20
+ /** Twitch: Zusatz, bei Subs z.B. die genauen Monate */
21
+ info?: string;
22
+ };
23
+ export type SevenTvPaintStop = {
24
+ at: number;
25
+ color: string;
26
+ };
27
+ export type SevenTvPaintLayer = {
28
+ type: "linear-gradient";
29
+ angle: number;
30
+ repeating: boolean;
31
+ stops: SevenTvPaintStop[];
32
+ opacity: number;
33
+ } | {
34
+ type: "radial-gradient";
35
+ shape: "circle" | "ellipse";
36
+ repeating: boolean;
37
+ stops: SevenTvPaintStop[];
38
+ opacity: number;
39
+ } | {
40
+ type: "color";
41
+ color: string;
42
+ opacity: number;
43
+ } | {
44
+ type: "image";
45
+ url: string;
46
+ animated: boolean;
47
+ opacity: number;
48
+ };
49
+ export type SevenTvPaint = {
50
+ id: string;
51
+ name: string;
52
+ layers: SevenTvPaintLayer[];
53
+ /** Schatten, Versatz und Unschärfe in px */
54
+ shadows: Array<{
55
+ color: string;
56
+ x: number;
57
+ y: number;
58
+ blur: number;
59
+ }>;
60
+ };
@@ -0,0 +1,2 @@
1
+ export declare const DEVICE_APP_SCOPES: readonly ["spotify"];
2
+ export type DeviceAppScope = (typeof DEVICE_APP_SCOPES)[number];
@@ -0,0 +1,17 @@
1
+ export type TwitchEventType = "stream.online" | "stream.offline" | "twitch.follow" | "twitch.sub" | "twitch.sub_gift" | "twitch.cheer" | "twitch.raid" | "twitch.channel_point" | "twitch.poll_started" | "twitch.poll_progress" | "twitch.poll_ended" | "twitch.prediction_started" | "twitch.prediction_progress" | "twitch.prediction_locked" | "twitch.prediction_ended" | "twitch.channel_point_resolved" | "twitch.power_up" | "twitch.channel_updated" | "twitch.title_changed" | "twitch.category_changed" | "twitch.ban" | "twitch.unban" | "twitch.vip_added" | "twitch.vip_removed" | "twitch.moderator_added" | "twitch.moderator_removed" | "twitch.hype_train_started" | "twitch.hype_train_progress" | "twitch.hype_train_ended" | "twitch.goal_started" | "twitch.goal_progress" | "twitch.goal_ended" | "twitch.charity_started" | "twitch.charity_progress" | "twitch.charity_donated" | "twitch.charity_stopped" | "twitch.ad_break" | "twitch.watch_streak" | "twitch.shoutout_sent" | "twitch.modiversary" | "twitch.chat_message" | "twitch.chat_cleared" | "twitch.chat_user_cleared" | "twitch.chat_message_deleted" | "twitch.chat_settings_updated" | "twitch.chat_message_held" | "twitch.chat_message_held_updated" | "twitch.shared_chat_started" | "twitch.shared_chat_updated" | "twitch.shared_chat_ended" | "twitch.guest_star_session_started" | "twitch.guest_star_session_ended" | "twitch.guest_star_guest_updated" | "twitch.guest_star_settings_updated";
2
+ export type StreamElementsEventType = "streamelements.tip";
3
+ export type EventlogEventType = TwitchEventType | StreamElementsEventType;
4
+ /** Was ein Client über den Live-Stream bekommen kann (je nach Scopes des Schlüssels) */
5
+ export type OrgEventType = TwitchEventType | StreamElementsEventType | "state.updated" | "state.snapshot" | "api_key.revoked" | "tts.ready" | "tts.skip" | "tts.clear" | "subs.updated" | "spotify.now_playing";
6
+ /**
7
+ * Ein Event des Kanals. `E` erweitert der Server um Events, die nie bei Clients ankommen
8
+ * (Dashboard, interne Signale, siehe OrgEvent in @pixl/types).
9
+ */
10
+ export type OrgEvent<E extends string = OrgEventType> = {
11
+ orgId: string;
12
+ event: E;
13
+ data: Record<string, unknown>;
14
+ id?: string;
15
+ at?: string;
16
+ replay?: boolean;
17
+ };
@@ -0,0 +1,8 @@
1
+ export * from "./events.js";
2
+ export * from "./chat.js";
3
+ export * from "./spotify.js";
4
+ export * from "./api-scopes.js";
5
+ export * from "./streamelements.js";
6
+ export * from "./tts.js";
7
+ export * from "./modules.js";
8
+ export * from "./device-apps.js";
@@ -0,0 +1,4 @@
1
+ export declare const CHANNEL_MODULES: readonly ["music", "tts"];
2
+ export type ChannelModule = (typeof CHANNEL_MODULES)[number];
3
+ /** Fehlercode in API-Antworten, wenn das Modul aus ist */
4
+ export declare const MODULE_DISABLED_CODE = "module_disabled";
@@ -0,0 +1,108 @@
1
+ export type SpotifyTrack = {
2
+ uri: string;
3
+ name: string;
4
+ artists: string[];
5
+ album: string;
6
+ /** Kleinstes Cover mit mindestens 64px, sonst das kleinste vorhandene */
7
+ image: string | null;
8
+ durationMs: number;
9
+ explicit: boolean;
10
+ /** Link zum Track bei Spotify */
11
+ url: string | null;
12
+ };
13
+ export type SpotifyPlayback = {
14
+ isPlaying: boolean;
15
+ progressMs: number;
16
+ track: SpotifyTrack | null;
17
+ device: {
18
+ name: string;
19
+ type: string;
20
+ volume: number | null;
21
+ supportsVolume: boolean;
22
+ } | null;
23
+ /** Was Spotify gerade nicht zulässt (z.B. während Werbung, oder Pause, wenn schon pausiert) */
24
+ disallows: {
25
+ pause: boolean;
26
+ resume: boolean;
27
+ next: boolean;
28
+ previous: boolean;
29
+ };
30
+ };
31
+ /** Eintrag der Warteschlange, ergänzt um den pixl-Nutzer, der ihn hinzugefügt hat */
32
+ export type SpotifyQueueItem = SpotifyTrack & {
33
+ addedBy: {
34
+ name: string;
35
+ image: string | null;
36
+ } | null;
37
+ };
38
+ /** Warum Spotify gerade nicht kann. Der Server liefert den Grund, die UI den Text. */
39
+ export type SpotifyProblem = "not_connected" | "reconnect" | "no_device" | "premium" | "restricted" | "offline" | "rate_limited" | "unavailable";
40
+ export type SpotifyState = {
41
+ problem: SpotifyProblem | null;
42
+ playback: SpotifyPlayback | null;
43
+ /** Wer den laufenden Song über pixl hinzugefügt hat */
44
+ currentAddedBy: SpotifyQueueItem["addedBy"];
45
+ queue: SpotifyQueueItem[];
46
+ /** Ob der Twitch-Kanal live ist. Offline dürfen nur Owner Spotify sehen und steuern, außer allowOffline ist an. */
47
+ channelLive: boolean;
48
+ /** Owner-Einstellung: Mitglieder dürfen auch offline steuern (Standard aus) */
49
+ allowOffline: boolean;
50
+ };
51
+ /**
52
+ * Was gerade läuft, für Widgets (/api/v1/spotify/now-playing, Event spotify.now_playing).
53
+ * Bewusst schlank: kein Gerät, keine Queue. progressMs gilt zum Zeitpunkt at, solange
54
+ * playing an ist, rechnet das Widget selbst weiter.
55
+ */
56
+ export type SpotifyNowPlaying = {
57
+ playing: boolean;
58
+ progressMs: number;
59
+ /** ISO-Zeit, zu der progressMs gemessen wurde */
60
+ at: string;
61
+ /** null = nichts geladen, kein Gerät aktiv */
62
+ track: SpotifyTrack | null;
63
+ /** Wer den Song über pixl in die Queue gelegt hat */
64
+ addedBy: {
65
+ name: string;
66
+ } | null;
67
+ };
68
+ /** Belohnungen, die pixl bei Twitch anlegt: Song wünschen, überspringen, letzten nochmal */
69
+ export declare const SPOTIFY_REWARD_KINDS: readonly ["request", "skip", "previous"];
70
+ export type SpotifyRewardKind = (typeof SPOTIFY_REWARD_KINDS)[number];
71
+ export declare function isSpotifyRewardKind(value: unknown): value is SpotifyRewardKind;
72
+ /** pending = in der Prüfliste, queued = an Spotify geschickt, played = lief bzw. ausgeführt, refunded = Punkte zurück */
73
+ export type SpotifyRequestStatus = "pending" | "queued" | "played" | "refunded";
74
+ /** Warum eine Einlösung erstattet wurde */
75
+ export type SpotifyRequestReason = "not_found" | "not_track" | "blocked" | "no_music" | "too_long" | "explicit" | "repeat" | "already_queued" | "manual" | "expired" | "stream_ended" | "error";
76
+ export type SpotifyRequest = {
77
+ id: string;
78
+ kind: SpotifyRewardKind;
79
+ /** Twitch-Name des Zuschauers */
80
+ viewer: string;
81
+ /** Was der Zuschauer eingegeben hat (nur bei Wünschen) */
82
+ input: string;
83
+ /** Gewünschter bzw. übersprungener Song */
84
+ track: SpotifyTrack | null;
85
+ status: SpotifyRequestStatus;
86
+ /** Warum erstattet, null = ohne Grund (in Twitch storniert, Belohnung entfernt) */
87
+ reason: SpotifyRequestReason | null;
88
+ createdAt: string;
89
+ /** Alles Gespeicherte für den Detail-Dialog (Fehlersuche, z.B. falscher Song erkannt) */
90
+ redemptionId: string;
91
+ rewardId: string;
92
+ viewerId: string;
93
+ viewerLogin: string;
94
+ trackUri: string | null;
95
+ queuedAt: string | null;
96
+ finishedAt: string | null;
97
+ };
98
+ /** Kurzer Stand der Musik-Belohnungen, z.B. für die Stream-Deck-Taste */
99
+ export type SpotifyRequestsSummary = {
100
+ /** Mindestens eine Belohnung ist an und bei Twitch angelegt */
101
+ active: boolean;
102
+ /** Von Hand pausiert bis, null = nicht. Weit in der Zukunft = bis zum Fortsetzen */
103
+ pausedUntil: string | null;
104
+ /** Pausiert, weil der Kanal offline ist */
105
+ pausedOffline: boolean;
106
+ /** Wünsche in der Warteschlange, die noch nicht gelaufen sind */
107
+ queued: number;
108
+ };
@@ -0,0 +1,3 @@
1
+ export declare const STREAMELEMENTS_PROXY_PATHS: readonly ["activities", "sessions", "tips/leaderboard", "points/top", "points/alltime", "store/items", "contests", "songrequest/queue", "songrequest/playing"];
2
+ export type StreamElementsProxyPath = (typeof STREAMELEMENTS_PROXY_PATHS)[number];
3
+ export declare function isStreamElementsProxyPath(value: unknown): value is StreamElementsProxyPath;
@@ -0,0 +1,27 @@
1
+ export declare const TTS_MAX_CHARS = 3000;
2
+ /** Warum nichts vorgelesen wird. Der Server liefert den Grund, die UI den Text. */
3
+ export type TtsProblem = "not_configured" | "disabled" | "quota" | "global_limit" | "blocked" | "empty" | "unknown_voice" | "unavailable";
4
+ /**
5
+ * Welche Filter ein eigener Aufruf (SDK `speak()`, `POST /api/v1/tts`) anwendet.
6
+ * Alles ist standardmäßig an. Unsichtbare Zeichen und die Längengrenze gelten immer.
7
+ */
8
+ export type TtsFilters = {
9
+ /** Links werden weggelassen */
10
+ links?: boolean;
11
+ /** Wiederholungen kürzen ("HYPEEEEEE", zehnmal dasselbe Wort) */
12
+ repeats?: boolean;
13
+ /** Gesperrte Wörter des Kanals: Text wird abgelehnt */
14
+ blockedWords?: boolean;
15
+ };
16
+ export declare const TTS_FILTER_KEYS: readonly ["links", "repeats", "blockedWords"];
17
+ /**
18
+ * Zeitmarke eines Wortes im Audio, in Millisekunden ab Start. Gemeinsames Format
19
+ * für alle Anbieter (Polly: Speech Marks, Fish: Timestamps). `end` fehlt, wenn der
20
+ * Anbieter nur Startzeiten kennt, dann gilt der Start des nächsten Wortes bzw. das
21
+ * Ende des Audios.
22
+ */
23
+ export type TtsMark = {
24
+ word: string;
25
+ start: number;
26
+ end: number | null;
27
+ };
package/dist/chat.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { ChatBadge, ChatEmote, SevenTvPaint } from "./api-types/index.js";
2
+ import type { Pixl } from "./client.js";
3
+ export type EmoteSize = 1 | 2 | 4;
4
+ /** Bild-URL eines Emotes. `size` 1, 2 oder 4 (Twitch und BTTV liefern bei 4 die größte Stufe). */
5
+ export declare function emoteUrl(emote: ChatEmote, size?: EmoteSize): string;
6
+ export declare function escapeHtml(text: string): string;
7
+ export type ChatHtmlOptions = {
8
+ /** Bildgröße der Emotes (Standard 2, wirkt scharf bei normaler Schriftgröße) */
9
+ size?: EmoteSize;
10
+ };
11
+ /**
12
+ * HTML einer Chat-Nachricht. Emotes als `<img class="pixl-emote">`, zero-width Emotes
13
+ * zusammen mit dem Emote davor in `<span class="pixl-emote-stack">`.
14
+ */
15
+ export declare function chatHtml(text: string, emotes?: Record<string, ChatEmote>, options?: ChatHtmlOptions): string;
16
+ /** Badges einer Nachricht (`data.badges`) als `<img class="pixl-badge">`, Titel als `alt` */
17
+ export declare function badgesHtml(badges?: ChatBadge[]): string;
18
+ /**
19
+ * Grund-CSS für Emotes und Badges in `chatHtml`. Einmal einbinden, z.B.
20
+ * `document.head.append(Object.assign(document.createElement("style"), { textContent: CHAT_CSS }))`.
21
+ */
22
+ export declare const CHAT_CSS = ".pixl-emote{height:1.75em;vertical-align:middle;margin:-.25em 0}\n.pixl-emote-stack{display:inline-grid;vertical-align:middle}\n.pixl-emote-stack>.pixl-emote{grid-area:1/1;justify-self:center;align-self:center;margin:0}\n.pixl-badge{height:1.1em;vertical-align:middle;margin-right:.25em}";
23
+ /**
24
+ * CSS für einen Namen mit 7TV-Paint, als Inline-Style. Setzt den Verlauf als
25
+ * Hintergrund und schneidet ihn auf den Text zu. `fallback` = Farbe, falls der
26
+ * Browser das nicht kann (z.B. die Twitch-Farbe des Users).
27
+ */
28
+ export declare function paintCss(paint: SevenTvPaint, fallback?: string): string;
29
+ /** Chat-Helfer an `pixl.chat` */
30
+ export declare class PixlChat {
31
+ private readonly pixl;
32
+ private paints;
33
+ private queue;
34
+ private timer;
35
+ constructor(pixl: Pixl);
36
+ /** Grund-CSS für Emotes im HTML (siehe `CHAT_CSS`) */
37
+ readonly css = ".pixl-emote{height:1.75em;vertical-align:middle;margin:-.25em 0}\n.pixl-emote-stack{display:inline-grid;vertical-align:middle}\n.pixl-emote-stack>.pixl-emote{grid-area:1/1;justify-self:center;align-self:center;margin:0}\n.pixl-badge{height:1.1em;vertical-align:middle;margin-right:.25em}";
38
+ /** Bild-URL eines Emotes */
39
+ emoteUrl(emote: ChatEmote, size?: EmoteSize): string;
40
+ /** Badges als HTML (siehe `badgesHtml`) */
41
+ badgesHtml(badges?: ChatBadge[]): string;
42
+ /** CSS für einen Namen mit 7TV-Paint (siehe `paintCss`) */
43
+ paintCss(paint: SevenTvPaint, fallback?: string): string;
44
+ /** HTML einer Nachricht (dasselbe wie `data.html` bei `twitch.chat_message`) */
45
+ html(text: string, emotes?: Record<string, ChatEmote>, options?: ChatHtmlOptions): string;
46
+ /** Alle Emotes des Kanals (7TV, BTTV, FFZ, Kanal und global), Gewinner bei gleichem Namen zuerst */
47
+ emotes(): Promise<ChatEmote[]>;
48
+ /** Definition eines 7TV-Paints (`data.paint` bei `twitch.chat_message`). Gecacht, mehrere Abfragen werden gebündelt. */
49
+ paint(id: string): Promise<SevenTvPaint | null>;
50
+ /** Inline-Style für einen Namen mit Paint, `fallback` wenn es keinen gibt */
51
+ paintStyle(id: string | null | undefined, fallback?: string): Promise<string>;
52
+ private flush;
53
+ }
@@ -0,0 +1,194 @@
1
+ import type { SpotifyNowPlaying, StreamElementsProxyPath } from "./api-types/index.js";
2
+ import { PixlChat } from "./chat.js";
3
+ import { EventFeed, type FeedOptions } from "./feed.js";
4
+ import { PixlTts } from "./tts.js";
5
+ export type PixlStatus = "connecting" | "connected" | "disconnected" | "unauthorized" | "closed";
6
+ export type PixlOptions = {
7
+ /** API-Schlüssel (`pixl_…`), erstellt unter Einstellungen → API-Schlüssel */
8
+ key: string;
9
+ /**
10
+ * Adresse der pixl-Instanz. Standard: der Server, von dem das SDK-Script
11
+ * geladen wurde, sonst https://pixl.lennox.zip
12
+ */
13
+ baseUrl?: string;
14
+ /** Stream automatisch verbinden (Standard: true). Sonst manuell per connect(). */
15
+ autoConnect?: boolean;
16
+ /**
17
+ * Abbruch und Neuverbinden der Live-Verbindung in der Browser-Konsole melden
18
+ * (Standard: true). Nur Wechsel, keine Events.
19
+ */
20
+ logs?: boolean;
21
+ };
22
+ export type PixlEvent = {
23
+ /** Event-Typ, z.B. `twitch.cheer` */
24
+ event: string;
25
+ /** Payload laut Event-Referenz */
26
+ data: Record<string, unknown>;
27
+ /** true bei erneut abgespielten Events aus dem Eventlog */
28
+ replay: boolean;
29
+ /** ID der Eventlog-Zeile (nur bei Events, die im Eventlog stehen) */
30
+ id?: string;
31
+ /** Zeitpunkt als ISO-String (nur bei Events, die im Eventlog stehen) */
32
+ at?: string;
33
+ /** true, wenn das Event aus der Historie nachgeladen wurde (nur bei feed()) */
34
+ history?: boolean;
35
+ };
36
+ export type EventHandler = (data: Record<string, unknown>, event: PixlEvent) => void;
37
+ export type StateHandler = (value: unknown, key: string) => void;
38
+ export type EventsQuery = {
39
+ /** Ab wann: Date, ISO-String, Unix-ms oder `"stream"` (Start des letzten Streams) */
40
+ since?: Date | string | number;
41
+ /** Bis wann (exklusiv) */
42
+ until?: Date | string | number;
43
+ /** Nur diese Event-Typen */
44
+ types?: string[];
45
+ /** Max. Anzahl pro Seite, 1 bis 200 (Standard 50) */
46
+ limit?: number;
47
+ /** `nextCursor` der vorherigen Seite */
48
+ cursor?: string;
49
+ };
50
+ export type EventsPage = {
51
+ events: {
52
+ id: string;
53
+ event: string;
54
+ data: Record<string, unknown>;
55
+ at: string;
56
+ }[];
57
+ nextCursor: string | null;
58
+ };
59
+ export type OrgInfo = {
60
+ id: string;
61
+ name: string;
62
+ logo: string | null;
63
+ twitchChannelLogin: string | null;
64
+ /** Erlaubnisse des verwendeten Schlüssels, z.B. `["events:read"]` */
65
+ scopes: string[];
66
+ };
67
+ export type SubCount = {
68
+ /** Aktive Subs laut Twitch, inklusive des Streamers selbst (zählt Twitch so) */
69
+ total: number;
70
+ /** Sub-Punkte laut Twitch: Stufe 1 = 1, Stufe 2 = 2, Stufe 3 = 6 */
71
+ points: number;
72
+ /** Wann pixl die Zahl bei Twitch abgefragt hat (ISO-String) */
73
+ at: string;
74
+ };
75
+ export declare class PixlApiError extends Error {
76
+ status: number;
77
+ /** Maschinenlesbarer Grund, falls der Server einen nennt (z.B. `quota` bei TTS) */
78
+ problem?: string;
79
+ constructor(status: number, message: string, problem?: string);
80
+ }
81
+ export declare function toQueryDate(value: Date | string | number): string;
82
+ export declare class Pixl {
83
+ /** Von global.ts gesetzt: Origin des geladenen SDK-Scripts */
84
+ static defaultBaseUrl: string | undefined;
85
+ readonly baseUrl: string;
86
+ private readonly key;
87
+ /** Text-to-Speech: vorlesen lassen und abspielen (siehe Doku "TTS") */
88
+ readonly tts: PixlTts;
89
+ /** Chat: Emotes als HTML, 7TV-Paints (siehe Doku "Chat") */
90
+ readonly chat: PixlChat;
91
+ private handlers;
92
+ private stateHandlers;
93
+ private statusHandlers;
94
+ private states;
95
+ private _status;
96
+ private abort;
97
+ private reconnectTimer;
98
+ private watchdog;
99
+ private attempt;
100
+ private stopped;
101
+ private readonly logs;
102
+ private everConnected;
103
+ private lostAt;
104
+ constructor(options: PixlOptions);
105
+ /**
106
+ * Handler für einen Event-Typ registrieren (`"twitch.cheer"`), oder für alle
107
+ * Events mit `"*"`. Gibt die Instanz zurück, Aufrufe lassen sich verketten.
108
+ */
109
+ on(event: string, handler: EventHandler): this;
110
+ /** Handler wieder entfernen */
111
+ off(event: string, handler: EventHandler): this;
112
+ /**
113
+ * State abonnieren. Der Handler läuft sofort mit dem aktuellen Wert (sobald
114
+ * bekannt) und danach bei jeder Änderung.
115
+ */
116
+ state(key: string, handler: StateHandler): this;
117
+ /** State-Handler wieder entfernen */
118
+ offState(key: string, handler: StateHandler): this;
119
+ /** Zuletzt bekannter Wert eines States (undefined, solange unbekannt) */
120
+ getState(key: string): unknown;
121
+ get status(): PixlStatus;
122
+ /** Statuswechsel beobachten. Gibt eine Funktion zum Abmelden zurück. */
123
+ onStatus(handler: (status: PixlStatus) => void): () => void;
124
+ private setStatus;
125
+ /** Rohzugriff auf einen /api/v1-Endpoint (Pfad ohne /api/v1, z.B. `/events`) */
126
+ request<T>(path: string, params?: Record<string, string | undefined>): Promise<T>;
127
+ /**
128
+ * Wie request(), aber mit beliebiger Methode und Body (JSON) bzw. roher
129
+ * Response. Für die TTS-Endpunkte, nicht Teil der öffentlichen Schnittstelle.
130
+ * @internal
131
+ */
132
+ _fetch(path: string, init?: {
133
+ method?: string;
134
+ json?: unknown;
135
+ }): Promise<Response>;
136
+ /** Eventlog abfragen (neueste zuerst, seitenweise) */
137
+ events(query?: EventsQuery): Promise<EventsPage>;
138
+ /** Infos zur Organisation, der der Schlüssel gehört, plus dessen Scopes */
139
+ org(): Promise<OrgInfo>;
140
+ /**
141
+ * Aktuelle Sub-Zahl direkt von Twitch, unabhängig vom Eventlog (höchstens 60 s alt).
142
+ * Braucht einen Schlüssel mit `subs:read`. Änderungen kommen live als `subs.updated`
143
+ * mit denselben Feldern.
144
+ */
145
+ subs(): Promise<SubCount>;
146
+ /**
147
+ * Was gerade auf Spotify läuft: `{ playing, progressMs, at, track, addedBy }`.
148
+ * Braucht einen Schlüssel mit `spotify:read` und das Modul Musik. Läuft nichts
149
+ * (kein Gerät aktiv), ist `track` null. Änderungen (anderer Song, Play/Pause,
150
+ * Sprung im Song) kommen live als `spotify.now_playing` mit denselben Feldern.
151
+ * Den laufenden Fortschritt liefert `Pixl.progress(nowPlaying)`.
152
+ */
153
+ nowPlaying(): Promise<SpotifyNowPlaying>;
154
+ /**
155
+ * Fortschritt in ms zum Zeitpunkt `now`, aus einer Messung von `nowPlaying()` bzw.
156
+ * `spotify.now_playing`. Läuft der Song, wird ab `at` weitergerechnet, höchstens bis
157
+ * zur Songlänge.
158
+ */
159
+ static progress(nowPlaying: SpotifyNowPlaying, now?: number): number;
160
+ /**
161
+ * Lesender Zugriff auf den verbundenen StreamElements-Kanal, pixl reicht die
162
+ * Anfrage mit dem hinterlegten Overlay Token weiter (der Token bleibt bei pixl).
163
+ * Braucht einen Schlüssel mit `streamelements:read`. `params` gehen 1:1 als
164
+ * Query-Parameter an StreamElements, die Antwort kommt unverändert zurück.
165
+ */
166
+ streamElements<T = unknown>(path: StreamElementsProxyPath, params?: Record<string, string | number | undefined>): Promise<T>;
167
+ /** Aktuelle Werte aller States */
168
+ fetchStates(): Promise<Record<string, unknown>>;
169
+ /**
170
+ * Alle Events seit `since` holen, über alle Seiten hinweg, älteste zuerst.
171
+ * Für große Zeiträume entsprechend viele Requests (200 Events pro Seite).
172
+ */
173
+ fetchAllEvents(query?: Omit<EventsQuery, "cursor" | "limit">): Promise<EventsPage["events"]>;
174
+ /**
175
+ * Historie + live in einem: `handler` bekommt erst alle Events seit `since`
176
+ * (älteste zuerst, `event.history === true`), danach jedes neue live. Jedes
177
+ * Event kommt genau einmal an, auch über Verbindungsabbrüche hinweg.
178
+ * Rechnet selbst nichts, Summen o.ä. baut der Aufrufer im Handler.
179
+ */
180
+ feed(options: FeedOptions, handler: EventHandler): EventFeed;
181
+ /** Live-Stream verbinden (passiert standardmäßig automatisch) */
182
+ connect(): void;
183
+ /** Stream trennen und nicht neu verbinden */
184
+ disconnect(): void;
185
+ private run;
186
+ private log;
187
+ private scheduleReconnect;
188
+ private armWatchdog;
189
+ private clearWatchdog;
190
+ private readStream;
191
+ private dispatch;
192
+ private emit;
193
+ private setState;
194
+ }
package/dist/feed.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { EventHandler, Pixl } from "./client.js";
2
+ export type FeedOptions = {
3
+ /**
4
+ * Ab wann die Historie geladen wird: Date, ISO-String, Unix-ms oder `"stream"`
5
+ * (Start des letzten Streams). Pflicht, damit nicht versehentlich das
6
+ * komplette Eventlog geladen wird.
7
+ */
8
+ since: Date | string | number;
9
+ /** Nur diese Event-Typen (Historie und live) */
10
+ types?: string[];
11
+ };
12
+ /**
13
+ * Historie + live, jedes Event genau einmal. Ablauf bei jedem (Re-)Connect:
14
+ * 1. Live-Events ab jetzt puffern (der Stream steht bereits)
15
+ * 2. Historie seit `since` laden und ausliefern, was noch nicht ausgeliefert ist
16
+ * 3. Puffer abarbeiten, Doppelte (gleiche ID) überspringen, danach direkt live
17
+ * Weil nach jedem Reconnect die Historie erneut abgeglichen wird, gehen auch
18
+ * Events aus einer Verbindungslücke nicht verloren. Nach dem ersten Laden reichen
19
+ * dafür die neuesten Seiten: geblättert wird nur bis kurz vor das neueste schon
20
+ * ausgelieferte Event. Sonst lüde jedes Overlay nach einem Deploy die komplette
21
+ * Historie seit `since` neu und liefe dabei ins Rate-Limit.
22
+ * Schlägt das Laden fehl, wird es mit Backoff wiederholt, solange der Stream steht.
23
+ *
24
+ * Bewusst ohne jede Auswertung: was der Handler mit den Events macht (Summen,
25
+ * Listen, Alerts), entscheidet der Aufrufer.
26
+ */
27
+ export declare class EventFeed {
28
+ /** true, sobald die Historie einmal komplett ausgeliefert wurde */
29
+ ready: boolean;
30
+ /** Letzter Fehler beim Laden der Historie */
31
+ error: Error | null;
32
+ private readonly pixl;
33
+ private readonly since;
34
+ private readonly types;
35
+ private readonly handler;
36
+ private readonly delivered;
37
+ private readonly errorListeners;
38
+ private buffer;
39
+ private loadSeq;
40
+ private stopped;
41
+ private latestAt;
42
+ private retryTimer;
43
+ private retryAttempt;
44
+ private readonly unsubscribeStatus;
45
+ private readonly onEvent;
46
+ constructor(pixl: Pixl, options: FeedOptions, handler: EventHandler);
47
+ /** Fehler beim Laden der Historie beobachten (z.B. noch nie gestreamt bei since="stream") */
48
+ onError(listener: (error: Error) => void): () => void;
49
+ /** Feed beenden, der Handler wird danach nicht mehr aufgerufen */
50
+ stop(): void;
51
+ private load;
52
+ private fetchHistory;
53
+ private scheduleRetry;
54
+ private clearRetry;
55
+ private handleLive;
56
+ private deliver;
57
+ }