Some checks failed
Auto Build and Push Docker Image / build (push) Failing after 14s
162 lines
6.5 KiB
TypeScript
162 lines
6.5 KiB
TypeScript
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<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);
|
|
}
|
|
}
|
|
|
|
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).');
|
|
}
|
|
} |