Feat : Handling idle state
This commit is contained in:
26
README.md
26
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
|
La rétention est celle du journal global (`LOG_RETENTION`) : les entrées les plus
|
||||||
anciennes sont purgées, toutes VM confondues.
|
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
|
## 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,
|
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.
|
aucune confirmation : reprendre ne coûte rien si la lecture était fausse.
|
||||||
|
|||||||
@@ -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.
|
// longtemps. Desactivé par défaut : rien ne doit s'enclencher sans un geste.
|
||||||
addColumnIfMissing('watch_targets', 'auto_record', 'INTEGER NOT NULL DEFAULT 0');
|
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<WatchSettings>(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 {
|
export interface AgentRow {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -334,8 +334,12 @@ export interface WatchSettings {
|
|||||||
username: string;
|
username: string;
|
||||||
pollIntervalMs: number;
|
pollIntervalMs: number;
|
||||||
/**
|
/**
|
||||||
* Statuts bruts de l'API considérés comme « pas de flux public ».
|
* Statuts bruts de l'API considérés comme « pas de flux public, mais le
|
||||||
* Valeurs observées côté Stripchat : public, private, p2p, groupShow, idle.
|
* 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[];
|
privateStatuses: string[];
|
||||||
/**
|
/**
|
||||||
@@ -417,7 +421,10 @@ export const DEFAULT_WATCH_SETTINGS: WatchSettings = {
|
|||||||
provider: 'stripchat',
|
provider: 'stripchat',
|
||||||
username: '',
|
username: '',
|
||||||
pollIntervalMs: 10_000,
|
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,
|
confirmations: 2,
|
||||||
pauseOnPrivate: true,
|
pauseOnPrivate: true,
|
||||||
resumeOnPublic: true,
|
resumeOnPublic: true,
|
||||||
@@ -861,7 +868,13 @@ export function stripchatProfileUrl(username: string): string {
|
|||||||
*
|
*
|
||||||
* Le champ autoritatif est `user.user.status` sur
|
* Le champ autoritatif est `user.user.status` sur
|
||||||
* `/api/front/v2/models/username/{pseudo}/cam`. Valeurs relevées en production :
|
* `/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
|
* Mutualisé entre l'agent (pause automatique) et le serveur (veille) : une seule
|
||||||
* définition de l'endpoint et du chemin du champ à maintenir.
|
* 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();
|
const value = raw.toLowerCase();
|
||||||
if (privateStatuses.some((status) => status.toLowerCase() === value)) return 'private';
|
if (privateStatuses.some((status) => status.toLowerCase() === value)) return 'private';
|
||||||
if (value === 'public') return 'public';
|
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';
|
return 'offline';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,7 +218,11 @@ function WatchStrip({
|
|||||||
onCheck: () => void;
|
onCheck: () => void;
|
||||||
onFullscreen: () => 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;
|
const stale = watch.lastCheckedAt > 0 && Date.now() - watch.lastCheckedAt > 60_000;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -508,8 +508,11 @@ export function AgentSettings({ agent, targets, onClose, onCommand, notify }: Pr
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span className="muted small">
|
<span className="muted small">
|
||||||
Statuts renvoyés par Stripchat : public, private, p2p, groupShow, idle. Retire
|
Statuts renvoyés par Stripchat : public, private, p2p, groupShow,
|
||||||
groupShow si tu veux continuer à enregistrer les shows de groupe.
|
virtualPrivate, ticketShow, idle, off. Ceux listés ici mettent en pause sans
|
||||||
|
clore. <strong>idle</strong> est le « revient bientôt » : le streamer est
|
||||||
|
connecté mais ne diffuse pas. Retire groupShow pour continuer à enregistrer
|
||||||
|
les shows de groupe.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ const STATE_LABELS: Record<StreamState, { text: string; tone: string }> = {
|
|||||||
unknown: { text: '…', tone: 'offline' },
|
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 {
|
interface Props {
|
||||||
targets: WatchTarget[];
|
targets: WatchTarget[];
|
||||||
agents: AgentView[];
|
agents: AgentView[];
|
||||||
@@ -114,7 +126,7 @@ function StreamerCard({
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [avatarBroken, setAvatarBroken] = 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 agent = agents.find((candidate) => candidate.id === target.agentId) ?? null;
|
||||||
const recording = agent?.status.recording ?? false;
|
const recording = agent?.status.recording ?? false;
|
||||||
const isLive = target.state === 'public';
|
const isLive = target.state === 'public';
|
||||||
@@ -171,7 +183,9 @@ function StreamerCard({
|
|||||||
<span className="muted small">@{target.username}</span>
|
<span className="muted small">@{target.username}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className={`badge ${state.tone}`}>{state.text}</span>
|
<span className={`badge ${state.tone}`} title={`statut brut « ${target.rawStatus ?? '?'} »`}>
|
||||||
|
{state.text}
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<dl className="streamer-facts">
|
<dl className="streamer-facts">
|
||||||
|
|||||||
Reference in New Issue
Block a user