|
| 1 | +/** Typed monitor responses. Public fields use camelCase like the transcript models. */ |
| 2 | +import { |
| 3 | + normalizeErrorBlock, normalizeTranscript, normalizeUsage, normalizeVideo, |
| 4 | + type ApiErrorBlock, type Transcript, type Usage, type Video, |
| 5 | +} from "./models"; |
| 6 | + |
| 7 | +export type MonitorInterval = 15 | 60 | 360 | 1440; |
| 8 | +export interface MonitorSnapshot { |
| 9 | + id: string; |
| 10 | + name: string | null; |
| 11 | + type: "channel"; |
| 12 | + platform: string; |
| 13 | + target: string; |
| 14 | +} |
| 15 | +export interface Monitor extends MonitorSnapshot { |
| 16 | + kind: "monitor"; |
| 17 | + options: { tab?: "videos" | "shorts" | "live" }; |
| 18 | + status: "active" | "paused"; |
| 19 | + hasNew: boolean; |
| 20 | + lastEventId: string | null; |
| 21 | + intervalMinutes: MonitorInterval; |
| 22 | + transcripts: boolean; |
| 23 | + webhookUrl: string | null; |
| 24 | + nextCheckAt: string | null; |
| 25 | + lastCheckedAt: string | null; |
| 26 | + lastError: (ApiErrorBlock & { at: string | null }) | null; |
| 27 | + createdAt: string; |
| 28 | + updatedAt: string; |
| 29 | + /** Returned on creation only. Save securely before discarding the response. */ |
| 30 | + webhookSecret: string | null; |
| 31 | + /** Returned on creation only; existing videos skipped by the baseline. */ |
| 32 | + baselineCount: number | null; |
| 33 | + usage: Usage | null; |
| 34 | +} |
| 35 | +export interface MonitorList { |
| 36 | + kind: "monitor_list"; |
| 37 | + monitors: Monitor[]; |
| 38 | + hasNew: boolean; |
| 39 | + lastEventId: string | null; |
| 40 | + limits: { maxMonitors: number | null; minIntervalMinutes: number }; |
| 41 | +} |
| 42 | +export interface MonitorDeleted { kind: "monitor_deleted"; id: string } |
| 43 | +export interface MonitorDelivery { |
| 44 | + status: "none" | "pending" | "delivered" | "failed"; |
| 45 | + attempts: number; |
| 46 | + lastAttemptAt: string | null; |
| 47 | + nextAttemptAt: string | null; |
| 48 | + lastError: string | null; |
| 49 | + deliveredAt: string | null; |
| 50 | +} |
| 51 | +export interface MonitorTranscriptResult { |
| 52 | + videoId: string; |
| 53 | + url: string; |
| 54 | + outcome: "ok" | "processing" | "error"; |
| 55 | + /** Present when an audio job has already been queued. */ |
| 56 | + jobId: string | null; |
| 57 | + transcript: Transcript | null; |
| 58 | + error: ApiErrorBlock | null; |
| 59 | +} |
| 60 | +interface MonitorEventBase { |
| 61 | + id: string; |
| 62 | + createdAt: string; |
| 63 | + monitor: MonitorSnapshot; |
| 64 | + creditsSpent: number; |
| 65 | + /** Absent on webhook bodies; present in event listing/check responses. */ |
| 66 | + delivery: MonitorDelivery | null; |
| 67 | +} |
| 68 | +export type MonitorEvent = MonitorEventBase & ( |
| 69 | + | { type: "monitor.videos"; data: { videos: Video[]; transcripts?: MonitorTranscriptResult[] } } |
| 70 | + | { type: "monitor.transcript"; data: MonitorTranscriptResult & { videosEventId: string } } |
| 71 | +); |
| 72 | +export interface MonitorEventList { |
| 73 | + kind: "monitor_event_list"; |
| 74 | + monitorId: string; |
| 75 | + events: MonitorEvent[]; |
| 76 | + nextCursor: string | null; |
| 77 | +} |
| 78 | +export interface MonitorCheck { |
| 79 | + kind: "monitor_check"; |
| 80 | + monitor: Monitor; |
| 81 | + newVideos: number; |
| 82 | + event: MonitorEvent | null; |
| 83 | + /** An HTTP 200 check may still report a listing failure. */ |
| 84 | + error: ApiErrorBlock | null; |
| 85 | + usage: Usage | null; |
| 86 | +} |
| 87 | + |
| 88 | +type Wire = Record<string, unknown>; |
| 89 | +const obj = (v: unknown): Wire => v && typeof v === "object" ? v as Wire : {}; |
| 90 | +const nullable = (v: unknown): string | null => typeof v === "string" ? v : null; |
| 91 | +const num = (v: unknown): number => typeof v === "number" ? v : 0; |
| 92 | +const arr = (v: unknown): unknown[] => Array.isArray(v) ? v : []; |
| 93 | +function snapshot(raw: unknown): MonitorSnapshot { |
| 94 | + const d = obj(raw); |
| 95 | + return { id: String(d.id ?? ""), name: nullable(d.name), type: "channel", platform: String(d.platform ?? ""), target: String(d.target ?? "") }; |
| 96 | +} |
| 97 | +export function normalizeMonitor(env: Wire): Monitor { |
| 98 | + const d = obj(env.data); |
| 99 | + const error = normalizeErrorBlock(d.last_error); |
| 100 | + return { |
| 101 | + ...snapshot(d), kind: "monitor", options: obj(d.options) as Monitor["options"], |
| 102 | + status: d.status as Monitor["status"], hasNew: d.has_new === true, lastEventId: nullable(d.last_event_id), |
| 103 | + intervalMinutes: num(d.interval_minutes) as MonitorInterval, transcripts: d.transcripts === true, |
| 104 | + webhookUrl: nullable(d.webhook_url), nextCheckAt: nullable(d.next_check_at), lastCheckedAt: nullable(d.last_checked_at), |
| 105 | + lastError: error ? { ...error, at: nullable(obj(d.last_error).at) } : null, |
| 106 | + createdAt: String(d.created_at ?? ""), updatedAt: String(d.updated_at ?? ""), |
| 107 | + webhookSecret: nullable(d.webhook_secret), baselineCount: typeof d.baseline_count === "number" ? d.baseline_count : null, |
| 108 | + usage: normalizeUsage(env), |
| 109 | + }; |
| 110 | +} |
| 111 | +export function normalizeMonitorList(env: Wire): MonitorList { |
| 112 | + const d = obj(env.data), limits = obj(d.limits); |
| 113 | + return { |
| 114 | + kind: "monitor_list", monitors: arr(d.monitors).map(data => normalizeMonitor({ data })), |
| 115 | + hasNew: d.has_new === true, lastEventId: nullable(d.last_event_id), |
| 116 | + limits: { maxMonitors: typeof limits.max_monitors === "number" ? limits.max_monitors : null, minIntervalMinutes: num(limits.min_interval_minutes) }, |
| 117 | + }; |
| 118 | +} |
| 119 | +function transcriptResult(raw: unknown): MonitorTranscriptResult { |
| 120 | + const d = obj(raw); |
| 121 | + return { |
| 122 | + videoId: String(d.video_id ?? ""), url: String(d.url ?? ""), outcome: d.outcome as MonitorTranscriptResult["outcome"], |
| 123 | + jobId: nullable(d.job_id), |
| 124 | + transcript: d.transcript ? normalizeTranscript({ data: d.transcript }) : null, |
| 125 | + error: normalizeErrorBlock(d.error), |
| 126 | + }; |
| 127 | +} |
| 128 | +export function normalizeMonitorEvent(raw: unknown): MonitorEvent { |
| 129 | + const e = obj(raw), d = obj(e.data), delivery = obj(e.delivery); |
| 130 | + const base: MonitorEventBase = { |
| 131 | + id: String(e.id ?? ""), createdAt: String(e.created_at ?? ""), monitor: snapshot(e.monitor), creditsSpent: num(e.credits_spent), |
| 132 | + delivery: e.delivery ? { |
| 133 | + status: delivery.status as MonitorDelivery["status"], attempts: num(delivery.attempts), |
| 134 | + lastAttemptAt: nullable(delivery.last_attempt_at), nextAttemptAt: nullable(delivery.next_attempt_at), |
| 135 | + lastError: nullable(delivery.last_error), deliveredAt: nullable(delivery.delivered_at), |
| 136 | + } : null, |
| 137 | + }; |
| 138 | + return e.type === "monitor.videos" |
| 139 | + ? { ...base, type: "monitor.videos", data: { videos: arr(d.videos).map(normalizeVideo), ...(Array.isArray(d.transcripts) ? { transcripts: d.transcripts.map(transcriptResult) } : {}) } } |
| 140 | + : { ...base, type: "monitor.transcript", data: { ...transcriptResult(d), videosEventId: String(d.videos_event_id ?? "") } }; |
| 141 | +} |
| 142 | +export function normalizeMonitorEvents(env: Wire): MonitorEventList { |
| 143 | + const d = obj(env.data); |
| 144 | + return { kind: "monitor_event_list", monitorId: String(d.monitor_id ?? ""), events: arr(d.events).map(normalizeMonitorEvent), nextCursor: nullable(d.next_cursor) }; |
| 145 | +} |
| 146 | +export function normalizeMonitorCheck(env: Wire): MonitorCheck { |
| 147 | + const d = obj(env.data); |
| 148 | + return { kind: "monitor_check", monitor: normalizeMonitor({ data: d.monitor }), newVideos: num(d.new_videos), event: d.event ? normalizeMonitorEvent(d.event) : null, error: normalizeErrorBlock(d.error), usage: normalizeUsage(env) }; |
| 149 | +} |
0 commit comments