🤖 Bot de Alertas de Conciertos con Telegram

Bot que avisa cuando tu artista favorito anuncia fecha cerca de ti

🎸 Demo del Bot

🤖 Configuración del Bot de Telegram

⚙️ Opciones de Notificación

📅 Antelación
🎭 Géneros
📍 Ubicación
🔔 Frecuencia
🤖

ConciertoBot

Bot
🎸 ¡Nuevo Concierto Anunciado!

Artista: Metallica
Fecha: 15 de junio, 2024
Lugar: Estadio Wanda Metropolitano, Madrid
Precio: Desde €65

📍 Distancia: 12 km de tu ubicación

Comprar Entradas →

🤖 Código del Bot de Telegram (Node.js)

const TelegramBot = require('node-telegram-bot-api'); const axios = require('axios'); const cron = require('node-cron'); // Configuración const config = { telegramToken: 'TU_TOKEN_AQUI', lastFmApiKey: 'TU_API_KEY_LASTFM', songkickApiKey: 'TU_API_KEY_SONGKICK', googleMapsApiKey: 'TU_API_KEY_GOOGLE_MAPS', adminUserId: 'TU_USER_ID' }; // Inicializar bot const bot = new TelegramBot(config.telegramToken, { polling: true }); // Base de datos simple (en producción usa MongoDB o similar) const users = new Map(); const concerts = new Map(); // Comando /start bot.onText(/\/start/, (msg) => { const chatId = msg.chat.id; const welcomeMessage = ` 🤖 *Bienvenido a ConciertoBot!* Te avisaré cuando tus artistas favoritos tengan conciertos cerca de ti. *Comandos disponibles:* • /register - Registrarse • /addartist [nombre] - Añadir artista • /removeartist [nombre] - Eliminar artista • /listartists - Ver tus artistas • /setlocation [ciudad] - Establecer ubicación • /settings - Ver configuración • /help - Ayuda `; bot.sendMessage(chatId, welcomeMessage, { parse_mode: 'Markdown' }); }); // Comando /register bot.onText(/\/register/, (msg) => { const chatId = msg.chat.id; users.set(chatId, { id: chatId, username: msg.from.username, artists: [], location: null, radius: 100, // km advanceNotice: 14, // días genres: ['metal', 'rock', 'punk'], active: true, createdAt: new Date() }); bot.sendMessage(chatId, '✅ Registro completado. Usa /addartist para añadir tus artistas favoritos.'); }); // Comando /addartist bot.onText(/\/addartist (.+)/, async (msg, match) => { const chatId = msg.chat.id; const artistName = match[1].trim(); const user = users.get(chatId); if (!user) { return bot.sendMessage(chatId, '❌ Primero debes registrarte con /register'); } try { // Buscar artista en Last.fm const artistInfo = await searchArtist(artistName); if (artistInfo) { user.artists.push({ name: artistInfo.name, mbid: artistInfo.mbid, image: artistInfo.image }); bot.sendMessage(chatId, `✅ Artista añadido: *${artistInfo.name}*`, { parse_mode: 'Markdown' }); // Buscar conciertos inmediatamente await checkArtistConcerts(artistInfo, user); } else { bot.sendMessage(chatId, '❌ No se encontró el artista. Verifica el nombre.'); } } catch (error) { console.error('Error adding artist:', error); bot.sendMessage(chatId, '❌ Error al añadir el artista.'); } }); // Función para buscar artistas async function searchArtist(artistName) { try { const response = await axios.get('https://ws.audioscrobbler.com/2.0/', { params: { method: 'artist.search', artist: artistName, api_key: config.lastFmApiKey, format: 'json', limit: 1 } }); const artists = response.data.results?.artistmatches?.artist; if (artists && artists.length > 0) { return { name: artists[0].name, mbid: artists[0].mbid, image: artists[0].image?.[3]?.['#text'] }; } return null; } catch (error) { console.error('Error searching artist:', error); return null; } } // Función para verificar conciertos async function checkArtistConcerts(artist, user) { try { // Buscar en Songkick const songkickResponse = await axios.get(`https://api.songkick.com/api/3.0/events.json`, { params: { apikey: config.songkickApiKey, artist_name: artist.name, location: 'clientip' // Usar ubicación del usuario } }); const events = songkickResponse.data.resultsPage?.results?.event || []; for (const event of events) { const concertKey = `${artist.mbid}-${event.id}`; if (!concerts.has(concertKey)) { concerts.set(concertKey, { id: event.id, artist: artist.name, venue: event.venue.displayName, city: event.location.city, date: event.start.date, time: event.start.time, url: event.uri, lat: event.venue.lat, lng: event.venue.lng }); // Calcular distancia const distance = await calculateDistance( user.location, { lat: event.venue.lat, lng: event.venue.lng } ); // Si está dentro del radio, enviar notificación if (distance <= user.radius) { await sendConcertAlert(user, artist, event, distance); } } } } catch (error) { console.error('Error checking concerts:', error); } } // Función para enviar alerta de concierto async function sendConcertAlert(user, artist, event, distance) { const message = ` 🎸 *¡Nuevo Concierto de ${artist.name}!* 📅 *Fecha:* ${new Date(event.start.date).toLocaleDateString('es-ES')} 🕐 *Hora:* ${event.start.time || 'Por confirmar'} 📍 *Lugar:* ${event.venue.displayName}, ${event.location.city} 🎫 *Entradas:* [Comprar aquí](${event.uri}) 📏 *Distancia:* ${distance.toFixed(1)} km ¿Te interesa este concierto? `; const options = { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [[ { text: '✅ Sí, avísame', callback_data: `interested_${event.id}` }, { text: '❌ No, gracias', callback_data: `not_interested_${event.id}` } ]] } }; await bot.sendMessage(user.id, message, options); } // Función para calcular distancia async function calculateDistance(location1, location2) { // Si no hay ubicación del usuario, usar Madrid como default if (!location1 || !location1.lat || !location1.lng) { location1 = { lat: 40.4168, lng: -3.7038 }; // Madrid } // Fórmula de Haversine const R = 6371; // Radio de la Tierra en km const dLat = (location2.lat - location1.lat) * Math.PI / 180; const dLng = (location2.lng - location1.lng) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(location1.lat * Math.PI / 180) * Math.cos(location2.lat * Math.PI / 180) * Math.sin(dLng/2) * Math.sin(dLng/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); const distance = R * c; return distance; } // Tarea programada para verificar conciertos cron.schedule('0 */6 * * *', async () => { console.log('Running scheduled concert check...'); for (const [userId, user] of users) { if (user.active && user.artists.length > 0) { for (const artist of user.artists) { await checkArtistConcerts(artist, user); } } } }); console.log('🤖 ConciertoBot iniciado');

🗄️ Base de Datos y API Integration

// MongoDB Schema (Mongoose) const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ telegramId: { type: String, unique: true, required: true }, username: String, firstName: String, lastName: String, location: { city: String, country: String, coordinates: { lat: Number, lng: Number } }, preferences: { radius: { type: Number, default: 100 }, // km advanceNotice: { type: Number, default: 14 }, // days genres: [{ type: String, enum: ['metal', 'rock', 'punk', 'industrial', 'alternative'] }], notificationFrequency: { type: String, enum: ['immediate', 'daily', 'weekly'], default: 'weekly' } }, artists: [{ name: String, mbid: String, // MusicBrainz ID image: String, addedAt: { type: Date, default: Date.now } }], alerts: [{ concertId: String, sentAt: { type: Date, default: Date.now }, status: { type: String, enum: ['sent', 'interested', 'not_interested', 'attending'], default: 'sent' } }], active: { type: Boolean, default: true }, createdAt: { type: Date, default: Date.now }, lastActivity: { type: Date, default: Date.now } }); const concertSchema = new mongoose.Schema({ externalId: String, artist: { name: String, mbid: String, image: String }, venue: { name: String, address: String, city: String, country: String, coordinates: { lat: Number, lng: Number } }, date: Date, time: String, ticketUrl: String, price: { min: Number, max: Number, currency: String }, attendees: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, status: { type: String, enum: ['interested', 'attending', 'not_interested'] }, notifiedAt: { type: Date, default: Date.now } }], source: { type: String, enum: ['songkick', 'lastfm', 'bandsintown', 'ticketmaster'] }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); // API Integration Service class ConcertService { constructor() { this.apis = { songkick: { baseUrl: 'https://api.songkick.com/api/3.0', key: process.env.SONGKICK_API_KEY }, bandsintown: { baseUrl: 'https://rest.bandsintown.com', key: process.env.BANDSINTOWN_API_KEY }, ticketmaster: { baseUrl: 'https://app.ticketmaster.com/discovery/v2', key: process.env.TICKETMASTER_API_KEY } }; } async searchConcerts(artist, location, radius = 100) { const concerts = []; // Buscar en múltiples APIs const promises = [ this.searchSongkick(artist, location, radius), this.searchBandsintown(artist, location, radius), this.searchTicketmaster(artist, location, radius) ]; const results = await Promise.allSettled(promises); for (const result of results) { if (result.status === 'fulfilled' && result.value) { concerts.push(...result.value); } } // Eliminar duplicados return this.removeDuplicates(concerts); } async searchSongkick(artist, location, radius) { try { const response = await axios.get(`${this.apis.songkick.baseUrl}/events.json`, { params: { apikey: this.apis.songkick.key, artist_name: artist.name, location: `geo:${location.lat},${location.lng}`, radius: radius } }); return response.data.resultsPage?.results?.event?.map(this.normalizeSongkickEvent) || []; } catch (error) { console.error('Songkick API error:', error); return []; } } async searchBandsintown(artist, location, radius) { try { const response = await axios.get(`${this.apis.bandsintown.baseUrl}/artists/${artist.name}/events`, { params: { app_id: this.apis.bandsintown.key, date: 'upcoming' } }); return response.data?.map(this.normalizeBandsintownEvent) || []; } catch (error) { console.error('Bandsintown API error:', error); return []; } } normalizeSongkickEvent(event) { return { externalId: `songkick_${event.id}`, artist: { name: event.performance[0]?.artist?.displayName, mbid: event.performance[0]?.artist?.identifier?.[0]?.mbid }, venue: { name: event.venue.displayName, city: event.location.city, coordinates: { lat: event.venue.lat, lng: event.venue.lng } }, date: new Date(event.start.date), time: event.start.time, ticketUrl: event.uri, source: 'songkick' }; } removeDuplicates(concerts) { const seen = new Set(); return concerts.filter(concert => { const key = `${concert.artist.name}-${concert.date.getTime()}-${concert.venue.name}`; if (seen.has(key)) { return false; } seen.add(key); return true; }); } } // Notification Service class NotificationService { constructor(bot) { this.bot = bot; } async sendConcertAlert(user, concert, distance) { const message = this.formatConcertMessage(concert, distance); const keyboard = this.createConcertKeyboard(concert); try { await this.bot.sendMessage(user.telegramId, message, { parse_mode: 'Markdown', reply_markup: keyboard }); // Guardar notificación en base de datos await this.saveNotification(user, concert, 'sent'); } catch (error) { console.error('Error sending notification:', error); throw error; } } formatConcertMessage(concert, distance) { const date = concert.date.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); return ` 🎸 *¡Concierto de ${concert.artist.name}!* 📅 *Fecha:* ${date} 🕐 *Hora:* ${concert.time || 'Por confirmar'} 📍 *Lugar:* ${concert.venue.name}, ${concert.venue.city} 📏 *Distancia:* ${distance.toFixed(1)} km de tu ubicación ${concert.ticketUrl ? `🎫 [Comprar Entradas](${concert.ticketUrl})` : ''} ¿Te interesa este concierto? `.trim(); } createConcertKeyboard(concert) { return { inline_keyboard: [ [ { text: '✅ Me interesa', callback_data: `interested_${concert._id}` }, { text: '🎫 Comprar', url: concert.ticketUrl } ], [ { text: '❌ No me interesa', callback_data: `not_interested_${concert._id}` }, { text: '🔕 Silenciar artista', callback_data: `mute_${concert.artist.name}` } ] ] }; } }

🔧 Instalación y Configuración

# Instalación del Bot de Conciertos ## Requisitos previos - Node.js 16+ - MongoDB - Cuenta en Telegram - APIs: Last.fm, Songkick, Bandsintown, Ticketmaster ## Paso 1: Clonar el repositorio git clone https://github.com/tu-usuario/concierto-bot.git cd concierto-bot ## Paso 2: Instalar dependencias npm install ## Paso 3: Configurar variables de entorno cp .env.example .env # Editar .env con tus claves: TELEGRAM_BOT_TOKEN=tu_token_aqui MONGODB_URI=mongodb://localhost:27017/conciertobot LASTFM_API_KEY=tu_clave_lastfm SONGKICK_API_KEY=tu_clave_songkick BANDSINTOWN_API_KEY=tu_clave_bandsintown TICKETMASTER_API_KEY=tu_clave_ticketmaster GOOGLE_MAPS_API_KEY=tu_clave_google_maps NODE_ENV=development ## Paso 4: Iniciar MongoDB sudo systemctl start mongod ## Paso 5: Ejecutar el bot npm start ## Paso 6: (Opcional) Ejecutar como servicio en Linux # Crear archivo de servicio sudo nano /etc/systemd/system/conciertobot.service # Contenido del archivo: [Unit] Description=ConciertoBot Telegram After=network.target [Service] Type=simple User=nodeuser WorkingDirectory=/ruta/al/bot ExecStart=/usr/bin/node /ruta/al/bot/bot.js Restart=on-failure Environment=NODE_ENV=production [Install] WantedBy=multi-user.target # Habilitar y iniciar servicio sudo systemctl enable conciertobot sudo systemctl start conciertobot ## Comandos útiles # Ver logs sudo journalctl -u conciertobot -f # Reiniciar servicio sudo systemctl restart conciertobot # Ver estado sudo systemctl status conciertobot ## Docker (alternativa) # Construir imagen docker build -t conciertobot . # Ejecutar docker run -d \ --name conciertobot \ --restart unless-stopped \ -e TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} \ -e MONGODB_URI=${MONGODB_URI} \ -v $(pwd)/logs:/app/logs \ conciertobot
← Volver a Tutoriales