From 242bbf13832b85616ac9abda1ae83eb433ed2559 Mon Sep 17 00:00:00 2001 From: jeanotx32 Date: Tue, 11 Aug 2026 23:31:26 +0200 Subject: [PATCH] Feat : Handling idle state --- README.md | 26 ++++++++++++- packages/server/src/db.ts | 39 +++++++++++++++++++ packages/shared/src/index.ts | 23 ++++++++--- packages/web/src/components/AgentCard.tsx | 6 ++- packages/web/src/components/AgentSettings.tsx | 7 +++- packages/web/src/components/StreamersPage.tsx | 18 ++++++++- 6 files changed, 108 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a03a174..0a416ae 100644 --- a/README.md +++ b/README.md @@ -239,9 +239,33 @@ aucun autre moyen de connaître. La rétention est celle du journal global (`LOG_RETENTION`) : les entrées les plus anciennes sont purgées, toutes VM confondues. +## Statuts Stripchat + +Le champ autoritatif est `user.user.status`. Deux familles, deux traitements : + +| Statut brut | Signification | Traitement | +| --- | --- | --- | +| `public` | diffusion publique | enregistré | +| `private`, `p2p`, `groupShow`, `virtualPrivate`, `ticketShow` | show payant | pause, sans clôture | +| `idle` | **« revient bientôt »** — connecté mais ne diffuse pas | pause, sans clôture | +| `off`, `notFound`, `deleted` | déconnecté | pause puis [clôture différée](#passage-hors-ligne) | + +`idle` se distingue de `off` par `isLive: false` mais `isOnline: true` : le streamer est +toujours là, il s'est simplement absenté. Le flux revient, donc l'enregistrement se met en +pause et attend, exactement comme pendant un show privé — il ne se clôt pas. + +Ne pas confondre avec `offlineStatus`, un champ voisin : c'est le message d'absence libre +du modèle (« I'll be back soon »), qui reste renseigné pendant qu'il diffuse. Il ne dit +rien de l'état courant et n'est pas utilisé. + +La liste des statuts mis en pause est modifiable par agent, dans « Surveillance du stream ». +Retirer `groupShow` pour continuer à enregistrer les shows de groupe, par exemple. Les +agents créés avant l'ajout d'`idle` sont migrés au démarrage du serveur, sauf si leur liste +a été personnalisée. + ## Passage hors-ligne -Quand le statut brut passe à « off » (ou « idle »), deux temps distincts : +Quand le statut brut passe à `off`, deux temps distincts : 1. **Pause immédiate.** L'écran d'attente n'a rien à faire dans le fichier. Aucun délai, aucune confirmation : reprendre ne coûte rien si la lecture était fausse. diff --git a/packages/server/src/db.ts b/packages/server/src/db.ts index dc4b48c..ed6c93c 100644 --- a/packages/server/src/db.ts +++ b/packages/server/src/db.ts @@ -112,6 +112,45 @@ addColumnIfMissing('watch_targets', 'last_live_ended_at', 'INTEGER'); // longtemps. Desactivé par défaut : rien ne doit s'enclencher sans un geste. addColumnIfMissing('watch_targets', 'auto_record', 'INTEGER NOT NULL DEFAULT 0'); +/** + * `idle` — le « revient bientôt » de Stripchat — relevait de la clôture et non de + * la pause dans les versions antérieures. Les agents créés avant ce changement + * portent la liste figée dans leur JSON et n'hériteraient pas du nouveau défaut. + * + * La migration ne touche que les listes laissées telles quelles : dès qu'un + * opérateur a personnalisé la sienne, elle ne correspond plus et reste intacte. + * Elle est de ce fait idempotente — après un passage, plus aucune ligne ne + * satisfait la condition. + */ +const LEGACY_PRIVATE_STATUSES = ['private', 'p2p', 'groupShow', 'virtualPrivate', 'ticketShow']; + +function migrateIdleStatus(): void { + const rows = db.prepare('SELECT id, watch_json FROM agents WHERE watch_json IS NOT NULL').all() as + unknown as Array<{ id: string; watch_json: string }>; + const update = db.prepare('UPDATE agents SET watch_json = ? WHERE id = ?'); + let migrated = 0; + + for (const row of rows) { + const watch = safeJsonParse(row.watch_json); + const statuses = watch?.privateStatuses; + if (!Array.isArray(statuses)) continue; + if (statuses.length !== LEGACY_PRIVATE_STATUSES.length) continue; + if (!LEGACY_PRIVATE_STATUSES.every((status) => statuses.includes(status))) continue; + + update.run( + JSON.stringify({ ...watch, privateStatuses: [...statuses, 'idle'] }), + row.id, + ); + migrated += 1; + } + + if (migrated > 0) { + console.log(`Migration : « idle » ajouté aux statuts de pause de ${migrated} agent(s)`); + } +} + +migrateIdleStatus(); + export interface AgentRow { id: string; name: string; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 42641a0..d407452 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -334,8 +334,12 @@ export interface WatchSettings { username: string; pollIntervalMs: number; /** - * Statuts bruts de l'API considérés comme « pas de flux public ». - * Valeurs observées côté Stripchat : public, private, p2p, groupShow, idle. + * Statuts bruts de l'API considérés comme « pas de flux public, mais le + * streamer est toujours là ». Ils mettent l'enregistrement en pause sans le + * clore : le flux est censé revenir. + * + * Énumération relevée sur l'API Stripchat : `public`, `private`, `p2p`, + * `groupShow`, `virtualPrivate`, `ticketShow`, `idle`, `off`. */ privateStatuses: string[]; /** @@ -417,7 +421,10 @@ export const DEFAULT_WATCH_SETTINGS: WatchSettings = { provider: 'stripchat', username: '', pollIntervalMs: 10_000, - privateStatuses: ['private', 'p2p', 'groupShow', 'virtualPrivate', 'ticketShow'], + // `idle` est le « revient bientôt » : le modèle est connecté (isOnline) mais ne + // diffuse pas (isLive faux). Il relève de la pause, pas de la clôture — à la + // différence de `off`, qui est une déconnexion franche. + privateStatuses: ['private', 'p2p', 'groupShow', 'virtualPrivate', 'ticketShow', 'idle'], confirmations: 2, pauseOnPrivate: true, resumeOnPublic: true, @@ -861,7 +868,13 @@ export function stripchatProfileUrl(username: string): string { * * Le champ autoritatif est `user.user.status` sur * `/api/front/v2/models/username/{pseudo}/cam`. Valeurs relevées en production : - * `public`, `private`, `p2p`, `groupShow`, `idle`. + * `public`, `private`, `p2p`, `groupShow`, `virtualPrivate`, `idle`, `off`. + * + * Deux champs voisins prêtent à confusion et ne doivent pas servir ici : + * - `offlineStatus` est le message d'absence libre du modèle. Il reste + * renseigné pendant qu'il diffuse : il ne dit rien de l'état courant. + * - `isLive` / `isOnline` distinguent `idle` (connecté, ne diffuse pas) de + * `off` (déconnecté), ce que `status` exprime déjà. * * Mutualisé entre l'agent (pause automatique) et le serveur (veille) : une seule * définition de l'endpoint et du chemin du champ à maintenir. @@ -928,6 +941,6 @@ export function mapStreamStatus(raw: string | undefined, privateStatuses: string const value = raw.toLowerCase(); if (privateStatuses.some((status) => status.toLowerCase() === value)) return 'private'; if (value === 'public') return 'public'; - // idle / off / offline / deleted : le modèle ne diffuse pas. + // off / offline / deleted / notFound : le modèle n'est plus là du tout. return 'offline'; } diff --git a/packages/web/src/components/AgentCard.tsx b/packages/web/src/components/AgentCard.tsx index e9ec8f6..a356ae2 100644 --- a/packages/web/src/components/AgentCard.tsx +++ b/packages/web/src/components/AgentCard.tsx @@ -218,7 +218,11 @@ function WatchStrip({ onCheck: () => void; onFullscreen: () => void; }) { - const info = WATCH_LABELS[watch.state]; + // Même nuance que sur la page Streamers : « revient bientôt » n'est pas un show privé. + const info = + watch.state === 'private' && watch.rawStatus === 'idle' + ? { text: 'revient bientôt', tone: 'warn' } + : WATCH_LABELS[watch.state]; const stale = watch.lastCheckedAt > 0 && Date.now() - watch.lastCheckedAt > 60_000; return ( diff --git a/packages/web/src/components/AgentSettings.tsx b/packages/web/src/components/AgentSettings.tsx index ef497fe..db0a92d 100644 --- a/packages/web/src/components/AgentSettings.tsx +++ b/packages/web/src/components/AgentSettings.tsx @@ -508,8 +508,11 @@ export function AgentSettings({ agent, targets, onClose, onCommand, notify }: Pr } /> - Statuts renvoyés par Stripchat : public, private, p2p, groupShow, idle. Retire - groupShow si tu veux continuer à enregistrer les shows de groupe. + Statuts renvoyés par Stripchat : public, private, p2p, groupShow, + virtualPrivate, ticketShow, idle, off. Ceux listés ici mettent en pause sans + clore. idle est le « revient bientôt » : le streamer est + connecté mais ne diffuse pas. Retire groupShow pour continuer à enregistrer + les shows de groupe. diff --git a/packages/web/src/components/StreamersPage.tsx b/packages/web/src/components/StreamersPage.tsx index 593a6f3..4c363cc 100644 --- a/packages/web/src/components/StreamersPage.tsx +++ b/packages/web/src/components/StreamersPage.tsx @@ -10,6 +10,18 @@ const STATE_LABELS: Record = { unknown: { text: '…', tone: 'offline' }, }; +/** + * `idle` et `private` partagent le même état normalisé — les deux mettent + * l'enregistrement en pause sans le clore — mais ne veulent pas dire la même + * chose à l'écran : le premier est une absence, le second un show payant. + */ +function stateLabel(target: WatchTarget): { text: string; tone: string } { + if (target.state === 'private' && target.rawStatus === 'idle') { + return { text: 'revient bientôt', tone: 'warn' }; + } + return STATE_LABELS[target.state]; +} + interface Props { targets: WatchTarget[]; agents: AgentView[]; @@ -114,7 +126,7 @@ function StreamerCard({ const [busy, setBusy] = useState(false); const [avatarBroken, setAvatarBroken] = useState(false); - const state = STATE_LABELS[target.state]; + const state = stateLabel(target); const agent = agents.find((candidate) => candidate.id === target.agentId) ?? null; const recording = agent?.status.recording ?? false; const isLive = target.state === 'public'; @@ -171,7 +183,9 @@ function StreamerCard({ @{target.username} - {state.text} + + {state.text} +