import { EmbedBuilder, TextChannel } from 'discord.js'; import { ExtendedClient } from './ExtendedClient.js'; export class TwitchManager { private static accessToken: string | null = null; private static tokenExpires: number = 0; private static async getAccessToken() { if (this.accessToken && Date.now() < this.tokenExpires) return this.accessToken; const clientId = process.env.TWITCH_CLIENT_ID; const clientSecret = process.env.TWITCH_CLIENT_SECRET; if (!clientId || !clientSecret) { console.error('[TWITCH] Missing Client ID or Secret in .env'); return null; } try { const response = await fetch(`https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, { method: 'POST' }); const data: any = await response.json(); this.accessToken = data.access_token; this.tokenExpires = Date.now() + (data.expires_in * 1000) - 60000; return this.accessToken; } catch (error) { console.error('[TWITCH] Error getting access token:', error); return null; } } /** * 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 || 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?${query}`, { headers: { 'Client-ID': process.env.TWITCH_CLIENT_ID!, 'Authorization': `Bearer ${token}` } }); const data: any = await response.json(); return data.data || []; } catch (error) { console.error(`[TWITCH] Error fetching batch (${channelNames.length} channels):`, error); 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: ExtendedClient) { const monitors: any[] = client.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(); // 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); } } const updates: { id: number, status: 'online' | 'offline', streamId?: string }[] = []; for (const monitor of monitors) { const stream = onlineStreamsMap.get(monitor.channel_name.toLowerCase()); if (stream) { const isNewStream = monitor.last_stream_id !== stream.id; const wasOffline = monitor.last_status === 'offline'; if (wasOffline || (monitor.last_status === 'online' && isNewStream)) { await this.sendNotification(client, monitor, stream); updates.push({ id: monitor.id, status: 'online', streamId: stream.id }); } } else { if (monitor.last_status === 'online') { updates.push({ id: monitor.id, status: 'offline' }); } } } if (updates.length > 0) { client.DB?.transaction(() => { for (const update of updates) { if (update.status === 'online') { client.DB?.run( "UPDATE twitch_monitors SET last_status = 'online', last_stream_id = ? WHERE id = ?", update.streamId, update.id ); } else { client.DB?.run( "UPDATE twitch_monitors SET last_status = 'offline', last_stream_id = NULL WHERE id = ?", update.id ); } } }); } } private static async sendNotification(client: ExtendedClient, monitor: any, stream: any) { 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 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(); 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); } } static startPolling(client: any) { // Polling every 2 minutes for faster notifications setInterval(() => this.checkStreams(client), 2 * 60 * 1000); console.log('[TWITCH] Batch-Polling started (2m interval).'); } }