Ajout de la gestion des informations de stream dans le bot Twitch. Implémentation de la fonction de remplacement des variables de commande pour inclure des informations dynamiques telles que le titre du stream, le nom du jeu, le nombre de viewers et l'uptime. Mise à jour de l'interface utilisateur pour afficher ces informations en temps réel dans la modération Twitch.

This commit is contained in:
2026-03-06 13:48:43 +01:00
parent 876eb1a080
commit 10dd78f630
5 changed files with 273 additions and 49 deletions
+36 -2
View File
@@ -104,11 +104,45 @@ async def _handleCustomCommand(msg: ChatMessage):
if commande:
permission = commande.twitch_permission or 'viewer'
if not _user_has_twitch_permission(msg, permission):
return # Pas de réponse = l'utilisateur n'a pas la permission
response = commande.response.replace('{user}', msg.user.name)
return
response = _replace_command_variables(commande.response, msg)
await msg.reply(response)
def _replace_command_variables(text: str, msg: ChatMessage) -> str:
"""Remplace les variables de template dans la réponse d'une commande."""
from datetime import datetime
result = text
result = result.replace('{user}', msg.user.name)
result = result.replace('{username}', msg.user.name)
result = result.replace('{channel}', msg.room.name)
bot_status = webapp.config.get("BOT_STATUS", {})
result = result.replace('{title}', bot_status.get("twitch_stream_title", ""))
result = result.replace('{game}', bot_status.get("twitch_game_name", ""))
result = result.replace('{viewers}', str(bot_status.get("twitch_viewer_count", 0)))
uptime_str = "hors ligne"
started_at_str = bot_status.get("twitch_started_at")
if started_at_str and bot_status.get("twitch_is_live", False):
try:
started_at = datetime.fromisoformat(started_at_str)
delta = datetime.now(started_at.tzinfo) - started_at
total_seconds = int(delta.total_seconds())
hours, remainder = divmod(max(0, total_seconds), 3600)
minutes, _ = divmod(remainder, 60)
if hours > 0:
uptime_str = f"{hours}h {minutes:02d}min"
else:
uptime_str = f"{minutes}min"
except (ValueError, TypeError):
pass
result = result.replace('{uptime}', uptime_str)
return result
async def _helloCommand(msg: ChatMessage):
await msg.reply(f'Bonjour {msg.user.name}')
+6
View File
@@ -67,9 +67,15 @@ async def checkOnlineStreamer(twitch: Twitch) :
if main_stream:
webapp.config["BOT_STATUS"]["twitch_is_live"] = True
webapp.config["BOT_STATUS"]["twitch_viewer_count"] = getattr(main_stream, 'viewer_count', 0)
webapp.config["BOT_STATUS"]["twitch_stream_title"] = getattr(main_stream, 'title', '') or ''
webapp.config["BOT_STATUS"]["twitch_game_name"] = getattr(main_stream, 'game_name', '') or ''
webapp.config["BOT_STATUS"]["twitch_started_at"] = main_stream.started_at.isoformat() if getattr(main_stream, 'started_at', None) else None
else:
webapp.config["BOT_STATUS"]["twitch_is_live"] = False
webapp.config["BOT_STATUS"]["twitch_viewer_count"] = 0
webapp.config["BOT_STATUS"]["twitch_stream_title"] = ""
webapp.config["BOT_STATUS"]["twitch_game_name"] = ""
webapp.config["BOT_STATUS"]["twitch_started_at"] = None
# Premier check : synchronisation sans notification
if _live_alert_first_check:
+3
View File
@@ -15,6 +15,9 @@ webapp.config["BOT_STATUS"] = {
"twitch_channel_name": None,
"twitch_is_live": False,
"twitch_viewer_count": 0,
"twitch_stream_title": "",
"twitch_game_name": "",
"twitch_started_at": None,
"twitch_chat_messages": [], # Derniers messages du chat (max 100)
"shoutbox_heartbeats": {}, # {"username": datetime} — présence des modos
}
+208 -47
View File
@@ -71,10 +71,10 @@
max-width: 380px;
}
#notificationStack > div { pointer-events: auto; }
#editModal {
#editModal, #addCommandModal {
transition: opacity 0.2s;
}
#editModal.hidden { display: none; }
#editModal.hidden, #addCommandModal.hidden { display: none; }
</style>
<div id="notificationStack"></div>
@@ -137,9 +137,9 @@
{% if is_live %}
<!-- ===== LAYOUT EN LIVE ===== -->
<!-- Ligne 1 : Player (2/3) | Formulaire commande (1/3) -->
<!-- Ligne 1 : Player (2/3) | Infos stream dynamiques (1/3) -->
<div class="grid lg:grid-cols-3 gap-4">
<!-- Player Twitch + stats dynamiques (2/3) -->
<!-- Player Twitch (2/3) -->
<div class="lg:col-span-2 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="aspect-video bg-gray-900">
<iframe
@@ -150,12 +150,12 @@
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="flex h-2 w-2 relative">
<span class="flex h-2 w-2 relative" id="liveIndicator">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
</span>
<span class="text-sm font-medium text-gray-900 dark:text-white">EN DIRECT</span>
<span class="text-sm text-gray-500 dark:text-gray-400">&#8226; {{ viewer_count }} viewers</span>
<span class="text-sm font-medium text-gray-900 dark:text-white" id="liveStatusLabel">EN DIRECT</span>
<span class="text-sm text-gray-500 dark:text-gray-400" id="playerViewerCount">&#8226; {{ viewer_count }} viewers</span>
</div>
<a href="https://www.twitch.tv/{{ twitch_channel }}" target="_blank" class="text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
@@ -164,54 +164,68 @@
</div>
</div>
<!-- Formulaire Nouvelle commande (1/3) -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
<h2 class="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
Nouvelle commande
<!-- Infos stream dynamiques (1/3) -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4 flex flex-col gap-4">
<h2 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<svg class="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Infos du stream
</h2>
<form action="{{ url_for('add_twitch_commande') }}" method="POST" class="space-y-3">
<input type="text" name="trigger" placeholder="!commande" required
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
<select name="twitch_permission"
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
{% for value, label in twitch_permissions.items() %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
<textarea name="response" rows="3" placeholder="Réponse... ({user} = mention)" required
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm resize-none"></textarea>
<button type="submit" class="w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium text-sm">Ajouter</button>
</form>
<div class="space-y-3 flex-1">
<!-- Titre -->
<div>
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Titre</div>
<div class="text-sm font-medium text-gray-900 dark:text-white leading-snug" id="streamTitle">{{ stream_title or 'N/A' }}</div>
</div>
<!-- Jeu / Catégorie -->
<div>
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Catégorie</div>
<div class="flex items-center gap-1.5">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300" id="streamGame">{{ game_name or 'N/A' }}</span>
</div>
</div>
<!-- Viewers -->
<div>
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Viewers</div>
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400" id="streamViewers">{{ viewer_count }}</div>
</div>
<!-- Uptime -->
<div>
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Uptime</div>
<div class="text-sm font-mono font-medium text-gray-900 dark:text-white" id="streamUptime">--:--:--</div>
</div>
</div>
<a href="https://www.twitch.tv/{{ twitch_channel }}" target="_blank" class="block w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium text-sm text-center">
Voir sur Twitch
</a>
</div>
</div>
<!-- Ligne 2 : Chat + Sanctions (pleine largeur) -->
<!-- Ligne 2 : Chat (pleine largeur) -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col" style="height: 500px;">
{% else %}
<!-- ===== LAYOUT HORS LIVE ===== -->
<div class="grid lg:grid-cols-2 gap-4">
<!-- Formulaire d'ajout de commande -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
<h2 class="text-base font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
Ajouter une commande
</h2>
<form action="{{ url_for('add_twitch_commande') }}" method="POST" class="space-y-3">
<div class="grid grid-cols-2 gap-2">
<input type="text" name="trigger" placeholder="!commande" required
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
<select name="twitch_permission"
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
{% for value, label in twitch_permissions.items() %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
<!-- Player Twitch (offline) + infos -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="aspect-video bg-gray-900">
<iframe
src="https://player.twitch.tv/?channel={{ twitch_channel }}&enableExtensions=true&muted=true&parent={{ embed_parent }}&player=popout&quality=auto&volume=0"
style="width: 100%; height: 100%; border: none;"
allowfullscreen>
</iframe>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="relative inline-flex rounded-full h-2 w-2 bg-gray-400"></span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400" id="liveStatusLabel">HORS LIGNE</span>
</div>
<textarea name="response" rows="2" placeholder="Réponse du bot... ({user} = mention)" required
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm resize-none"></textarea>
<button type="submit" class="w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium text-sm">Ajouter</button>
</form>
<a href="https://www.twitch.tv/{{ twitch_channel }}" target="_blank" class="text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
Ouvrir sur Twitch
</a>
</div>
</div>
<!-- Chat (1/2) -->
@@ -402,8 +416,12 @@
<!-- Commandes personnalisées Twitch -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex items-center justify-between">
<h3 class="font-semibold text-gray-900 dark:text-white">Commandes personnalisées Twitch (<span id="customCmdCount">{{ custom_commands|length }}</span>)</h3>
<button onclick="openAddCommandModal()" class="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium text-xs flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
Ajouter
</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm" id="customCommandsTable">
@@ -512,6 +530,17 @@
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Réponse</label>
<textarea id="editResponse" rows="3" class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm resize-none"></textarea>
<div class="mt-2">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1.5">Variables disponibles <span class="text-gray-400">(cliquer pour insérer)</span></div>
<div class="flex flex-wrap gap-1">
<button type="button" onclick="insertVariable('editResponse','{user}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Nom de l'utilisateur">{user}</button>
<button type="button" onclick="insertVariable('editResponse','{channel}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Nom de la chaîne">{channel}</button>
<button type="button" onclick="insertVariable('editResponse','{title}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Titre du stream">{title}</button>
<button type="button" onclick="insertVariable('editResponse','{game}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Jeu / catégorie">{game}</button>
<button type="button" onclick="insertVariable('editResponse','{uptime}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Durée du live">{uptime}</button>
<button type="button" onclick="insertVariable('editResponse','{viewers}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Nombre de viewers">{viewers}</button>
</div>
</div>
</div>
</div>
<div class="px-5 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-2">
@@ -521,6 +550,56 @@
</div>
</div>
<!-- Modal d'ajout de commande -->
<div id="addCommandModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50" onclick="if(event.target===this)closeAddCommandModal()">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-md mx-4 animate-fade-in">
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 class="font-semibold text-gray-900 dark:text-white">Ajouter une commande</h3>
<button onclick="closeAddCommandModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<form action="{{ url_for('add_twitch_commande') }}" method="POST">
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Commande</label>
<input type="text" name="trigger" placeholder="!commande" required
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Permission</label>
<select name="twitch_permission"
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
{% for value, label in twitch_permissions.items() %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Réponse</label>
<textarea name="response" id="addResponse" rows="3" placeholder="Réponse du bot..." required
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm resize-none"></textarea>
<div class="mt-2">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1.5">Variables disponibles <span class="text-gray-400">(cliquer pour insérer)</span></div>
<div class="flex flex-wrap gap-1">
<button type="button" onclick="insertVariable('addResponse','{user}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Nom de l'utilisateur">{user}</button>
<button type="button" onclick="insertVariable('addResponse','{channel}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Nom de la chaîne">{channel}</button>
<button type="button" onclick="insertVariable('addResponse','{title}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Titre du stream">{title}</button>
<button type="button" onclick="insertVariable('addResponse','{game}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Jeu / catégorie">{game}</button>
<button type="button" onclick="insertVariable('addResponse','{uptime}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Durée du live">{uptime}</button>
<button type="button" onclick="insertVariable('addResponse','{viewers}')" class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-purple-600 dark:text-purple-400 rounded text-xs font-mono hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors" title="Nombre de viewers">{viewers}</button>
</div>
</div>
</div>
</div>
<div class="px-5 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-2">
<button type="button" onclick="closeAddCommandModal()" class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">Annuler</button>
<button type="submit" class="px-4 py-2 text-sm bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium">Ajouter</button>
</div>
</form>
</div>
</div>
<script>
(function() {
// --- Recherche commandes de modération ---
@@ -1088,5 +1167,87 @@ setInterval(fetchShoutbox, 3000);
fetchShoutbox();
setInterval(shoutboxHeartbeat, 10000);
shoutboxHeartbeat();
// =============================
// Modal ajout de commande
// =============================
function openAddCommandModal() {
document.getElementById('addCommandModal').classList.remove('hidden');
}
function closeAddCommandModal() {
document.getElementById('addCommandModal').classList.add('hidden');
}
// =============================
// Insertion de variables dans les textareas
// =============================
function insertVariable(textareaId, variable) {
var textarea = document.getElementById(textareaId);
if (!textarea) return;
var start = textarea.selectionStart;
var end = textarea.selectionEnd;
var text = textarea.value;
textarea.value = text.substring(0, start) + variable + text.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + variable.length;
textarea.focus();
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeAddCommandModal();
});
// =============================
// Polling infos stream dynamique
// =============================
var _streamStartedAt = {{ "'" ~ started_at ~ "'" if started_at else 'null' }};
var _uptimeInterval = null;
function updateUptime() {
var el = document.getElementById('streamUptime');
if (!el || !_streamStartedAt) return;
var start = new Date(_streamStartedAt);
var now = new Date();
var diff = Math.max(0, Math.floor((now - start) / 1000));
var h = Math.floor(diff / 3600);
var m = Math.floor((diff % 3600) / 60);
var s = diff % 60;
el.textContent = (h > 0 ? h + 'h ' : '') + (m < 10 && h > 0 ? '0' : '') + m + 'min ' + (s < 10 ? '0' : '') + s + 's';
}
function fetchStreamInfo() {
fetch('{{ url_for("twitch_stream_info") }}')
.then(function(r) { return r.json(); })
.then(function(data) {
var titleEl = document.getElementById('streamTitle');
var gameEl = document.getElementById('streamGame');
var viewersEl = document.getElementById('streamViewers');
var playerViewersEl = document.getElementById('playerViewerCount');
if (titleEl) titleEl.textContent = data.title || 'N/A';
if (gameEl) gameEl.textContent = data.game_name || 'N/A';
if (viewersEl) viewersEl.textContent = data.viewer_count;
if (playerViewersEl) playerViewersEl.innerHTML = '&#8226; ' + data.viewer_count + ' viewers';
if (data.started_at) {
_streamStartedAt = data.started_at;
if (!_uptimeInterval) {
_uptimeInterval = setInterval(updateUptime, 1000);
}
updateUptime();
}
})
.catch(function(e) { console.error('Erreur stream info:', e); });
}
{% if is_live %}
if (_streamStartedAt) {
_uptimeInterval = setInterval(updateUptime, 1000);
updateUptime();
}
{% endif %}
setInterval(fetchStreamInfo, 15000);
fetchStreamInfo();
</script>
{% endblock %}
+20
View File
@@ -136,6 +136,9 @@ def twitch_moderation():
bot_status = webapp.config.get("BOT_STATUS", {})
is_live = bot_status.get("twitch_is_live", False)
viewer_count = bot_status.get("twitch_viewer_count", 0)
stream_title = bot_status.get("twitch_stream_title", "")
game_name = bot_status.get("twitch_game_name", "")
started_at = bot_status.get("twitch_started_at")
return render_template(
"twitch-moderation.html",
@@ -149,6 +152,9 @@ def twitch_moderation():
banned_words=banned_words,
is_live=is_live,
viewer_count=viewer_count,
stream_title=stream_title,
game_name=game_name,
started_at=started_at,
)
@webapp.route("/twitch-moderation/logs/clear")
@@ -309,6 +315,20 @@ def get_twitch_messages():
messages = webapp.config["BOT_STATUS"].get("twitch_chat_messages", [])
return jsonify({"messages": messages})
@webapp.route("/twitch-moderation/stream-info")
@require_page("twitch_moderation")
def twitch_stream_info():
"""Retourne les infos du stream en cours pour le polling dynamique."""
bot_status = webapp.config.get("BOT_STATUS", {})
return jsonify({
"is_live": bot_status.get("twitch_is_live", False),
"viewer_count": bot_status.get("twitch_viewer_count", 0),
"title": bot_status.get("twitch_stream_title", ""),
"game_name": bot_status.get("twitch_game_name", ""),
"started_at": bot_status.get("twitch_started_at"),
})
@webapp.route("/twitch-moderation/logs/poll")
@require_page("twitch_moderation")
def poll_twitch_logs():