Ajout de la surveillance des nouvelles vidéos YouTube

This commit is contained in:
Mow
2025-12-06 12:45:03 +01:00
parent 42db84f010
commit b49f914501
10 changed files with 318 additions and 2 deletions
+10
View File
@@ -93,3 +93,13 @@ class DiscordInvite(db.Model):
revoked = db.Column(db.Boolean, default=False)
last_sync = db.Column(db.DateTime)
class YoutubeAlert(db.Model):
__tablename__ = 'youtube_alert'
id = db.Column(db.Integer, primary_key=True)
enable = db.Column(db.Boolean, default=True)
channel_id = db.Column(db.String(64), unique=True)
channel_name = db.Column(db.String(256))
notify_channel = db.Column(db.Integer)
message = db.Column(db.String(2000))
last_video_id = db.Column(db.String(64))
+10
View File
@@ -108,3 +108,13 @@ CREATE TABLE IF NOT EXISTS `discord_invite` (
`revoked` BOOLEAN NOT NULL DEFAULT FALSE,
`last_sync` DATETIME NOT NULL
);
CREATE TABLE IF NOT EXISTS `youtube_alert` (
id INTEGER PRIMARY KEY AUTOINCREMENT,
`enable` BOOLEAN NOT NULL DEFAULT TRUE,
`channel_id` VARCHAR(64) UNIQUE NOT NULL,
`channel_name` VARCHAR(256),
`notify_channel` INTEGER NOT NULL,
`message` VARCHAR(2000) NOT NULL,
`last_video_id` VARCHAR(64)
);
+7
View File
@@ -49,6 +49,7 @@ class DiscordBot(discord.Client):
self.loop.create_task(self.updateStatus())
self.loop.create_task(self.updateHumbleBundle())
self.loop.create_task(self.updateFreeGames())
self.loop.create_task(self.updateYoutubeAlerts())
self.loop.create_task(self._periodic_stats_update())
def _update_shared_stats(self):
@@ -115,6 +116,12 @@ class DiscordBot(discord.Client):
await checkFreeGamesAndNotify(self)
await asyncio.sleep(60*60) # Vérification toutes les heures
async def updateYoutubeAlerts(self):
from youtubebot.youtube_alert import checkNewVideos
while not self.is_closed():
await checkNewVideos(self)
await asyncio.sleep(10*60) # Vérification toutes les 10 minutes
def getAllTextChannel(self) -> list[TextChannel]:
channels = []
for channel in self.get_all_channels():
+1 -1
View File
@@ -4,4 +4,4 @@ import os
webapp = Flask(__name__)
webapp.secret_key = os.environ.get('FLASK_SECRET_KEY', 'mamie-henriette-secret-key-change-me')
from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation, freegames
from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation, freegames, youtube_alert
+2 -1
View File
@@ -543,7 +543,8 @@ table tbody tr:hover {
background: var(--accent-soft);
}
table.live-alert tr td:last-child {
table.live-alert tr td:last-child,
table.youtube-alert tr td:last-child {
white-space: nowrap;
}
+6
View File
@@ -45,6 +45,12 @@
<li><a href="/live-alert">📺 Alerte Live</a></li>
</ul>
</li>
<li class="has-submenu">
<a href="#">YouTube</a>
<ul class="submenu">
<li><a href="/youtube-alert">🎬 Alertes Vidéos</a></li>
</ul>
</li>
<li class="has-submenu">
<a href="#">Outils</a>
<ul class="submenu">
+94
View File
@@ -0,0 +1,94 @@
{% extends "template.html" %}
{% block content %}
<h1>Alertes YouTube</h1>
<p>
Liste des chaînes YouTube surveillées pour les alertes de nouvelles vidéos.
Le bot vérifie régulièrement les nouvelles vidéos publiées sur les chaînes configurées.
Lorsqu'une nouvelle vidéo est détectée, le bot envoie une notification sur le canal Discord configuré.
</p>
{% if not alert %}
<h2>Chaînes surveillées</h2>
<table class="youtube-alert">
<thead>
<tr>
<th>Chaîne</th>
<th>Canal Discord</th>
<th>Message</th>
<th>#</th>
</tr>
</thead>
<tbody>
{% for alert in alerts %}
<tr>
<td>
<a href="https://www.youtube.com/channel/{{alert.channel_id}}" target="_blank">
{{alert.channel_name or alert.channel_id}}
</a>
</td>
<td>{{alert.notify_channel_name}}</td>
<td>{{alert.message[:50]}}{% if alert.message|length > 50 %}...{% endif %}</td>
<td>
<a href="{{ url_for('toggleYoutubeAlert', id = alert.id) }}" class="icon">{{ '✅' if alert.enable else '❌' }}</a>
<a href="{{ url_for('openEditYoutubeAlert', id = alert.id) }}" class="icon"></a>
<a href="{{ url_for('delYoutubeAlert', id = alert.id) }}"
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette alerte ?')" class="icon">🗑</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<h2>{{ 'Editer une alerte' if alert else 'Ajouter une alerte YouTube' }}</h2>
<form action="{{ url_for('submitEditYoutubeAlert', id = alert.id) if alert else url_for('addYoutubeAlert') }}" method="POST">
<label for="channel_id">ID de la chaîne YouTube</label>
<input name="channel_id" type="text" maxlength="64" required="required" value="{{alert.channel_id if alert}}" placeholder="UCxxxxxxxxxxxxxxxxxxxxxxxx"/>
<label for="notify_channel">Canal de Notification Discord</label>
<select name="notify_channel">
{% for channel in channels %}
<option value="{{channel.id}}"{% if alert and alert.notify_channel == channel.id %}
selected="selected" {% endif %}>{{channel.name}}</option>
{% endfor %}
</select>
<label for="message">Message</label>
<textarea name="message" rows="5" cols="50" required="required">{{alert.message if alert else '🎬 **Nouvelle vidéo !**\n\n**{0.author}** vient de publier : **{0.title}**\n\n👉 {0.link}'}}</textarea>
<input type="Submit" value="{{ 'Modifier' if alert else 'Ajouter' }}">
<h3>Comment trouver l'ID de chaîne YouTube ?</h3>
<p>
L'ID de chaîne YouTube commence par <code>UC</code> et contient 24 caractères.
</p>
<ol>
<li>Allez sur la page de la chaîne YouTube</li>
<li>Cliquez droit sur la page et choisissez "Afficher le code source"</li>
<li>Recherchez <code>channelId</code> ou <code>externalId</code></li>
<li>Copiez l'ID qui ressemble à : <code>UCxxxxxxxxxxxxxxxxxxxxxxxx</code></li>
</ol>
<p>
Vous pouvez aussi utiliser des outils en ligne comme <a href="https://commentpicker.com/youtube-channel-id.php" target="_blank">Comment Picker</a> pour trouver l'ID.
</p>
<h3>Variables disponibles pour le message</h3>
<p>
Pour le message vous avez accès à ces variables :
</p>
<ul>
<li><code>{0.title}</code> : Titre de la vidéo</li>
<li><code>{0.author}</code> : Nom de la chaîne</li>
<li><code>{0.link}</code> : Lien vers la vidéo</li>
<li><code>{0.video_id}</code> : ID de la vidéo</li>
<li><code>{0.thumbnail}</code> : URL de la miniature</li>
</ul>
<p>
Le message est au format <a href="https://commonmark.org/" target="_blank">common-mark</a> dans la limite de ce que supporte Discord.
</p>
</form>
{% endblock %}
+88
View File
@@ -0,0 +1,88 @@
from flask import render_template, request, redirect, url_for, flash
from webapp import webapp
from database import db
from database.models import YoutubeAlert
from discordbot import bot
from youtubebot.youtube_alert import _get_channel_name
@webapp.route("/youtube-alert")
def openYoutubeAlert():
alerts: list[YoutubeAlert] = YoutubeAlert.query.all()
channels = bot.getAllTextChannel()
for alert in alerts:
for channel in channels:
if alert.notify_channel == channel.id:
alert.notify_channel_name = channel.name
return render_template("youtube-alert.html", alerts=alerts, channels=channels)
@webapp.route("/youtube-alert/add", methods=['POST'])
def addYoutubeAlert():
channel_id = request.form.get('channel_id').strip()
existing = YoutubeAlert.query.filter_by(channel_id=channel_id).first()
if existing:
flash("Cette chaîne YouTube est déjà surveillée.", "error")
return redirect(url_for("openYoutubeAlert"))
channel_name = _get_channel_name(channel_id)
if not channel_name:
flash("Impossible de trouver cette chaîne YouTube. Vérifiez l'ID de la chaîne.", "error")
return redirect(url_for("openYoutubeAlert"))
alert = YoutubeAlert(
enable=True,
channel_id=channel_id,
channel_name=channel_name,
notify_channel=request.form.get('notify_channel'),
message=request.form.get('message')
)
db.session.add(alert)
db.session.commit()
flash(f"Alerte ajoutée pour la chaîne {channel_name}.", "success")
return redirect(url_for("openYoutubeAlert"))
@webapp.route("/youtube-alert/toggle/<int:id>")
def toggleYoutubeAlert(id):
alert: YoutubeAlert = YoutubeAlert.query.get_or_404(id)
alert.enable = not alert.enable
db.session.commit()
return redirect(url_for("openYoutubeAlert"))
@webapp.route("/youtube-alert/edit/<int:id>")
def openEditYoutubeAlert(id):
alert = YoutubeAlert.query.get_or_404(id)
channels = bot.getAllTextChannel()
return render_template("youtube-alert.html", alert=alert, channels=channels)
@webapp.route("/youtube-alert/edit/<int:id>", methods=['POST'])
def submitEditYoutubeAlert(id):
alert: YoutubeAlert = YoutubeAlert.query.get_or_404(id)
new_channel_id = request.form.get('channel_id').strip()
if new_channel_id != alert.channel_id:
channel_name = _get_channel_name(new_channel_id)
if not channel_name:
flash("Impossible de trouver cette chaîne YouTube. Vérifiez l'ID de la chaîne.", "error")
return redirect(url_for("openEditYoutubeAlert", id=id))
alert.channel_id = new_channel_id
alert.channel_name = channel_name
alert.last_video_id = None
alert.notify_channel = request.form.get('notify_channel')
alert.message = request.form.get('message')
db.session.commit()
return redirect(url_for("openYoutubeAlert"))
@webapp.route("/youtube-alert/del/<int:id>")
def delYoutubeAlert(id):
alert = YoutubeAlert.query.get_or_404(id)
db.session.delete(alert)
db.session.commit()
return redirect(url_for("openYoutubeAlert"))
View File
+100
View File
@@ -0,0 +1,100 @@
import logging
import feedparser
from dataclasses import dataclass
from datetime import datetime
from database import db
from database.models import YoutubeAlert
from webapp import webapp
logger = logging.getLogger('youtube-alert')
logger.setLevel(logging.INFO)
YOUTUBE_RSS_URL = "https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
@dataclass
class YoutubeVideo:
video_id: str
title: str
author: str
link: str
published: datetime
thumbnail: str
def _fetch_latest_video(channel_id: str) -> YoutubeVideo | None:
feed_url = YOUTUBE_RSS_URL.format(channel_id=channel_id)
try:
feed = feedparser.parse(feed_url)
if feed.entries:
entry = feed.entries[0]
video_id = entry.yt_videoid
return YoutubeVideo(
video_id=video_id,
title=entry.title,
author=entry.author,
link=entry.link,
published=datetime(*entry.published_parsed[:6]),
thumbnail=f"https://i.ytimg.com/vi/{video_id}/maxresdefault.jpg"
)
except Exception as e:
logger.error(f"Erreur lors de la récupération du flux RSS pour {channel_id}: {e}")
return None
def _get_channel_name(channel_id: str) -> str | None:
feed_url = YOUTUBE_RSS_URL.format(channel_id=channel_id)
try:
feed = feedparser.parse(feed_url)
if feed.feed and hasattr(feed.feed, 'author'):
return feed.feed.author
if feed.entries:
return feed.entries[0].author
except Exception as e:
logger.error(f"Erreur lors de la récupération du nom de chaîne pour {channel_id}: {e}")
return None
async def checkNewVideos(bot):
with webapp.app_context():
alerts: list[YoutubeAlert] = YoutubeAlert.query.filter_by(enable=True).all()
for alert in alerts:
logger.info(f"Vérification de la chaîne : {alert.channel_name or alert.channel_id}")
video = _fetch_latest_video(alert.channel_id)
if video:
if not alert.channel_name:
alert.channel_name = video.author
if alert.last_video_id != video.video_id:
logger.info(f"Nouvelle vidéo détectée : {video.title}")
if alert.last_video_id is not None:
await _notifyAlert(bot, alert, video)
alert.last_video_id = video.video_id
else:
logger.warning(f"Impossible de récupérer les vidéos pour {alert.channel_id}")
db.session.commit()
async def _notifyAlert(bot, alert: YoutubeAlert, video: YoutubeVideo):
try:
message = alert.message.format(video)
logger.info(f"Message de notification : {message}")
bot.loop.create_task(_sendMessage(bot, alert.notify_channel, message))
except Exception as e:
logger.error(f"Erreur lors du formatage du message : {e}")
async def _sendMessage(bot, channel: int, message: str):
logger.info(f"Envoi de notification : {message}")
channel_obj = bot.get_channel(channel)
if channel_obj:
await channel_obj.send(message)
logger.info("Notification envoyée")
else:
logger.error(f"Canal Discord non trouvé : {channel}")