Skip to content

kapp.twitch ​

Piloter des actions Twitch côté chaîne : raid, clip, sondage, prédiction, marqueur, infos de stream — lire les compteurs de la chaîne et suivre le Hype Train.

Service partagé appli + widget : API identique.

Quand l'utiliser (vs alternative) ​

  • Actions « broadcaster » : lancer un raid, créer un clip, ouvrir un sondage/prédiction natif Twitch.
  • Compteurs de la chaîne (nb de followers/subs/viewers) : followerCount(), subscriberCount(), viewerCount(). Récupérés sur appel, jamais au boot. (Titre/jeu = kapp.twitch.infos ; heure de début du live = kapp.stream.stream_started_at.)
  • Pour parler dans le chat, c'est kapp.chat ; pour les infos d'un viewer, kapp.users.
  • Pour des sondages/prédictions natifs Kappapps (multi-canal, mises en monnaie interne), voir polls.

Méthodes ​

Actions (broadcaster) ​

  • raid(channel: string): Promise<void> — démarre un raid vers channel (login). async.
  • unraid(): Promise<void> — annule le raid en cours. async.
  • clip({ title?, duration? }): Promise<ClipCreateResponse> — crée un clip et rend l'accusé de réception (id + edit_url). title = titre du clip (sinon Twitch en génère un), duration = 5 à 60 s (défaut 30). Nécessite que la chaîne soit en live, sinon rejet. async.
  • clip({ wait: true, title?, duration?, timeout? }): Promise<ReadyClip> — même chose, mais attend que Twitch ait publié le clip et rend le clip complet (ClipData + edit_url) : url, embed_url, thumbnail_url, duration… Compte quelques secondes. async.
  • getClip(clip_id: string): Promise<ClipData | null> — données complètes d'un clip, ou null s'il n'existe pas (encore). async.
  • getClips({ channel } | { game_id }, filtres?): Promise<ClipsPage> — page de clips d'une chaîne (login) ou d'une catégorie. Filtres : started_at, ended_at, is_featured, first (1-100), after, before. Rend { clips, cursor }. async.
  • marker(description: string): Promise<void> — pose un marqueur sur le VOD. async.
  • setInfos({ title?, category? }): Promise<void> — change titre / catégorie du stream. async. Appli : scope channel-infos:write.
  • poll(data): Promise<TwitchPoll> — crée un sondage natif Twitch. async. Le modèle TwitchPoll expose await getData() (état à jour), await terminate() (clôture avec résultats) et await archive() (clôture et masque) ; ces deux dernières ne font rien sur un sondage déjà fini.
  • prediction(data): Promise<TwitchPrediction> — crée une prédiction native Twitch. async.
  • channel(login: string): Promise<TwitchChannelInformations> — infos publiques d'une chaîne avec son live dans stream (null hors ligne). Appelle channels([login]) et rend un seul objet ; rejette en 404 si introuvable. async.
  • channels(logins: string[]): Promise<TwitchChannelInformations[]> — mêmes données pour 1 à 100 logins, dans un appel groupé. Voir ci-dessous. async.
  • game(name, method = 'name'): Promise<TwitchGameData> — recherche un jeu. async.

Infos et live de plusieurs chaînes ​

channels() accepte des logins Twitch (1 à 25 lettres, chiffres ou _, sans URL ni @). Le tableau rendu contient les chaînes trouvées dans l'ordre demandé, avec dédoublonnage sans distinction de casse. Les logins inconnus sont omis ; si aucun n'existe, le résultat est []. Aucun scope d'appli requis.

Chaque chaîne conserve ses champs broadcaster_*, title, game_id, game_name, tags, delay, content_classification_labels et is_branded_content. Elle contient aussi :

  • stream: null si elle est hors ligne ; la présence de cet objet suffit à savoir qu'elle est en live, sans champ is_live.
  • En live, l'objet public Twitch complet : id (ID du live), user_id, user_login, user_name, game_id, game_name, type, title, tags, viewer_count, started_at, language, thumbnail_url, tag_ids et is_mature.
  • viewer_count peut valoir 0 en live. started_at est une date UTC RFC3339, pas un timestamp. Remplacer {width} et {height} dans thumbnail_url par les dimensions en pixels. Les champs Twitch dépréciés tag_ids (tableau vide) et is_mature (false) sont conservés ; utiliser tags et les labels de contenu de la chaîne.

Une liste vide, plus de 100 entrées (avant dédoublonnage) ou un login mal formé rejettent en 400.

Cache serveur de 30 secondes par ID Twitch, commun à channel(), channels() et team.getChannels(), partagé entre les applis et widgets qui interrogent la même chaîne. Les infos de chaîne et l'objet stream sont cachés ensemble, y compris stream: null hors ligne. Une lecture en cache ne prolonge pas sa durée de vie. L'état live, le nombre de viewers, le titre et la catégorie peuvent donc avoir jusqu'à 30 secondes de retard ; les objets déjà reçus ne se mettent pas à jour automatiquement. Éviter le polling rapproché : redemander plus souvent ne force pas un rafraîchissement.

Seules les chaînes absentes du cache ou expirées sont redemandées à Twitch, toujours en lot : un appel pour leurs infos, un pour leurs lives (first=100), et éventuellement un pour résoudre les logins en IDs. Une erreur Twitch rejette la demande ; elle n'est pas mise en cache, ni convertie en liste partielle ou en statut hors ligne.

js
const channels = await kapp.twitch.channels(['twitchdev', 'another_streamer']);
for (const channel of channels) {
    if (channel.stream) {
        console.log(channel.broadcaster_login, channel.stream.viewer_count);
    }
}

Compteurs de la chaîne (lecture, à la demande) ​

Chiffres de la chaîne courante, récupérés sur appel (jamais poussés au boot). Valeurs cachées ~30-60 s côté serveur (pas du temps réel strict).

  • followerCount(): Promise<number> — nombre total de followers. 0 si illisible (streamer pas reconnecté depuis l'ajout du scope). async.
  • subscriberCount(): Promise<{ count: number, points: number }> — nombre d'abonnés (count) + total de sub points (points). { count: 0, points: 0 } si illisible. async.
  • viewerCount(): Promise<number> — nombre de viewers en direct. 0 si hors ligne. async.

Titre / jeu / uptime : pas ici. Déjà dispo sans appel réseau et en temps réel : kapp.twitch.infos.title + kapp.twitch.infos.category_name / category_id (le jeu), et kapp.stream.stream_started_at (pour l'uptime). Seul le nombre de viewers manquait vraiment, d'où viewerCount().

Scope (infos.json → scopes) : followerCount() et subscriberCount() exigent la permission channel-stats:read. viewerCount() n'exige aucun scope (le nombre de viewers est public).

Events d'alerte temps réel (écoute) ​

Enregistre un callback appelé à chaque occurrence de l'event, tant que l'app/widget tourne. Chaque callback reçoit un model typé (props brutes + helpers). sync (l'enregistrement ; le callback, lui, peut être async).

  • onFollow(cb: (event: FollowEvent) => void) — nouveau follow. event.user_name, event.getUser(), event.getFollowedAt() (Date).
  • onSub(cb: (event: SubEvent) => void) — nouvel abo (hors gift/resub). event.getTier() (1|2|3), event.getTierRaw() ("1000"…), event.isGift().
  • onResub(cb: (event: ResubEvent) => void) — resub avec message partagé. event.getCumulativeMonths(), event.getStreakMonths(), event.getDurationMonths() (mois couverts par ce renouvellement ou null), event.getMessage().
  • onSubGift(cb: (event: SubGiftEvent) => void) — don d'abo(s). event.total (ou event.getTotal()), event.getCumulativeTotal() (total historique du donneur, null si non partagé ou anonyme), event.isAnonymous(), event.getGifter() (null si anonyme).
  • onRaid(cb: (event: RaidEvent) => void) — raid entrant. event.from_broadcaster_user_name, event.viewers (ou event.getViewers()), event.getRaider().

Hype Train (état + temps réel) ​

  • hypeTrain.getStatus(): Promise<HypeTrainStatus> — état initial, train actif (current) et records. Cache serveur ~45 s. Exige le scope d'app twitch-hype-train:read.
  • hypeTrain.onBegin(cb) — début d'un train.
  • hypeTrain.onProgress(cb) — progression (niveau, total, objectif, top contributeurs, Shared Hype Train).
  • hypeTrain.onEnd(cb) — fin, avec ended_at et cooldown_ends_at.

Le résultat vaut { available: false, reason: 'missing_twitch_scope', ... } si le streamer n'a pas encore accordé channel:read:hype_train. Hydrate une fois avec getStatus(), puis utilise les événements : ne poll pas Helix depuis le widget. Twitch peut livrer progress avant begin, donc le handler onProgress doit aussi savoir initialiser son affichage.

Pubs, Goals et Charity (état + temps réel) ​

  • ads.getSchedule(): Promise<AdScheduleStatus> — prochain/dernier ad, snoozes et temps sans preroll. Scope twitch-ads:read.
  • ads.snooze(): Promise<AdSnoozeResult> et ads.start(30|60|90|120|150|180): Promise<AdStartResult> — actions sensibles, scope twitch-ads:manage.
  • ads.onBegin(cb) — début d'une coupure manuelle ou automatique, avec durée et requester typés.
  • goals.getActive(): Promise<CreatorGoalsStatus> puis goals.onBegin/onProgress/onEnd(cb) — Creator Goals. Scope twitch-goals:read.
  • charity.getCampaign(): Promise<CharityCampaignStatus> puis charity.onStart/onProgress/onStop/onDonate(cb) — campagne active et dons. Scope twitch-charity:read.

Comme pour le Hype Train, hydrate une fois par get*(), puis maintiens l'affichage par EventSub. Les réponses d'état portent available et éventuellement reason: 'missing_twitch_scope'; ne transforme pas une indisponibilité en faux zéro.

Planning, Bits, Shared Chat, vidéos et marqueurs (lecture) ​

  • schedule.get(options?): Promise<TwitchSchedule> et schedule.getNext(): Promise<TwitchScheduleSegment | null> — planning en lecture seule.
  • bits.getLeaderboard(options?): Promise<BitsLeaderboardStatus>, bits.getCheermotes(): Promise<TwitchCheermote[]>, bits.getPowerUps(): Promise<BitsPowerUpsStatus> — scope twitch-bits:read pour leaderboard/Power-ups; les Cheermotes réutilisent le cache serveur partagé.
  • sharedChat.getSession(): Promise<SharedChatSession | null> — session active et participants. Les messages bot Kappapps restent limités à la chaîne source; les annonces broadcaster se propagent à la session.
  • videos.list({ channel, ...filtres }) ou videos.list({ game_id, ...filtres }): Promise<VideosPage> — exactement une source est requise. videos.get(id) renvoie TwitchVideo | null.
  • getMarkers(options?): Promise<StreamMarkersStatus> — liste les marqueurs du dernier VOD ou d'une vidéo précise. Scope stream-markers:read.

Sécurité avancée (kapp.twitch.moderation) ​

  • checkMessage(text): Promise<AutoModCheckResult> — pré-valide un message via AutoMod. Scope moderation:check; appel limité, ne pas l'utiliser sur chaque frappe.
  • getShieldMode(): Promise<ShieldModeStatus> — scope shield-mode:read.
  • setShieldMode(enabled): Promise<ShieldModeStatus> — scope sensible shield-mode:manage, avec rate limit et audit backend structurés.

Ces opérations portent sur la chaîne Twitch dans son ensemble et vivent donc sous kapp.twitch.moderation. kapp.users.moderation reste réservé aux actions visant un viewer précis (ban, timeout, VIP, modérateur, blacklist).

Écoute (k.twitch.onX) vs trigger de lancement (ONSUB/ONFOLLOW/ONRAID) — deux mécanismes distincts :

  • Trigger = le widget est lancé par l'event (one-shot), données dans kapp.triggerRequest.setter. Pour « afficher une alerte quand quelqu'un sub ».
  • onX = une app/widget qui tourne déjà réagit à l'event (continu). Pour un jeu/overlay persistant qui doit réagir aux subs/raids sans être relancé.

getUser()/getGifter()/getRaider() sont async (lookup du profil Kappapps : currency, rôle…). Pour juste afficher un pseudo, lis event.user_name (synchrone).

Exemple (V2, exécutable) ​

js
// Action broadcaster : lancer un raid
const target = 'gamesdone';
try {
    await kapp.twitch.raid(target.trim().toLowerCase().replace(/^@/, ''));
} catch (err) {
    console.error('Raid impossible : ' + (err?.message || err));
}

// Compteurs de la chaîne, à la demande
const followers = await kapp.twitch.followerCount();
const subs = await kapp.twitch.subscriberCount();
const viewers = await kapp.twitch.viewerCount();
kapp.chat.say(`${followers} followers · ${subs.count} abos (${subs.points} pts) · ${viewers} viewers`);

// Titre / jeu / uptime : pas d'appel réseau, déjà en mémoire et en temps réel
console.log(`${kapp.twitch.infos.title} — ${kapp.twitch.infos.category_name}`);
if (kapp.stream.stream_started_at) {
    const uptime = Math.floor(Date.now() / 1000 - kapp.stream.stream_started_at);
    console.log(`En live depuis ${uptime}s`);
}

// Hype Train : hydratation initiale, puis mises à jour EventSub
const hype = await kapp.twitch.hypeTrain.getStatus();
if (hype.available && hype.current) {
    console.log(`Hype Train niveau ${hype.current.level}: ${hype.current.progress}/${hype.current.goal}`);
}
kapp.twitch.hypeTrain.onProgress((train) => {
    console.log(`Hype Train niveau ${train.level}: ${train.progress}/${train.goal}`);
});

// Écoute d'alertes : une app qui tourne réagit aux subs & raids
kapp.twitch.onSub((event) => {
    kapp.chat.say(`Bienvenue ${event.user_name} — abo tier ${event.getTier()} ! 🎉`);
});
kapp.twitch.onResub((event) => {
    kapp.chat.say(`${event.user_name} resub (${event.getCumulativeMonths()} mois) : ${event.getMessage()}`);
});
kapp.twitch.onSubGift((event) => {
    const who = event.isAnonymous() ? 'Un viewer anonyme' : event.user_name;
    kapp.chat.say(`${who} a offert ${event.total} abo(s) ! 🎁`);
});
kapp.twitch.onRaid((event) => {
    kapp.chat.say(`Raid de ${event.from_broadcaster_user_name} (+${event.viewers}) ! 🚀`);
});

Pièges ​

  • raid() peut throw (chaîne hors-ligne, droits…) → entoure d'un try/catch.
  • Après clip(), Twitch met quelques secondes à rendre le clip disponible : getClip(id) retourne null pendant ce délai (Twitch dit de considérer l'échec passé 60 s). N'écris pas la boucle de polling toi-même → clip({ wait: true }) le fait et te rend le clip complet. En cas de dépassement, rejet avec une erreur name === 'ClipNotReadyError' portant clip_id et edit_url (le clip existe, c'est sa publication qui traîne).
  • clip({ wait: true }) coûte quelques appels API de plus : réserve-le aux cas où tu as besoin de url / thumbnail_url. Pour juste déclencher un clip, clip() suffit.
  • title et duration se passent à la création — plus besoin de l'edit_url pour ça. Le title est soumis à AutoMod : un titre refusé fait échouer tout l'appel (400), et c'est imprévisible côté appli. Twitch n'impose aucune limite de longueur sur ce titre (les 140 caractères, c'est le titre du stream, pas du clip).
  • duration = 5 à 60 s inclus, intervalle continu (pas une liste de valeurs comme les pubs), arrondi à 0,1 s. Hors bornes → RangeError levé immédiatement par le SDK, sans appel réseau.
  • has_delay est mort : Twitch a retiré le paramètre (il n'avait aucun effet). Il reste accepté pour ne rien casser, mais n'est plus transmis — ne l'utilise pas dans du nouveau code.
  • vod_offset vaut null et video_id est une chaîne vide sur un clip pris pendant le live en cours (la VOD n'est pas encore découpée) — c'est le cas normal de clip({ wait: true }), ne fais pas de calcul dessus sans tester.
  • clip() capture ~90 s autour de l'appel (~85 s avant, ~5 s après) et publie les 30 dernières secondes par défaut. L'edit_url (re-découper l'extrait, mettre en avant le clip) est valide 24 h ou jusqu'à publication.
  • clip() exige aussi que les clips soient activés dans les réglages du créateur ; les restrictions follower-only / sub-only s'appliquent. Le message d'erreur remonté vient de Twitch.
  • getClips() est trié par nombre de vues décroissant, PAS du plus récent au plus ancien. Pour « les derniers clips », filtre avec started_at / ended_at — trier par date n'est pas possible.
  • getClips({ started_at }) sans ended_at = fenêtre de 7 jours à partir de started_at (défaut Twitch), pas « depuis cette date jusqu'à maintenant ».
  • La pagination de getClips() plafonne à ~1000 clips cumulés : au-delà, découpe en plusieurs fenêtres started_at/ended_at au lieu de continuer à suivre le cursor.
  • Si tu lances un raid avec décompte, pense à unraid() dans un hook de sortie (onBeforeAbort widget / onBeforeFinish appli) pour les annulations.
  • channel/login : pseudo Twitch en minuscules, sans @.
  • onSub ≠ onResub ≠ onSubGift : trois events Twitch distincts. Pour réagir à tout abo, enregistre les trois.
  • event.tier est la valeur brute Twitch ("1000"/"2000"/"3000") → utilise event.getTier() pour l'échelon 1|2|3.
  • onSubGift décrit le donneur et le nombre de dons (total), pas chaque bénéficiaire. getGifter() = null si le don est anonyme.
  • Le cheer/bits n'a pas d'event dédié ici : il arrive via le chat → chatEvent.isCheer() / getCheer() (voir chat.md).
  • Compteurs (followerCount/subscriberCount/viewerCount) ≠ temps réel : valeurs cachées ~30-60 s. Pour un décompte exact à la seconde (barre de progression d'un goal…), additionne les events onFollow/onSub à un point de départ lu une fois, plutôt que de poller la méthode.
  • followerCount()/subscriberCount() renvoient 0 (et non une erreur) tant que le streamer n'a pas reconnecté sa chaîne après l'ajout des permissions Twitch correspondantes — traite 0 comme « indisponible », pas forcément « zéro abonné ».
  • viewerCount() renvoie 0 quand la chaîne est hors ligne (indistinct d'un live à 0 viewer). Pour savoir si la chaîne est en live, teste kapp.stream.stream_id (non-null si live) ou écoute kapp.stream (STREAM_ONLINE/STREAM_OFFLINE).
  • Ne re-sers pas titre/jeu/uptime via un compteur : kapp.twitch.infos (titre/catégorie) et kapp.stream.stream_started_at sont en temps réel et gratuits (mis à jour par event), là où un appel serait câché ~30 s.
  • Ces callbacks servent une app/widget qui tourne. Pour lancer un widget au moment d'un sub/raid/follow, configure plutôt un trigger ONSUB/ONRAID/ONFOLLOW. Dans le parcours de création de widgets, l'outil read_sdk_doc avec service: "triggers" décrit cette configuration ; cette fiche interne n'est pas livrée avec le corpus applis.