Feat : STRCHA Stream status
This commit is contained in:
@@ -43,6 +43,8 @@ export const AGENT_ACTIONS = [
|
||||
'profile.set',
|
||||
'collection.set',
|
||||
'recordDirectory.set',
|
||||
'watch.check',
|
||||
'hotkey.fullscreen',
|
||||
'agent.ping',
|
||||
] as const;
|
||||
|
||||
@@ -60,6 +62,97 @@ export interface AgentActionParams {
|
||||
'recordDirectory.set': { directory: string };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Surveillance du statut d'un stream (pause auto pendant les shows privés)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const WATCH_PROVIDERS = ['stripchat'] as const;
|
||||
export type WatchProvider = (typeof WATCH_PROVIDERS)[number];
|
||||
|
||||
/** État normalisé du stream surveillé. */
|
||||
export type StreamState = 'public' | 'private' | 'offline' | 'unknown';
|
||||
|
||||
export interface FullscreenSettings {
|
||||
/** Rappeler le plein écran à la fin d'un show privé. */
|
||||
enabled: boolean;
|
||||
/** Touche à envoyer au lecteur (raccourci plein écran, « f » sur Stripchat). */
|
||||
key: string;
|
||||
/** Fragment de titre de fenêtre identifiant le lecteur (insensible à la casse). */
|
||||
windowMatch: string;
|
||||
/** Délai avant l'envoi, le temps que le flux public soit rechargé. */
|
||||
delayMs: number;
|
||||
}
|
||||
|
||||
export interface WatchSettings {
|
||||
enabled: boolean;
|
||||
provider: WatchProvider;
|
||||
/** Pseudo du streamer, tel qu'il apparaît dans l'URL de sa page. */
|
||||
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.
|
||||
*/
|
||||
privateStatuses: string[];
|
||||
/**
|
||||
* Lectures « privé » consécutives exigées avant de mettre en pause.
|
||||
* Asymétrique volontairement : une fausse pause coûte du contenu perdu,
|
||||
* une fausse reprise ne coûte que quelques secondes d'écran d'attente.
|
||||
*/
|
||||
confirmations: number;
|
||||
pauseOnPrivate: boolean;
|
||||
resumeOnPublic: boolean;
|
||||
fullscreen: FullscreenSettings;
|
||||
}
|
||||
|
||||
export const DEFAULT_WATCH_SETTINGS: WatchSettings = {
|
||||
enabled: false,
|
||||
provider: 'stripchat',
|
||||
username: '',
|
||||
pollIntervalMs: 10_000,
|
||||
privateStatuses: ['private', 'p2p', 'groupShow', 'virtualPrivate', 'ticketShow'],
|
||||
confirmations: 2,
|
||||
pauseOnPrivate: true,
|
||||
resumeOnPublic: true,
|
||||
fullscreen: {
|
||||
enabled: true,
|
||||
key: 'f',
|
||||
windowMatch: 'Stripchat',
|
||||
delayMs: 4000,
|
||||
},
|
||||
};
|
||||
|
||||
/** État courant de la surveillance, remonté avec le statut de l'agent. */
|
||||
export interface WatchState {
|
||||
enabled: boolean;
|
||||
provider: WatchProvider;
|
||||
username: string;
|
||||
state: StreamState;
|
||||
/** Statut brut renvoyé par l'API, utile pour diagnostiquer un mapping. */
|
||||
rawStatus?: string;
|
||||
/** Depuis quand l'état normalisé est stable. */
|
||||
since: number;
|
||||
lastCheckedAt: number;
|
||||
lastError?: string;
|
||||
/** Vrai si c'est la surveillance — et non l'opérateur — qui a mis en pause. */
|
||||
autoPaused: boolean;
|
||||
/** Lectures « privé » accumulées, en attente du seuil de confirmation. */
|
||||
pendingConfirmations: number;
|
||||
}
|
||||
|
||||
export function emptyWatchState(settings: WatchSettings): WatchState {
|
||||
return {
|
||||
enabled: settings.enabled,
|
||||
provider: settings.provider,
|
||||
username: settings.username,
|
||||
state: 'unknown',
|
||||
since: Date.now(),
|
||||
lastCheckedAt: 0,
|
||||
autoPaused: false,
|
||||
pendingConfirmations: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Statut remonté par un agent
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -102,6 +195,9 @@ export interface AgentStatus {
|
||||
diskFreeBytes?: number;
|
||||
diskTotalBytes?: number;
|
||||
|
||||
/** Surveillance du stream source, absente si elle n'est pas configurée. */
|
||||
watch?: WatchState;
|
||||
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
@@ -181,6 +277,7 @@ export interface WelcomeMessage {
|
||||
statusIntervalMs: number;
|
||||
/** Si vrai, l'agent tente de se connecter à OBS dès le démarrage. */
|
||||
autoConnectObs: boolean;
|
||||
watch: WatchSettings;
|
||||
}
|
||||
|
||||
export interface CommandMessage {
|
||||
@@ -194,6 +291,7 @@ export interface ConfigMessage {
|
||||
type: 'config';
|
||||
obs: ObsSettings;
|
||||
autoConnectObs: boolean;
|
||||
watch: WatchSettings;
|
||||
}
|
||||
|
||||
export interface PingMessage {
|
||||
@@ -218,6 +316,7 @@ export interface AgentView {
|
||||
createdAt: number;
|
||||
obs: ObsSettings;
|
||||
autoConnectObs: boolean;
|
||||
watch: WatchSettings;
|
||||
notes: string | null;
|
||||
status: AgentStatus;
|
||||
}
|
||||
@@ -255,3 +354,59 @@ export function safeJsonParse<T>(raw: string): T | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise une configuration de surveillance venue du réseau ou de la base :
|
||||
* champs manquants complétés, valeurs numériques bornées.
|
||||
*/
|
||||
export function normalizeWatchSettings(raw: unknown): WatchSettings {
|
||||
const input = (raw ?? {}) as Partial<WatchSettings>;
|
||||
const base = DEFAULT_WATCH_SETTINGS;
|
||||
const fullscreen = (input.fullscreen ?? {}) as Partial<FullscreenSettings>;
|
||||
|
||||
const clamp = (value: unknown, fallback: number, min: number, max: number): number => {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.min(Math.max(Math.round(parsed), min), max);
|
||||
};
|
||||
|
||||
const statuses = Array.isArray(input.privateStatuses)
|
||||
? input.privateStatuses.filter((s): s is string => typeof s === 'string' && s.trim() !== '')
|
||||
: base.privateStatuses;
|
||||
|
||||
return {
|
||||
enabled: input.enabled === true,
|
||||
provider: WATCH_PROVIDERS.includes(input.provider as WatchProvider)
|
||||
? (input.provider as WatchProvider)
|
||||
: base.provider,
|
||||
username: typeof input.username === 'string' ? input.username.trim() : base.username,
|
||||
// Plancher à 3 s : inutile de marteler l'API, un show privé dure des minutes.
|
||||
pollIntervalMs: clamp(input.pollIntervalMs, base.pollIntervalMs, 3000, 300_000),
|
||||
privateStatuses: statuses.length > 0 ? statuses : base.privateStatuses,
|
||||
confirmations: clamp(input.confirmations, base.confirmations, 1, 10),
|
||||
pauseOnPrivate: input.pauseOnPrivate !== false,
|
||||
resumeOnPublic: input.resumeOnPublic !== false,
|
||||
fullscreen: {
|
||||
enabled: fullscreen.enabled !== false,
|
||||
key:
|
||||
typeof fullscreen.key === 'string' && fullscreen.key.trim()
|
||||
? fullscreen.key.trim()
|
||||
: base.fullscreen.key,
|
||||
windowMatch:
|
||||
typeof fullscreen.windowMatch === 'string'
|
||||
? fullscreen.windowMatch.trim()
|
||||
: base.fullscreen.windowMatch,
|
||||
delayMs: clamp(fullscreen.delayMs, base.fullscreen.delayMs, 0, 120_000),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Traduit un statut brut de l'API en état normalisé. */
|
||||
export function mapStreamStatus(raw: string | undefined, privateStatuses: string[]): StreamState {
|
||||
if (!raw) return 'unknown';
|
||||
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.
|
||||
return 'offline';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user