import logging import asyncio from datetime import datetime import discord from twitchAPI.twitch import Twitch from twitchAPI.object.api import Stream from database import db from database.models import LiveAlert from discordbot import bot from webapp import webapp logger = logging.getLogger('live-alert') logger.setLevel(logging.INFO) _live_alert_first_check = True def _stream_thumbnail_url(stream: Stream) -> str: """URL de la miniature du stream (preview Twitch).""" url = getattr(stream, 'thumbnail_url', None) or '' if '{width}' in url or '{height}' in url: url = url.replace('{width}', '320').replace('{height}', '180') if not url: url = f"https://static-cdn.jtvnw.net/previews-ttv/live_user_{stream.user_login}-320x180.jpg" return url def _format_embed_text(text: str, stream: Stream, stream_url: str, thumbnail: str) -> str: """Formate un texte d'embed avec les variables stream.""" if not text: return '' try: return text.format( user_login=stream.user_login or '', user_name=stream.user_name or '', game_name=getattr(stream, 'game_name', None) or '', title=stream.title or '', language=getattr(stream, 'language', None) or '', stream_url=stream_url, thumbnail=thumbnail, ) except KeyError: return text async def checkOnlineStreamer(twitch: Twitch) : global _live_alert_first_check with webapp.app_context() : alerts : list[LiveAlert] = LiveAlert.query.all() bot_status = webapp.config["BOT_STATUS"] was_live = bot_status.get("twitch_is_live", False) try: streams = await _retreiveStreams(twitch, alerts) except Exception as e: logger.error(f'Erreur lors de la récupération des streams, on conserve l\'état actuel : {e}') return watch_stream = None # Récupération du statut du live principal (channel configuré) from database.helpers import ConfigurationHelper main_channel = ConfigurationHelper().getValue('twitch_channel') main_stream = None if main_channel: main_stream = next((s for s in streams if s.user_login.lower() == main_channel.lower()), None) # Mise à jour du BOT_STATUS pour la webapp if main_stream: bot_status["twitch_is_live"] = True bot_status["twitch_viewer_count"] = getattr(main_stream, 'viewer_count', 0) bot_status["twitch_stream_title"] = getattr(main_stream, 'title', '') or '' bot_status["twitch_game_name"] = getattr(main_stream, 'game_name', '') or '' bot_status["twitch_started_at"] = main_stream.started_at.isoformat() if getattr(main_stream, 'started_at', None) else None bot_status["twitch_ended_at"] = None bot_status["twitch_chat_clear_notice_sent"] = False else: bot_status["twitch_is_live"] = False bot_status["twitch_viewer_count"] = 0 bot_status["twitch_stream_title"] = "" bot_status["twitch_game_name"] = "" bot_status["twitch_started_at"] = None if was_live and not bot_status.get("twitch_ended_at"): bot_status["twitch_ended_at"] = datetime.now().isoformat() if was_live and not bot_status.get("twitch_chat_clear_notice_sent"): messages = bot_status.setdefault("twitch_chat_messages", []) now_iso = datetime.now().isoformat() messages.append({ 'username': 'System', 'text': 'Live terminé, ce chat sera vidé dans 1h.', 'timestamp': now_iso, 'is_mod': False, 'is_subscriber': False, 'is_vip': False, 'color': '#22c55e', 'panel_only': True, }) if len(messages) > 100: messages.pop(0) bot_status["twitch_chat_clear_notice_sent"] = True # Premier check : synchronisation sans notification if _live_alert_first_check: logger.info('Live Alert: première vérification, synchronisation sans notification') for alert in alerts: stream = next((s for s in streams if s.user_login.lower() == (alert.login or '').lower()), None) if stream: alert.online = True if alert.watch_activity and alert.enable: watch_stream = stream else: alert.online = False await _updateBotActivity(watch_stream) db.session.commit() _live_alert_first_check = False return # Vérifications normales ensuite for alert in alerts : stream = next((s for s in streams if s.user_login.lower() == (alert.login or '').lower()), None) if stream : logger.info(f'Streamer en ligne : {alert.login}') if not alert.online and alert.enable : logger.info(f'N\'etait pas en ligne auparavant : {alert.login}') await _notifyAlert(alert, stream) alert.online = True if alert.watch_activity and alert.enable: watch_stream = stream else : logger.info(f'Streamer hors ligne : {alert.login}') alert.online = False await _updateBotActivity(watch_stream) db.session.commit() async def _updateBotActivity(stream: Stream | None): if not bot.loop or bot.loop.is_closed(): logger.warning("Loop Discord non disponible pour mise à jour de présence") return if stream: logger.info(f'Mise à jour de l\'activité : Regarde le live de {stream.user_name}') activity = discord.Streaming( name=f'Regarde le live de {stream.user_name}', url=f'https://www.twitch.tv/{stream.user_login}' ) with webapp.app_context(): webapp.config["BOT_STATUS"]["discord_streaming_activity"] = True future = asyncio.run_coroutine_threadsafe( bot.change_presence(status=discord.Status.online, activity=activity), bot.loop ) future.result(timeout=10) else: logger.info('Aucun stream à regarder, retour à l\'activité normale') with webapp.app_context(): webapp.config["BOT_STATUS"]["discord_streaming_activity"] = False # Remettre une humeur aléatoire from database.models import Humeur import random humeurs = Humeur.query.all() if humeurs: humeur = random.choice(humeurs) logger.info(f'Réinitialisation du statut : {humeur.text}') future = asyncio.run_coroutine_threadsafe( bot.change_presence(status=discord.Status.online, activity=discord.CustomActivity(humeur.text)), bot.loop ) future.result(timeout=10) else: # Si pas de humeur, remettre un statut par défaut future = asyncio.run_coroutine_threadsafe( bot.change_presence(status=discord.Status.online, activity=None), bot.loop ) future.result(timeout=10) async def _notifyAlert(alert: LiveAlert, stream: Stream): stream_url = f'https://www.twitch.tv/{stream.user_login}' thumbnail = _stream_thumbnail_url(stream) # Message texte optionnel (avant l'embed) message_text = None if alert.message and alert.message.strip(): try: message_text = alert.message.format(stream) except KeyError: message_text = alert.message # Construction de l'embed Discord try: embed_color = int(alert.embed_color or '9146FF', 16) except ValueError: embed_color = 0x9146FF embed_title = _format_embed_text(alert.embed_title, stream, stream_url, thumbnail) if alert.embed_title else (stream.title or f"{stream.user_name} est en live") embed_description = _format_embed_text(alert.embed_description, stream, stream_url, thumbnail) if alert.embed_description else None embed = discord.Embed( title=embed_title, url=stream_url, color=embed_color ) if embed_description: embed.description = embed_description author_name = _format_embed_text(alert.embed_author_name, stream, stream_url, thumbnail) if alert.embed_author_name else stream.user_name user_id = getattr(stream, 'user_id', None) author_icon = alert.embed_author_icon or (f"https://static-cdn.jtvnw.net/jtv_user_pictures/{user_id}-profile_image-70x70.png" if user_id else "https://static-cdn.jtvnw.net/ttv-favicon/favicon-32x32.png") embed.set_author(name=author_name, icon_url=author_icon) if alert.embed_thumbnail and thumbnail: embed.set_thumbnail(url=thumbnail) if alert.embed_image and thumbnail: embed.set_image(url=thumbnail) if alert.embed_footer: footer_text = _format_embed_text(alert.embed_footer, stream, stream_url, thumbnail) if footer_text: embed.set_footer(text=footer_text) logger.info(f'Envoi de notification live (embed) : {stream.user_login}') bot.loop.create_task(_sendMessage(alert.notify_channel, message_text, embed)) async def _sendMessage(channel_id: int, message: str | None, embed: discord.Embed): try: discord_channel = bot.get_channel(channel_id) if not discord_channel: logger.error(f"Canal Discord {channel_id} introuvable") return if message and message.strip(): await discord_channel.send(content=message, embed=embed) else: await discord_channel.send(embed=embed) logger.info('Notification live envoyée') except Exception as e: logger.error(f"Erreur lors de l'envoi de la notification live : {e}") async def _retreiveStreams(twitch: Twitch, alerts: list[LiveAlert]) -> list[Stream]: streams: list[Stream] = [] logger.info(f'Recherche de streams pour : {alerts}') async for stream in twitch.get_streams(user_login=[alert.login for alert in alerts]): streams.append(stream) logger.info(f'Ces streams sont en ligne : {streams}') return streams