Ajout de fonctionnalités majeures Twitch, Discord et interface web

Nouvelles fonctionnalités :
- Système de modération Twitch complet (bans, timeouts, avertissements)
- Filtre de liens intelligent pour Twitch avec whitelist/blacklist
- Notifications d'événements Twitch (follows, subs, raids, etc.)
- Système Freeloot Discord avec flux RSS dédié
- Salons automatiques Discord (création/suppression dynamique)
- Authentification utilisateur pour l'interface web (login/register)
- Gestion des utilisateurs et permissions
- Interface de modération Twitch dans la webapp
- Interface de gestion des événements Twitch
- Configuration des paramètres utilisateur
- Migration BDD pour les mots bannis

Améliorations de l'interface :
- Refonte complète des templates (configurations, commandes, humeurs, etc.)
- Nouvelle page de settings utilisateur
- Page de gestion des utilisateurs (admin)
- Page d'erreur 403 personnalisée
- Amélioration de la navigation et du design global
- Intégration de nouvelles sections dans le menu principal

Modifications techniques :
- Ajout de nouveaux modèles en base de données
- Extension des helpers database
- Mise à jour des dépendances (requirements.txt)
- Amélioration de la gestion des annonces Twitch
- Refactorisation du code pour meilleure maintenabilité

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-12 22:37:22 +01:00
co-authored by Cursor
parent d941fdcf9c
commit b274dcaa51
52 changed files with 7700 additions and 665 deletions
+25 -2
View File
@@ -7,8 +7,9 @@ from webapp import webapp
from database import db
from database.helpers import ConfigurationHelper
from database.models import Configuration, Humeur, Commande
from discord import Message, TextChannel, Member
from discord import Message, TextChannel, Member, VoiceChannel
from discordbot.humblebundle import checkHumbleBundleAndNotify
from discordbot.freeloot import checkFreeLootAndNotify
from discordbot.moderation import (
handle_warning_command,
handle_remove_warning_command,
@@ -24,6 +25,7 @@ from discordbot.moderation import (
)
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
from discordbot.youtube import checkYouTubeVideos
from discordbot.auto_rooms import on_voice_state_update_auto_rooms, on_raw_reaction_add_auto_rooms
from protondb import searhProtonDb
class DiscordBot(discord.Client):
@@ -40,6 +42,7 @@ class DiscordBot(discord.Client):
self.loop.create_task(self.updateStatus())
self.loop.create_task(self.updateHumbleBundle())
self.loop.create_task(self.updateYouTube())
self.loop.create_task(self.updateFreeLoot())
async def on_disconnect(self):
webapp.config["BOT_STATUS"]["discord_connected"] = False
@@ -64,13 +67,25 @@ class DiscordBot(discord.Client):
await checkYouTubeVideos()
await asyncio.sleep(5*60)
async def updateFreeLoot(self):
while not self.is_closed():
await checkFreeLootAndNotify(self)
await asyncio.sleep(30*60)
def getAllTextChannel(self) -> list[TextChannel]:
channels = []
for channel in self.get_all_channels():
if isinstance(channel, TextChannel):
channels.append(channel)
return channels
def getAllVoiceChannels(self) -> list[VoiceChannel]:
channels = []
for channel in self.get_all_channels():
if isinstance(channel, VoiceChannel):
channels.append(channel)
return channels
def getAllRoles(self):
guilds_roles = []
for guild in self.guilds:
@@ -261,6 +276,14 @@ async def on_message(message: Message):
except Exception as e:
logging.error(f"Échec de l'envoi de l'embed ProtonDB : {e}")
@bot.event
async def on_voice_state_update(member: Member, before, after):
await on_voice_state_update_auto_rooms(bot, member, before, after)
@bot.event
async def on_raw_reaction_add(payload: discord.RawReactionActionEvent):
await on_raw_reaction_add_auto_rooms(bot, payload)
@bot.event
async def on_member_join(member: Member):
await sendWelcomeMessage(bot, member)
+332
View File
@@ -0,0 +1,332 @@
# discordbot/auto_rooms.py — Auto rooms : message et réactions dans la partie texte du salon vocal (onglet Discussion)
import logging
from typing import Optional
import discord
from discord import Member, VoiceState
from database.helpers import ConfigurationHelper
# (guild_id, owner_id) -> room_data (voice_channel_id, control_message_id, whitelist, blacklist, access_mode)
_rooms: dict[tuple[int, int], dict] = {}
# message_id -> (guild_id, owner_id) pour retrouver la room depuis une réaction
_control_message_ids: dict[int, tuple[int, int]] = {}
# Emoji -> action
REACTIONS = [
("🔓", "open", "Ouvert"),
("🔒", "closed", "Fermé"),
("🛡️", "private", "Privé"),
("", "whitelist", "Liste blanche"),
("🚫", "blacklist", "Liste noire"),
("🧹", "purge", "Purge"),
("👑", "transfer", "Propriété"),
("🎤", "speak", "Micro"),
("📹", "stream", "Vidéo"),
("📝", "status", "Statut"),
]
def _status_display(access_mode: str) -> str:
"""Cadenas ouvert ou fermé selon si le salon est ouvert ou pas."""
if access_mode == "open":
return "🔓 Ouvert"
if access_mode == "closed":
return "🔒 Fermé"
if access_mode == "private":
return "🔒 Privé"
return "🔓 Ouvert"
def _status_emoji(access_mode: str) -> str:
"""Emoji cadenas seul pour le nom du channel."""
return "🔓" if access_mode == "open" else "🔒"
def _build_control_embed(owner: Member, voice_channel: discord.VoiceChannel, access_mode: str) -> discord.Embed:
"""Construit lembed de config avec infos du salon."""
embed = discord.Embed(
title="Configuration du salon",
description=(
"Voici lespace de configuration de votre salon vocal. "
"Utilisez les réactions ci-dessous — seul le propriétaire peut réagir."
),
color=discord.Color.blurple()
)
members_count = len(voice_channel.members)
user_limit = voice_channel.user_limit or 0
limit_text = f"{user_limit} max" if user_limit else "Illimitée"
members_text = f"{members_count} / {user_limit}" if user_limit else str(members_count)
bitrate_kbps = (voice_channel.bitrate or 0) // 1000
embed.add_field(name="Propriétaire", value=owner.mention, inline=True)
embed.add_field(name="Statut du salon", value=_status_display(access_mode), inline=True)
embed.add_field(name="Nom du salon", value=voice_channel.name, inline=True)
embed.add_field(name="Membres", value=members_text, inline=True)
embed.add_field(name="Limite", value=limit_text, inline=True)
embed.add_field(name="Bitrate", value=f"{bitrate_kbps} kbps", inline=True)
embed.add_field(name="Accès", value="🔓 Ouvert · 🔒 Fermé · 🛡️ Privé", inline=False)
embed.add_field(name="Listes", value="✅ Liste blanche · 🚫 Liste noire", inline=False)
embed.add_field(name="Actions", value="🧹 Purge · 👑 Propriété · 🎤 Micro · 📹 Vidéo · 📝 Statut", inline=False)
return embed
def _room_key(guild_id: int, owner_id: int) -> tuple[int, int]:
return (guild_id, owner_id)
def _get_room(guild_id: int, owner_id: int) -> Optional[dict]:
return _rooms.get(_room_key(guild_id, owner_id))
def _set_room(guild_id: int, owner_id: int, data: dict):
_rooms[_room_key(guild_id, owner_id)] = data
mid = data.get("control_message_id")
if mid:
_control_message_ids[mid] = (guild_id, owner_id)
def _del_room(guild_id: int, owner_id: int):
data = _rooms.pop(_room_key(guild_id, owner_id), None)
if data and data.get("control_message_id"):
_control_message_ids.pop(data["control_message_id"], None)
def _find_room_by_channel(guild_id: int, channel_id: int) -> Optional[tuple[int, dict]]:
for (gid, oid), data in _rooms.items():
if gid == guild_id and data.get("voice_channel_id") == channel_id:
return (oid, data)
return None
def _find_room_by_message(message_id: int) -> Optional[tuple[int, int, dict]]:
key = _control_message_ids.get(message_id)
if not key:
return None
guild_id, owner_id = key
data = _get_room(guild_id, owner_id)
if not data:
_control_message_ids.pop(message_id, None)
return None
return (guild_id, owner_id, data)
async def _apply_access_mode(channel: discord.VoiceChannel, mode: str, whitelist: set, blacklist: set):
guild = channel.guild
everyone = guild.default_role
overwrites = {}
everyone_ow = discord.PermissionOverwrite()
if mode == "open":
everyone_ow.connect = True
everyone_ow.view_channel = True
for uid in blacklist:
m = guild.get_member(uid)
if m:
overwrites[m] = discord.PermissionOverwrite(connect=False, view_channel=True)
elif mode == "closed":
everyone_ow.connect = False
everyone_ow.view_channel = True
for uid in whitelist:
m = guild.get_member(uid)
if m:
overwrites[m] = discord.PermissionOverwrite(connect=True, view_channel=True)
elif mode == "private":
everyone_ow.connect = False
everyone_ow.view_channel = False
for uid in whitelist:
m = guild.get_member(uid)
if m:
overwrites[m] = discord.PermissionOverwrite(connect=True, view_channel=True)
overwrites[everyone] = everyone_ow
await channel.edit(overwrites=overwrites)
async def _handle_reaction_action(bot: discord.Client, guild_id: int, owner_id: int, action: str, channel):
"""channel = salon vocal (partie texte / onglet Discussion)."""
room = _get_room(guild_id, owner_id)
if not room:
await channel.send("Ce salon nexiste plus.")
return
voice_channel = bot.get_channel(room["voice_channel_id"])
if not voice_channel or not isinstance(voice_channel, discord.VoiceChannel):
await channel.send("Salon vocal introuvable.")
return
if action in ("open", "closed", "private"):
room["access_mode"] = action
await _apply_access_mode(voice_channel, action, room.get("whitelist", set()), room.get("blacklist", set()))
# Mettre à jour le cadenas dans le nom du channel
try:
base_name = voice_channel.name.rstrip(" 🔓🔒")
new_name = f"{base_name} {_status_emoji(action)}"
await voice_channel.edit(name=new_name)
except discord.HTTPException:
pass
await channel.send(f"Accès du salon défini sur **{action}**.")
# Mettre à jour uniquement le statut (cadenas) dans le message de config
control_message_id = room.get("control_message_id")
if control_message_id:
try:
msg = await channel.fetch_message(control_message_id)
if msg.embeds:
embed = msg.embeds[0].copy()
for i, f in enumerate(embed.fields):
if f.name == "Statut du salon":
embed.set_field_at(i, name="Statut du salon", value=_status_display(action), inline=f.inline)
break
else:
embed.add_field(name="Statut du salon", value=_status_display(action), inline=False)
await msg.edit(embed=embed)
except discord.HTTPException:
pass
elif action == "whitelist":
await channel.send("Liste blanche : mentionnez un membre pour lajouter/retirer.")
elif action == "blacklist":
await channel.send("Liste noire : mentionnez un membre pour lajouter/retirer.")
elif action == "purge":
whitelist = room.get("whitelist", set())
kicked = 0
for member in list(voice_channel.members):
if member.id == owner_id or member.id in whitelist:
continue
try:
await member.move_to(None)
kicked += 1
except discord.HTTPException:
pass
await channel.send(f"Purge effectuée : {kicked} membre(s) déconnecté(s).")
elif action == "transfer":
await channel.send("Transférer le salon : mentionnez le membre à qui donner la propriété.")
elif action in ("speak", "stream"):
everyone = voice_channel.guild.default_role
overwrites = dict(voice_channel.overwrites)
ow = overwrites.get(everyone) or discord.PermissionOverwrite()
current = getattr(ow, action)
setattr(ow, action, not current if current is not None else False)
overwrites[everyone] = ow
await voice_channel.edit(overwrites=overwrites)
label = "Micro" if action == "speak" else "Vidéo"
await channel.send(f"{label} : {'autorisé' if getattr(ow, action) else 'désactivé'} pour tous.")
elif action == "status":
status_text = _status_display(room.get("access_mode", "open"))
await channel.send(f"Statut du salon : {status_text}\nRépondez avec le nouveau nom du salon pour le modifier.")
async def send_control_panel(bot: discord.Client, guild_id: int, owner: Member, voice_channel: discord.VoiceChannel) -> Optional[int]:
"""Envoie le message de config avec réactions dans la partie texte du salon vocal (onglet Discussion). Seul le proprio peut réagir. Retourne lid du message."""
embed = _build_control_embed(owner, voice_channel, "open")
try:
# Message dans la partie texte du vocal (onglet Discussion à droite)
msg = await voice_channel.send(embed=embed)
for emoji, _action, _label in REACTIONS:
await msg.add_reaction(emoji)
return msg.id
except discord.HTTPException as e:
logging.error(f"Impossible denvoyer le panneau Auto Room dans le vocal : {e}")
return None
async def on_voice_state_update_auto_rooms(bot: discord.Client, member: Member, before: VoiceState, after: VoiceState):
config = ConfigurationHelper()
if not config.getValue("auto_rooms_enable"):
return
trigger_channel_id = config.getIntValue("auto_rooms_channel_id")
if not trigger_channel_id:
return
guild = member.guild
if after.channel and after.channel.id == trigger_channel_id:
category = after.channel.category
# Nom du salon avec statut (cadenas) à la création
channel_name = f"Salon de {member.display_name} {_status_emoji('open')}"
try:
new_channel = await guild.create_voice_channel(
name=channel_name,
category=category,
reason="Auto room"
)
await member.move_to(new_channel)
control_message_id = await send_control_panel(bot, guild.id, member, new_channel)
_set_room(guild.id, member.id, {
"guild_id": guild.id,
"voice_channel_id": new_channel.id,
"control_message_id": control_message_id,
"owner_id": member.id,
"whitelist": set(),
"blacklist": set(),
"access_mode": "open",
})
logging.info(f"Auto room créé : {new_channel.name} pour {member.display_name}")
except discord.HTTPException as e:
logging.error(f"Erreur création auto room : {e}")
if before.channel and before.channel.id != trigger_channel_id:
result = _find_room_by_channel(guild.id, before.channel.id)
if result:
owner_id, room = result
remaining = [m for m in before.channel.members if m.id != member.id]
if member.id == owner_id:
_del_room(guild.id, owner_id)
try:
await before.channel.delete(reason="Propriétaire parti (auto room)")
except discord.HTTPException:
pass
elif len(remaining) == 0:
_del_room(guild.id, owner_id)
try:
await before.channel.delete(reason="Auto room vide")
except discord.HTTPException:
pass
async def on_raw_reaction_add_auto_rooms(bot: discord.Client, payload: discord.RawReactionActionEvent):
"""Seul le propriétaire peut réagir ; on retire la réaction des autres."""
if payload.user_id == bot.user.id:
return
if not ConfigurationHelper().getValue("auto_rooms_enable"):
return
room_info = _find_room_by_message(payload.message_id)
if not room_info:
return
guild_id, owner_id, room = room_info
if payload.user_id != owner_id:
try:
channel = bot.get_channel(payload.channel_id)
if channel:
msg = await channel.fetch_message(payload.message_id)
user = payload.member or await bot.fetch_user(payload.user_id)
await msg.remove_reaction(payload.emoji, user)
except discord.HTTPException:
pass
return
emoji_str = str(payload.emoji)
action = None
for e, a, _ in REACTIONS:
if e == emoji_str:
action = a
break
if not action:
return
# Canal = salon vocal (le message est dans la partie texte du vocal)
channel = bot.get_channel(payload.channel_id)
if not channel or not hasattr(channel, "send"):
return
await _handle_reaction_action(bot, guild_id, owner_id, action, channel)
try:
msg = await channel.fetch_message(payload.message_id)
user = payload.member or await bot.fetch_user(payload.user_id)
await msg.remove_reaction(payload.emoji, user)
except discord.HTTPException:
pass
+232
View File
@@ -0,0 +1,232 @@
# FreeLoot Discord : notifications depuis le feed LootScraper (jeux gratuits Epic, Amazon Prime, GOG, etc.)
import asyncio
import logging
from discord import Client
from database import db
from database.helpers import ConfigurationHelper
from database.models import FreeLootEntry
from freeloot_feed import (
SOURCES,
fetch_feed,
source_key_from_entry,
game_name_from_title,
extract_image_from_content,
extract_description_from_content,
extract_valid_to_from_content,
extract_recommended_price_from_content,
extract_genres_from_content,
extract_rating_from_content,
)
def _get_mention_content() -> str:
"""Construit le contenu du message (mentions) depuis la config."""
raw = ConfigurationHelper().getValue("freeloot_mention")
if not raw or not str(raw).strip():
return ""
parts = []
for s in str(raw).strip().split(","):
s = s.strip()
if s == "everyone":
parts.append("@everyone")
elif s == "here":
parts.append("@here")
elif s.isdigit():
parts.append(f"<@&{s}>")
return " ".join(parts) if parts else ""
def _is_enabled_source(source_key: str) -> bool:
"""Vérifie si cette source est activée dans la config (freeloot_sources)."""
raw = ConfigurationHelper().getValue("freeloot_sources")
if raw is None or (isinstance(raw, str) and raw.strip() == ""):
return True
enabled = [s.strip() for s in str(raw).split(",") if s.strip()]
return source_key in enabled if enabled else True
def _store_label_for_title(source_key: str) -> str:
"""Libellé court pour le titre style DraftBot (ex: 'l'Epic Games Store')."""
labels = {
"epic_pc": "l'Epic Games Store",
"epic_android": "l'Epic Games Store (Android)",
"epic_ios": "l'Epic Games Store (iOS)",
"amazon_prime": "Amazon Prime Gaming",
"gog": "GOG",
"google_play": "Google Play",
"apple_app_store": "l'App Store",
}
return labels.get(source_key, "la boutique")
# Logo (thumbnail) de chaque boutique pour l'embed Discord (affiché en haut à droite)
SOURCE_LOGO_URLS = {
"epic_pc": "https://store.epicgames.com/favicon.ico",
"epic_android": "https://store.epicgames.com/favicon.ico",
"epic_ios": "https://store.epicgames.com/favicon.ico",
"amazon_prime": "https://gaming.amazon.com/favicon.ico",
"gog": "https://www.gog.com/favicon.ico",
"google_play": "https://play.google.com/favicon.ico",
"apple_app_store": "https://www.apple.com/favicon.ico",
}
def _build_embed(entry: dict, source_key: str):
import discord
game_name = game_name_from_title(entry["title"])
source_label = next((s[1] for s in SOURCES if s[0] == source_key), source_key)
link = entry.get("link") or ""
content_raw = entry.get("content") or ""
img_url = extract_image_from_content(content_raw)
description = extract_description_from_content(content_raw, max_len=350)
valid_to = extract_valid_to_from_content(content_raw)
store_title = _store_label_for_title(source_key)
# Couleur barre gauche style DraftBot (orange-rouge)
color = 0xE67E22
title = f"{game_name} gratuit sur {store_title} !"
embed = discord.Embed(
title=title,
url=link if link.startswith("http") else None,
color=color,
)
if description:
embed.description = description
# Prix / gratuit / validité (Discord : pas de couleur dans le texte, seulement **gras** / markdown)
value_parts = ["**Gratuit**"]
if valid_to:
try:
from datetime import datetime
end = datetime.fromisoformat(valid_to.replace("Z", "+00:00"))
value_parts.append(f"jusqu'au {end.strftime('%d/%m/%Y')}")
except Exception:
value_parts.append(f"jusqu'au {valid_to[:10]}")
embed.add_field(
name="Prix",
value="".join(value_parts),
inline=False,
)
recommended_price = extract_recommended_price_from_content(content_raw)
if recommended_price:
embed.add_field(name="Prix recommandé", value=recommended_price, inline=True)
genres = extract_genres_from_content(content_raw)
if genres:
embed.add_field(name="Genres", value=genres, inline=True)
rating = extract_rating_from_content(content_raw)
if rating:
embed.add_field(name="Ratings", value=rating, inline=True)
if link and link.startswith("http"):
embed.add_field(
name="\u200b",
value=f"[Ouvrir dans la boutique !]({link})",
inline=False,
)
# Thumbnail (logo de la boutique en haut à droite)
logo_url = SOURCE_LOGO_URLS.get(source_key)
if logo_url and logo_url.startswith("http"):
embed.set_thumbnail(url=logo_url)
# Image principale (style DraftBot)
if img_url and img_url.startswith("http"):
embed.set_image(url=img_url)
embed.set_footer(text="MamieHenriette • FreeLoot")
return embed
_freeloot_first_check = True
async def checkFreeLootAndNotify(bot: Client):
global _freeloot_first_check
helper = ConfigurationHelper()
if not helper.getValue("freeloot_enable"):
return
channel_id = helper.getIntValue("freeloot_channel_id")
if not channel_id:
return
channel = bot.get_channel(channel_id)
if not channel:
logging.warning("FreeLoot: canal Discord introuvable")
return
entries = fetch_feed()
if not entries:
return
# Au premier check après le démarrage, on synchronise sans notifier
if _freeloot_first_check:
logging.info("FreeLoot: première vérification, synchronisation sans notification")
for entry in entries:
entry_id = entry["id"]
if not FreeLootEntry.query.get(entry_id):
source_key = source_key_from_entry(entry["title"], entry["link"])
if source_key and _is_enabled_source(source_key):
try:
db.session.add(FreeLootEntry(entry_id=entry_id))
db.session.commit()
except Exception as e:
logging.error(f"FreeLoot: erreur de synchronisation pour {entry_id}: {e}")
db.session.rollback()
_freeloot_first_check = False
return
# Vérifications suivantes : notification normale
for entry in entries:
entry_id = entry["id"]
if FreeLootEntry.query.get(entry_id):
continue
source_key = source_key_from_entry(entry["title"], entry["link"])
if not source_key or not _is_enabled_source(source_key):
continue
try:
embed = _build_embed(entry, source_key)
content = _get_mention_content()
await channel.send(content=content or None, embed=embed)
db.session.add(FreeLootEntry(entry_id=entry_id))
db.session.commit()
except Exception as e:
logging.error(f"FreeLoot: envoi Discord échoué pour {entry_id}: {e}")
db.session.rollback()
async def _send_entry_to_discord_async(bot: Client, entry_id: str) -> tuple[bool, str]:
"""
Envoie une entrée FreeLoot sur Discord (appel manuel). Retourne (succès, message).
"""
channel_id = ConfigurationHelper().getIntValue("freeloot_channel_id")
if not channel_id:
return (False, "Aucun canal Discord configuré pour FreeLoot.")
channel = bot.get_channel(channel_id)
if not channel:
return (False, "Canal Discord introuvable.")
entries = fetch_feed()
if not entries:
return (False, "Impossible de charger le flux.")
entry = next((e for e in entries if e.get("id") == entry_id), None)
if not entry:
return (False, "Entrée introuvable dans le flux.")
source_key = source_key_from_entry(entry["title"], entry["link"])
if not source_key:
return (False, "Source non reconnue pour cette entrée.")
try:
embed = _build_embed(entry, source_key)
content = _get_mention_content()
await channel.send(content=content or None, embed=embed)
if not FreeLootEntry.query.get(entry_id):
db.session.add(FreeLootEntry(entry_id=entry_id))
db.session.commit()
return (True, "Annonce envoyée sur Discord.")
except Exception as e:
logging.error(f"FreeLoot: envoi manuel échoué pour {entry_id}: {e}")
db.session.rollback()
return (False, str(e))
def send_entry_to_discord_sync(bot: Client, entry_id: str) -> tuple[bool, str]:
"""Appel synchrone pour envoyer une entrée sur Discord (depuis la webapp)."""
try:
future = asyncio.run_coroutine_threadsafe(
_send_entry_to_discord_async(bot, entry_id),
bot.loop,
)
return future.result(timeout=15)
except Exception as e:
logging.error(f"FreeLoot: send_entry_to_discord_sync: {e}")
return (False, str(e))
+14
View File
@@ -8,6 +8,8 @@ from database.helpers import ConfigurationHelper
from database.models import GameBundle
from discord import Client
_humblebundle_first_check = True
def _isEnable():
helper = ConfigurationHelper()
@@ -40,10 +42,22 @@ def _formatMessage(bundle):
return message
async def checkHumbleBundleAndNotify(bot: Client):
global _humblebundle_first_check
if _isEnable() :
try :
bundles = _callGithub()
bundle = _findFirstNotNotified(bundles)
# Premier check : synchronisation sans notification
if _humblebundle_first_check:
if bundle != None:
logging.info(f'HumbleBundle: première vérification, synchronisation sans notification pour {bundle["name"]}')
db.session.add(GameBundle(url=bundle['url'], name=bundle['name'], json = json.dumps(bundle)))
db.session.commit()
_humblebundle_first_check = False
return
# Vérifications normales ensuite
if bundle != None :
message = _formatMessage(bundle)
await bot.get_channel(ConfigurationHelper().getIntValue('humble_bundle_channel')).send(message)
+19 -2
View File
@@ -10,23 +10,31 @@ from webapp import webapp
logger = logging.getLogger('youtube-notification')
logger.setLevel(logging.INFO)
_youtube_first_check = True
async def checkYouTubeVideos():
global _youtube_first_check
with webapp.app_context():
try:
notifications: list[YouTubeNotification] = YouTubeNotification.query.filter_by(enable=True).all()
for notification in notifications:
try:
await _checkChannelVideos(notification)
await _checkChannelVideos(notification, is_first_check=_youtube_first_check)
except Exception as e:
logger.error(f"Erreur lors de la vérification de la chaîne {notification.channel_id}: {e}")
continue
# Après la première vérification complète, on désactive le flag
if _youtube_first_check:
_youtube_first_check = False
logger.info("YouTube: première vérification terminée, notifications activées")
except Exception as e:
logger.error(f"Erreur lors de la vérification YouTube: {e}")
async def _checkChannelVideos(notification: YouTubeNotification):
async def _checkChannelVideos(notification: YouTubeNotification, is_first_check: bool = False):
try:
channel_id = notification.channel_id
@@ -109,6 +117,15 @@ async def _checkChannelVideos(notification: YouTubeNotification):
if videos:
latest_video_id, latest_video = videos[0]
# Si c'est la première vérification après démarrage, on synchronise sans notifier
if is_first_check:
if not notification.last_video_id or notification.last_video_id != latest_video_id:
logger.info(f"YouTube: synchronisation initiale pour {channel_id}, dernière vidéo: {latest_video_id}")
notification.last_video_id = latest_video_id
db.session.commit()
return
# Vérifications normales ensuite
if not notification.last_video_id:
notification.last_video_id = latest_video_id
db.session.commit()