Optimize Twitch polling with batching and implement database caching with WAL mode

This commit is contained in:
2026-04-28 20:42:34 +02:00
parent 21c3b01e64
commit b2c3f5731b
12 changed files with 240 additions and 614 deletions

View File

@@ -29,35 +29,58 @@ export class TwitchManager {
}
}
static async fetchStreamData(channelName: string) {
/**
* Fetches stream data for multiple channels in a single batch (max 100).
*/
private static async fetchStreamsBatch(channelNames: string[]) {
const token = await this.getAccessToken();
if (!token) return null;
if (!token || channelNames.length === 0) return [];
// Twitch API allows up to 100 user_login parameters
const query = channelNames.map(name => `user_login=${encodeURIComponent(name)}`).join('&');
try {
const response = await fetch(`https://api.twitch.tv/helix/streams?user_login=${channelName}`, {
const response = await fetch(`https://api.twitch.tv/helix/streams?${query}`, {
headers: {
'Client-ID': process.env.TWITCH_CLIENT_ID!,
'Authorization': `Bearer ${token}`
}
});
const data: any = await response.json();
return data.data?.[0] || null;
return data.data || [];
} catch (error) {
console.error(`[TWITCH] Error fetching stream data for ${channelName}:`, error);
return null;
console.error(`[TWITCH] Error fetching batch (${channelNames.length} channels):`, error);
return [];
}
}
static async checkStreams(client: any) {
const token = await this.getAccessToken();
if (!token) return;
/**
* Legacy helper for single channel fetch (e.g. for commands)
*/
static async fetchStreamData(channelName: string) {
const streams = await this.fetchStreamsBatch([channelName]);
return streams[0] || null;
}
static async checkStreams(client: any) {
const monitors: any[] = (client as any).DB?.all('SELECT * FROM twitch_monitors') || [];
if (monitors.length === 0) return;
const uniqueChannels = [...new Set(monitors.map(m => m.channel_name.toLowerCase()))];
const onlineStreamsMap = new Map<string, any>();
// Process in chunks of 100 (Twitch API limit)
for (let i = 0; i < uniqueChannels.length; i += 100) {
const chunk = uniqueChannels.slice(i, i + 100);
const streams = await this.fetchStreamsBatch(chunk);
for (const stream of streams) {
onlineStreamsMap.set(stream.user_login.toLowerCase(), stream);
}
}
for (const monitor of monitors) {
try {
const stream = await this.fetchStreamData(monitor.channel_name);
const stream = onlineStreamsMap.get(monitor.channel_name.toLowerCase());
if (stream) {
const isNewStream = monitor.last_stream_id !== stream.id;
@@ -77,44 +100,49 @@ export class TwitchManager {
}
}
} catch (error) {
console.error(`[TWITCH] Error checking stream ${monitor.channel_name}:`, error);
console.error(`[TWITCH] Error processing monitor ${monitor.channel_name}:`, error);
}
}
}
private static async sendNotification(client: any, monitor: any, stream: any) {
const guild = client.guilds.cache.get(monitor.guild_id);
if (!guild || !monitor.discord_channel_id) return;
try {
const guild = client.guilds.cache.get(monitor.guild_id);
if (!guild || !monitor.discord_channel_id) return;
const channel = guild.channels.cache.get(monitor.discord_channel_id) as TextChannel;
if (!channel) return;
const channel = guild.channels.cache.get(monitor.discord_channel_id) as TextChannel;
if (!channel) return;
const thumbnailUrl = stream.thumbnail_url
.replace('{width}', '1280')
.replace('{height}', '720') + `?t=${Date.now()}`;
const thumbnailUrl = stream.thumbnail_url
.replace('{width}', '1280')
.replace('{height}', '720') + `?t=${Date.now()}`;
const embed = new EmbedBuilder()
.setTitle(`${stream.user_name} ist jetzt LIVE auf Twitch!`)
.setURL(`https://twitch.tv/${stream.user_login}`)
.setColor(0x6441a5)
.setImage(thumbnailUrl)
.addFields(
{ name: 'Titel', value: stream.title || 'Kein Titel', inline: false },
{ name: 'Kategorie', value: stream.game_name || 'Unbekannt', inline: true },
{ name: 'Zuschauer', value: stream.viewer_count.toString(), inline: true }
)
.setTimestamp();
const embed = new EmbedBuilder()
.setTitle(`${stream.user_name} ist jetzt LIVE auf Twitch!`)
.setURL(`https://twitch.tv/${stream.user_login}`)
.setColor(0x6441a5)
.setImage(thumbnailUrl)
.addFields(
{ name: 'Titel', value: stream.title || 'Kein Titel', inline: false },
{ name: 'Kategorie', value: stream.game_name || 'Unbekannt', inline: true },
{ name: 'Zuschauer', value: stream.viewer_count.toString(), inline: true }
)
.setTimestamp();
let content = `📢 **${stream.user_name}** ist online!`;
if (monitor.custom_message) {
content += `\n\n${monitor.custom_message}`;
let content = `📢 **${stream.user_name}** ist online!`;
if (monitor.custom_message) {
content += `\n\n${monitor.custom_message}`;
}
await channel.send({ content, embeds: [embed] });
} catch (error) {
console.error(`[TWITCH] Failed to send notification for ${monitor.channel_name}:`, error);
}
await channel.send({ content, embeds: [embed] });
}
static startPolling(client: any) {
setInterval(() => this.checkStreams(client), 5 * 60 * 1000);
console.log('[TWITCH] Polling started (5m interval).');
// Polling every 2 minutes for faster notifications
setInterval(() => this.checkStreams(client), 2 * 60 * 1000);
console.log('[TWITCH] Batch-Polling started (2m interval).');
}
}