Ajout de la surveillance des nouvelles vidéos YouTube
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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"))
|
||||
@@ -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}")
|
||||
Reference in New Issue
Block a user