StreamPoller
Service that periodically checks all tracked streamers for live status changes and triggers alerts.
import { StreamPoller, createStreamPoller } from './services/StreamPoller.js';
import { Client } from 'discord.js';
const client = new Client({ /* ... */ });
const poller = new StreamPoller(client);
Constructor
Creates a new StreamPoller instance.
const poller = new StreamPoller(client);
Signature:
constructor(client: Client)
Discord.js Client instance for fetching channels and sending alerts.
Methods
start()
Starts the polling loop to check streamers at regular intervals.
Signature:
Behavior:
- Runs an immediate poll on start
- Sets up interval polling (default: 60 seconds from
POLL_INTERVAL)
- Logs start message to console
- Prevents duplicate polling if already running
Example:
import { createStreamPoller } from './services/StreamPoller.js';
import { POLL_INTERVAL } from './utils/constants.js';
const poller = createStreamPoller(client);
poller.start();
console.log(`Polling every ${POLL_INTERVAL / 1000} seconds`);
stop()
Stops the polling loop.
Signature:
Behavior:
- Clears the polling interval
- Sets running state to false
- Logs stop message to console
Example:
// Stop polling on shutdown
process.on('SIGINT', () => {
poller.stop();
process.exit(0);
});
Internal Methods
poll() (private)
Runs a single poll cycle for all guilds with tracked streamers.
Process:
- Fetches all guilds with streamers from database
- Iterates through each guild
- Checks each streamer’s live status
- Processes status changes and sends alerts
- Updates streamer data in database
Example flow:
const guilds = getAllGuildsWithStreamers();
// Returns: [{ guildId: '123', streamers: [...] }, ...]
for (const { guildId, streamers } of guilds) {
await this.checkGuildStreamers(guildId, streamers);
}
checkGuildStreamers() (private)
Checks all streamers for a specific guild.
Signature:
private async checkGuildStreamers(
guildId: string,
streamers: Streamer[]
): Promise<void>
Array of streamers to check
Process:
const updatedStreamers: Streamer[] = [];
for (const streamer of streamers) {
const status = await this.checkStreamer(streamer);
const updated = await this.processStatus(guildId, streamer, status);
updatedStreamers.push(updated);
}
updateStreamers(guildId, updatedStreamers);
checkStreamer() (private)
Checks a single streamer’s live status using the appropriate platform checker.
Signature:
private async checkStreamer(streamer: Streamer): Promise<LiveStatus>
Streamer object from database
Returns: LiveStatus object from platform checker
Example:
const checker = getChecker(streamer.platform);
const status = await checker(streamer.username);
logger.platform(streamer.platform, streamer.username, status.isLive);
return status;
processStatus() (private)
Processes a status update, sends alerts if needed, and returns updated streamer data.
Signature:
private async processStatus(
guildId: string,
streamer: Streamer,
status: LiveStatus
): Promise<Streamer>
New live status from platform check
Updated streamer object with new live data
Alert Logic:
const cacheKey = `${guildId}-${streamer.id}`;
const lastData = lastLiveData.get(cacheKey);
// Send alert if:
// - Now live AND (wasn't live before OR title changed)
const shouldAlert =
status.isLive &&
(!lastData?.isLive || (status.title && lastData.title !== status.title));
if (shouldAlert) {
await sendLiveAlert(this.client, streamer.channelId, status);
lastLiveData.set(cacheKey, { title: status.title, isLive: true });
}
Cache Cleanup:
// Clean up cache when stream ends
if (!status.isLive && lastData?.isLive) {
lastLiveData.delete(cacheKey);
}
Data Merging:
return {
...streamer,
isLive: status.isLive,
lastLiveAt: status.isLive ? new Date().toISOString() : streamer.lastLiveAt,
title: status.title ?? streamer.title,
viewers: status.viewers ?? streamer.viewers,
followers: status.followers ?? streamer.followers,
thumbnail: status.thumbnail ?? streamer.thumbnail,
profileImage: status.profileImage ?? streamer.profileImage,
startedAt: status.startedAt ?? streamer.startedAt,
verified: status.verified ?? streamer.verified,
bio: status.bio ?? streamer.bio,
};
createStreamPoller()
Factory function to create a StreamPoller instance.
import { createStreamPoller } from './services/StreamPoller.js';
const poller = createStreamPoller(client);
Signature:
export function createStreamPoller(client: Client): StreamPoller
Discord.js Client instance
New StreamPoller instance
AlertService
Service class for sending live stream notifications to Discord channels.
import { AlertService } from './services/AlertService.js';
const alertService = new AlertService(client);
Constructor
const service = new AlertService(client);
Signature:
constructor(client: Client)
Discord.js Client instance
Methods
sendAlert()
Sends a live alert to a Discord channel.
const success = await alertService.sendAlert(channelId, status);
Signature:
async sendAlert(channelId: string, status: LiveStatus): Promise<boolean>
Discord channel ID where the alert will be sent
Live status object containing stream information
true if alert was sent successfully, false otherwise
Example:
import { AlertService } from './services/AlertService.js';
import { checkTwitchLive } from './platforms/twitch.js';
const service = new AlertService(client);
const status = await checkTwitchLive('xqc');
if (status.isLive) {
const sent = await service.sendAlert('123456789', status);
console.log(`Alert sent: ${sent}`);
}
sendLiveAlert()
Standalone function to send a live alert.
import { sendLiveAlert } from './services/AlertService.js';
const success = await sendLiveAlert(client, channelId, status);
Signature:
export async function sendLiveAlert(
client: Client,
channelId: string,
status: LiveStatus
): Promise<boolean>
Discord.js Client instance
true if successful, false if failed
Process:
- Fetches the Discord channel
- Validates channel exists and is text-based
- Creates live embed with stream info
- Creates “Watch Now” button
- Sends message with embed and button
- Logs success/failure
Implementation:
try {
const channel = await client.channels.fetch(channelId);
if (!channel || !channel.isTextBased()) {
logger.warn(`Channel ${channelId} not found or not text-based`);
return false;
}
const embed = createLiveEmbed(status);
const row = createWatchButtonRow(status.url, status.platform);
await (channel as TextChannel).send({
embeds: [embed],
components: [row],
});
logger.info(
`Sent live alert for ${status.username} (${status.platform}) to channel ${channelId}`
);
return true;
} catch (error) {
logger.error(`Failed to send alert to channel ${channelId}:`, error);
return false;
}
Full Usage Example
import { Client, GatewayIntentBits } from 'discord.js';
import { createStreamPoller } from './services/StreamPoller.js';
import { AlertService } from './services/AlertService.js';
import { addStreamer, createStreamerId } from './database/index.js';
import { TOKEN } from './config.js';
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
});
// Create services
const poller = createStreamPoller(client);
const alerts = new AlertService(client);
client.once('ready', () => {
console.log(`Logged in as ${client.user?.tag}`);
// Add a streamer to track
const streamer = {
id: createStreamerId('twitch', 'xqc'),
platform: 'twitch' as const,
username: 'xqc',
channelId: '123456789', // Discord channel ID
isLive: false,
};
addStreamer('guild_123', streamer);
// Start polling
poller.start();
console.log('Stream poller started');
});
// Manual alert example
client.on('messageCreate', async (message) => {
if (message.content === '!test-alert') {
const testStatus = {
isLive: true,
platform: 'twitch' as const,
username: 'xqc',
title: 'TEST STREAM',
viewers: 50000,
url: 'https://twitch.tv/xqc',
};
await alerts.sendAlert(message.channel.id, testStatus);
}
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('Stopping poller...');
poller.stop();
process.exit(0);
});
await client.login(TOKEN);