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)
|
revoked = db.Column(db.Boolean, default=False)
|
||||||
last_sync = db.Column(db.DateTime)
|
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,
|
`revoked` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
`last_sync` DATETIME NOT NULL
|
`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.updateStatus())
|
||||||
self.loop.create_task(self.updateHumbleBundle())
|
self.loop.create_task(self.updateHumbleBundle())
|
||||||
self.loop.create_task(self.updateFreeGames())
|
self.loop.create_task(self.updateFreeGames())
|
||||||
|
self.loop.create_task(self.updateYoutubeAlerts())
|
||||||
self.loop.create_task(self._periodic_stats_update())
|
self.loop.create_task(self._periodic_stats_update())
|
||||||
|
|
||||||
def _update_shared_stats(self):
|
def _update_shared_stats(self):
|
||||||
@@ -115,6 +116,12 @@ class DiscordBot(discord.Client):
|
|||||||
await checkFreeGamesAndNotify(self)
|
await checkFreeGamesAndNotify(self)
|
||||||
await asyncio.sleep(60*60) # Vérification toutes les heures
|
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]:
|
def getAllTextChannel(self) -> list[TextChannel]:
|
||||||
channels = []
|
channels = []
|
||||||
for channel in self.get_all_channels():
|
for channel in self.get_all_channels():
|
||||||
|
|||||||
+1
-1
@@ -4,4 +4,4 @@ import os
|
|||||||
webapp = Flask(__name__)
|
webapp = Flask(__name__)
|
||||||
webapp.secret_key = os.environ.get('FLASK_SECRET_KEY', 'mamie-henriette-secret-key-change-me')
|
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);
|
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;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,12 @@
|
|||||||
<li><a href="/live-alert">📺 Alerte Live</a></li>
|
<li><a href="/live-alert">📺 Alerte Live</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</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">
|
<li class="has-submenu">
|
||||||
<a href="#">Outils</a>
|
<a href="#">Outils</a>
|
||||||
<ul class="submenu">
|
<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