Skip to main content

Channel Status

Overview​

The channel status module tracks live stream status per platform for each account. It stores whether a channel is online, along with metadata such as stream title, category, viewer count, and stream start time. Status is updated by platform workers (Twitch, YouTube, Kick) and includes a manual Spotify connect feature for controlling the Spotify polling worker independently of stream status.

Architecture​

Platform Workers (Twitch/YouTube/Kick)
|
+--> db::channel_status::set_online / set_offline / update_stream_data
|
v
channel_status table (PostgreSQL)
^
|
Dashboard UI
|
v
Next.js API Proxy
|
v
GraphQL (ChannelStatusQuery / ChannelStatusMutation)
|
+--> db::channel_status::get_status
+--> services::channel_status (manual connect via Redis)

Status Updates​

Platform workers poll for stream status changes and update the database:

  • Going online -- set_online() upserts a row with is_online = TRUE and metadata (title, category, viewer count, start time).
  • Going offline -- set_offline() sets is_online = FALSE and clears started_at.
  • Data updates -- update_stream_data() updates viewer count, title, and category for channels that are already online.

Spotify Manual Connect​

The Spotify worker normally polls only when a channel is online. The manual connect feature allows users to start Spotify polling independently, using a Redis key with a 30-minute TTL. This is useful for pre-stream setup or listening sessions without going live.

  • Start -- Sets a Redis key and publishes a synthetic status change event via Redis pub/sub.
  • Stop -- Deletes the Redis key and publishes a status change event.
  • Query -- Returns whether manual connect is active and the remaining seconds.

Spotify Worker Lifecycle (relay)​

The channel-status relay (apps/api/src/workers/channel_status_relay.rs) owns the Spotify worker's start/stop lifecycle. It subscribes to lumio:channel_status:* and, on every status change, evaluates whether the worker should run for that account:

  • Should run when any channel is online or a manual connect is active. On a should-run transition the relay resolves the worker config from the live channel_connections row on demand (resolve_spotify_config) and starts the worker with the connection's current connection_id. There is no boot-time config snapshot, so a Spotify connection linked, re-authorized, or disconnected-and-reconnected after the API booted starts polling on the next stream-start or manual connect -- without an API restart. An account with no Spotify connection resolves nothing and is simply not started.
  • Should not run (no channel online, no manual connect) tears the worker down. A separate 60-second poll also stops any worker whose manual connect has expired.

This is why stopping the worker at stream end / manual-connect expiry is safe: it always restarts from the DB on the next trigger. Note the emission asymmetry: only the worker produces spotify:track; spotify:skip/play/pause/… come from the playback-control path and do not depend on the worker running.

API​

GraphQL Queries​

QueryArgsReturnsPermission
channelStatus--[ChannelStatus]connections:read
channelSchedulefrom: DateTime, to: DateTime[ScheduledStream!]!connections:read
spotifyManualStatus--SpotifyManualStatusspotify:read
  • channelStatus returns all platform statuses for the active account.
  • channelSchedule returns the stream plan of the account's connected channels — see Stream plan.
  • spotifyManualStatus checks the Redis TTL key and returns active status with remaining seconds.

GraphQL Mutations​

MutationArgsReturnsPermission
startSpotifyManual--SpotifyManualStatusspotify:worker
stopSpotifyManual--Booleanspotify:worker

REST Endpoints​

All paths live under /v1.

MethodPathPermissionDescription
GET/v1/channel-statusconnections:readAll platform statuses for the active account
GET/v1/channels/schedule?from=&to=connections:readStream plan of the connected channels (twin of channelSchedule, body { "data": [ScheduledStream] } in snake_case)
GET/v1/spotify/manual-connectspotify:readManual-connect status + remaining seconds
POST/v1/spotify/manual-connectspotify:workerStart manual Spotify polling (30 min TTL)
DELETE/v1/spotify/manual-connectspotify:workerStop manual Spotify polling

GraphQL Types​

type ChannelStatus {
platform: String!
broadcastId: String!
isOnline: Boolean!
broadcastStatus: String
streamTitle: String
category: String
viewerCount: Int
likeCount: Int
totalViews: Int
startedAt: DateTime
scheduledStart: DateTime
liveChatId: String
updatedAt: DateTime!
}

type ScheduledStream {
platform: String! # youtube | twitch | kick
title: String! # "" when the platform has none
category: String
startsAt: DateTime!
endsAt: DateTime # null when the platform has no end (YouTube always)
isRecurring: Boolean!
url: String! # YouTube watch page, Twitch/Kick channel page
source: String! # youtube_broadcast | twitch_schedule | kick_schedule
}

type SpotifyManualStatus {
active: Boolean!
remainingSeconds: Int
}

Stream plan​

channelSchedule(from, to) / GET /v1/channels/schedule?from=&to= lists the planned broadcasts of the account's connected channels, starting inside from..to and sorted by startsAt. Both protocols share the guard (connections:read), the validation and the messages:

  • from defaults to now, to to from + 30 days (RFC 3339).
  • Bad request: `to` must be after `from` when to <= from; Bad request: The window from `from` to `to` must not exceed 60 days when the window is longer. Code BAD_REQUEST on both protocols; REST answers 400.
PlatformSourcesourceNotes
YouTubechannel_status rows the YouTube worker writes for discovered public scheduled broadcasts (broadcast_status = "upcoming", scheduled_start)youtube_broadcastOne entry per scheduled broadcast with a known start time; a chat ID is not required. url is the watch page; no end time; never recurring. A broadcast that went live becomes a live row and drops out.
TwitchHelix GET /schedule (official, no scope) with the channel connection's tokentwitch_scheduleRecurring segments arrive expanded. Cancelled occurrences (canceled_until) and segments inside the vacation window are dropped. No schedule (404) is an empty plan.
Kickweb.kick.com/api/v1/channels/{channel_id}/calendar/events?from_utc=&to_utc= — undocumented, the data behind the "Schedule" tab on kick.com; no tokenkick_scheduleThe official Kick API has no schedule endpoint. Recurring events arrive expanded (event_type = RECURRING). The kick.com channel id is looked up from the slug (channel_login). A format change or an unknown channel is an empty plan.

Twitch and Kick are fetched for the next 61 days and cached per channel in Redis (lumio:channel_schedule:{platform}:{channel_id}) for 10 minutes; a failed fetch is cached as an empty plan for 2 minutes. Entries later than 61 days from the fetch are not returned. Every failure (platform down, no usable token, reconnect required, Redis unavailable) only empties that channel — it never fails the list. Tokens refresh only through get_fresh_connection_token.

While a platform is live, the plan entry covering the current time on that platform is left out (startsAt <= now < endsAt; without an end, any past start). There is no WebSocket channel for the plan: clients reload it on page load and on the platform online/offline events. Reading the plan emits no audit event. The counter channel_schedule_fetches_total{platform, outcome} (cache_hit, ok, no_token, no_slug, error) is exposed on the internal /metrics.

Permissions​

PermissionDescription
connections:readView channel status and the stream plan
spotify:readView the Spotify manual connect status
spotify:workerStart/stop Spotify manual connect

Database​

Table: channel_status​

ColumnTypeDescription
account_idUUID (PK, FK)Account ID
platformTEXT (PK)Platform identifier
broadcast_idTEXT (PK)Broadcast/stream identifier (per-broadcast rows for multi-stream)
is_onlineBOOLEANWhether the channel is currently live
broadcast_statusTEXT NULL"live", "upcoming", or NULL (non-YouTube platforms)
stream_titleTEXTCurrent stream title
categoryTEXTCurrent stream category/game
viewer_countINTCurrent viewer count
like_countINT NULLCurrent like count (YouTube only)
total_viewsBIGINT NULLLifetime view count (YouTube only)
started_atTIMESTAMPTZWhen the stream started
scheduled_startTIMESTAMPTZ NULLWhen an upcoming broadcast is scheduled to start
live_chat_idTEXT NULLYouTube live chat ID for this broadcast
updated_atTIMESTAMPTZLast status update

Primary key: (account_id, platform, broadcast_id) -- one status row per broadcast per platform per account, using upsert on conflict.

DB Functions​

FunctionDescription
set_onlineUpsert with is_online = TRUE and metadata (title, viewer count, like count, total views, scheduled start, live chat ID)
set_offlineSet is_online = FALSE, clear started_at
upsert_upcomingUpsert an upcoming broadcast (is_online = FALSE) with title and scheduled start when available; the live chat ID may be absent until resolution succeeds
delete_broadcastDelete a specific broadcast row (used when a broadcast disappears from discovery)
get_statusGet all statuses for an account, ordered by platform
is_any_onlineCheck if any platform is online (DB fallback)
update_stream_dataUpdate viewer count, title, category for online channels
get_all_online_accountsBoot recovery: all accounts with online channels, grouped

Redis Keys​

KeyTTLDescription
lumio:spotify_manual:{account_id}30 minManual Spotify connect active flag
lumio:channel_schedule:{platform}:{channel_id}10 min (failed fetch: 2 min)Cached Twitch/Kick stream plan

Key Files​

FilePurpose
apps/api/src/graphql/channel_status.rsGraphQL queries and mutations
apps/api/src/db/channel_status.rsDatabase operations
apps/api/src/services/channel_status.rsManual connect logic (Redis TTL, pub/sub)
apps/api/src/services/channel_schedule.rsStream plan: sources, window, cache