Ajout d'un historique des vidéos YouTube dans la base de données et l'interface utilisateur. Création de la table youtube_video_history pour stocker les vidéos détectées, avec des fonctionnalités pour afficher l'historique et forcer l'envoi de notifications Discord. Mise à jour des modèles, des routes et des templates pour intégrer cette nouvelle fonctionnalité.

This commit is contained in:
2026-03-09 18:31:27 +01:00
parent 540d23a3cf
commit 2e83096550
6 changed files with 348 additions and 53 deletions
+15
View File
@@ -195,6 +195,21 @@ class YouTubeNotification(db.Model):
embed_image = db.Column(db.Boolean, default=True) embed_image = db.Column(db.Boolean, default=True)
class YouTubeVideoHistory(db.Model):
__tablename__ = 'youtube_video_history'
id = db.Column(db.Integer, primary_key=True)
notification_id = db.Column(db.Integer, db.ForeignKey('youtube_notification.id'), nullable=False)
video_id = db.Column(db.String(128), nullable=False)
title = db.Column(db.String(512))
url = db.Column(db.String(512))
channel_name = db.Column(db.String(256))
thumbnail = db.Column(db.String(512))
published_at = db.Column(db.String(64))
is_short = db.Column(db.Boolean, default=False)
notified = db.Column(db.Boolean, default=False)
detected_at = db.Column(db.DateTime, default=datetime.utcnow)
class FreeLootEntry(db.Model): class FreeLootEntry(db.Model):
__tablename__ = 'freeloot_entry' __tablename__ = 'freeloot_entry'
entry_id = db.Column(db.String(256), primary_key=True) entry_id = db.Column(db.String(256), primary_key=True)
+15
View File
@@ -178,6 +178,21 @@ CREATE TABLE IF NOT EXISTS `webapp_page_permission` (
description VARCHAR(256) NULL description VARCHAR(256) NULL
); );
CREATE TABLE IF NOT EXISTS `youtube_video_history` (
id INTEGER PRIMARY KEY AUTOINCREMENT,
`notification_id` INTEGER NOT NULL,
`video_id` VARCHAR(128) NOT NULL,
`title` VARCHAR(512),
`url` VARCHAR(512),
`channel_name` VARCHAR(256),
`thumbnail` VARCHAR(512),
`published_at` VARCHAR(64),
`is_short` BOOLEAN NOT NULL DEFAULT FALSE,
`notified` BOOLEAN NOT NULL DEFAULT FALSE,
`detected_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`notification_id`) REFERENCES `youtube_notification`(`id`) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS `freeloot_entry` ( CREATE TABLE IF NOT EXISTS `freeloot_entry` (
entry_id VARCHAR(256) PRIMARY KEY entry_id VARCHAR(256) PRIMARY KEY
); );
+147 -51
View File
@@ -2,6 +2,7 @@ import logging
import asyncio import asyncio
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import requests import requests
import discord
from database import db from database import db
from database.models import YouTubeNotification from database.models import YouTubeNotification
@@ -24,14 +25,32 @@ async def checkYouTubeVideos():
await _checkChannelVideos(notification, is_first_check=_youtube_first_check) await _checkChannelVideos(notification, is_first_check=_youtube_first_check)
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de la vérification de la chaîne {notification.channel_id}: {e}") logger.error(f"Erreur lors de la vérification de la chaîne {notification.channel_id}: {e}")
db.session.rollback()
continue continue
# Après la première vérification complète, on désactive le flag
if _youtube_first_check: if _youtube_first_check:
_youtube_first_check = False _youtube_first_check = False
logger.info("YouTube: première vérification terminée, notifications activées") logger.info("YouTube: première vérification terminée, notifications activées")
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de la vérification YouTube: {e}") logger.error(f"Erreur lors de la vérification YouTube: {e}")
db.session.rollback()
def _extract_embed_config(notification: YouTubeNotification) -> dict:
"""Extrait toutes les valeurs ORM nécessaires à l'envoi dans un dict plain Python.
Doit être appelé pendant que le contexte Flask est actif."""
return {
'notify_channel': notification.notify_channel,
'message_template': notification.message or '',
'embed_title': notification.embed_title,
'embed_description': notification.embed_description,
'embed_color': notification.embed_color or 'FF0000',
'embed_footer': notification.embed_footer,
'embed_author_name': notification.embed_author_name,
'embed_author_icon': (notification.embed_author_icon or '').strip(),
'embed_thumbnail': bool(notification.embed_thumbnail),
'embed_image': bool(notification.embed_image),
}
async def _checkChannelVideos(notification: YouTubeNotification, is_first_check: bool = False): async def _checkChannelVideos(notification: YouTubeNotification, is_first_check: bool = False):
@@ -84,64 +103,92 @@ async def _checkChannelVideos(notification: YouTubeNotification, is_first_check:
if video_title and ('#shorts' in video_title.lower() or '#short' in video_title.lower()): if video_title and ('#shorts' in video_title.lower() or '#short' in video_title.lower()):
is_short = True is_short = True
video_data = {
'title': video_title,
'url': video_url,
'published': published_at,
'channel_name': channel_name,
'thumbnail': thumbnail,
'is_short': is_short
}
if notification.video_type == 'all': if notification.video_type == 'all':
videos.append((video_id, { videos.append((video_id, video_data))
'title': video_title,
'url': video_url,
'published': published_at,
'channel_name': channel_name,
'thumbnail': thumbnail,
'is_short': is_short
}))
elif notification.video_type == 'short' and is_short: elif notification.video_type == 'short' and is_short:
videos.append((video_id, { videos.append((video_id, video_data))
'title': video_title,
'url': video_url,
'published': published_at,
'channel_name': channel_name,
'thumbnail': thumbnail,
'is_short': is_short
}))
elif notification.video_type == 'video' and not is_short: elif notification.video_type == 'video' and not is_short:
videos.append((video_id, { videos.append((video_id, video_data))
'title': video_title,
'url': video_url,
'published': published_at,
'channel_name': channel_name,
'thumbnail': thumbnail,
'is_short': is_short
}))
videos.sort(key=lambda x: x[1]['published'], reverse=True) videos.sort(key=lambda x: x[1]['published'], reverse=True)
if videos: if videos:
latest_video_id, latest_video = videos[0] 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 is_first_check:
if not notification.last_video_id or notification.last_video_id != latest_video_id: 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}") logger.info(f"YouTube: synchronisation initiale pour {channel_id}, dernière vidéo: {latest_video_id}")
_save_video_history(notification.id, latest_video_id, latest_video, notified=False)
notification.last_video_id = latest_video_id notification.last_video_id = latest_video_id
db.session.commit() db.session.commit()
return return
# Vérifications normales ensuite
if not notification.last_video_id: if not notification.last_video_id:
_save_video_history(notification.id, latest_video_id, latest_video, notified=False)
notification.last_video_id = latest_video_id notification.last_video_id = latest_video_id
db.session.commit() db.session.commit()
return return
if latest_video_id != notification.last_video_id: if latest_video_id != notification.last_video_id:
logger.info(f"Nouvelle vidéo détectée: {latest_video_id} pour la chaîne {notification.channel_id}") logger.info(f"Nouvelle vidéo détectée: {latest_video_id} pour la chaîne {notification.channel_id}")
await _notifyVideo(notification, latest_video, latest_video_id) embed_config = _extract_embed_config(notification)
notification.last_video_id = latest_video_id success = await _notifyVideo(embed_config, latest_video, latest_video_id)
db.session.commit() if success:
_save_video_history(notification.id, latest_video_id, latest_video, notified=True)
notification.last_video_id = latest_video_id
db.session.commit()
else:
_save_video_history(notification.id, latest_video_id, latest_video, notified=False)
notification.last_video_id = latest_video_id
db.session.commit()
logger.warning(f"Notification échouée pour {latest_video_id}, vidéo enregistrée comme non notifiée")
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de la vérification des vidéos: {e}") logger.error(f"Erreur lors de la vérification des vidéos: {e}")
db.session.rollback()
async def _notifyVideo(notification: YouTubeNotification, video_data: dict, video_id: str): def _save_video_history(notification_id: int, video_id: str, video_data: dict, notified: bool):
"""Enregistre une vidéo dans l'historique (ne fait rien si déjà présente)."""
from database.models import YouTubeVideoHistory
try:
existing = YouTubeVideoHistory.query.filter_by(
notification_id=notification_id, video_id=video_id
).first()
if existing:
if notified and not existing.notified:
existing.notified = True
db.session.commit()
return
entry = YouTubeVideoHistory(
notification_id=notification_id,
video_id=video_id,
title=video_data.get('title', 'Sans titre'),
url=video_data.get('url', f"https://www.youtube.com/watch?v={video_id}"),
channel_name=video_data.get('channel_name', 'Inconnu'),
thumbnail=video_data.get('thumbnail'),
published_at=video_data.get('published', ''),
is_short=video_data.get('is_short', False),
notified=notified,
)
db.session.add(entry)
db.session.commit()
except Exception as e:
logger.error(f"Erreur lors de l'enregistrement de l'historique vidéo: {e}")
db.session.rollback()
async def _notifyVideo(embed_config: dict, video_data: dict, video_id: str) -> bool:
"""Envoie la notification Discord. Retourne True si l'envoi a réussi."""
from discordbot import bot from discordbot import bot
try: try:
channel_name = video_data.get('channel_name', 'Inconnu') channel_name = video_data.get('channel_name', 'Inconnu')
@@ -151,8 +198,9 @@ async def _notifyVideo(notification: YouTubeNotification, video_data: dict, vide
published_at = video_data.get('published', '') published_at = video_data.get('published', '')
is_short = video_data.get('is_short', False) is_short = video_data.get('is_short', False)
message_template = embed_config.get('message_template', '')
try: try:
message = notification.message.format( message = message_template.format(
channel_name=channel_name or 'Inconnu', channel_name=channel_name or 'Inconnu',
video_title=video_title or 'Sans titre', video_title=video_title or 'Sans titre',
video_url=video_url, video_url=video_url,
@@ -161,15 +209,16 @@ async def _notifyVideo(notification: YouTubeNotification, video_data: dict, vide
published_at=published_at or '', published_at=published_at or '',
is_short=is_short is_short=is_short
) )
except KeyError as e: except (KeyError, AttributeError, ValueError) as e:
logger.error(f"Variable manquante dans le message de notification: {e}") logger.error(f"Erreur de formatage du message: {e}")
message = f"🎥 Nouvelle vidéo de {channel_name}: [{video_title}]({video_url})" message = f"🎥 Nouvelle vidéo de {channel_name}: [{video_title}]({video_url})"
logger.info(f"Envoi de notification YouTube: {message}") logger.info(f"Envoi de notification YouTube: {message}")
bot.loop.create_task(_sendMessage(notification, message, video_url, thumbnail, video_title, channel_name, video_id, published_at, is_short)) return await _sendMessage(embed_config, message, video_url, thumbnail, video_title, channel_name, video_id, published_at, is_short)
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de la notification: {e}") logger.error(f"Erreur lors de la notification: {e}")
return False
def _format_embed_text(text: str, channel_name: str, video_title: str, video_url: str, video_id: str, thumbnail: str, published_at: str, is_short: bool) -> str: def _format_embed_text(text: str, channel_name: str, video_title: str, video_url: str, video_id: str, thumbnail: str, published_at: str, is_short: bool) -> str:
@@ -190,26 +239,25 @@ def _format_embed_text(text: str, channel_name: str, video_title: str, video_url
return text return text
async def _sendMessage(notification: YouTubeNotification, message: str, video_url: str, thumbnail: str, video_title: str, channel_name: str, video_id: str, published_at: str, is_short: bool): async def _sendMessage(embed_config: dict, message: str, video_url: str, thumbnail: str, video_title: str, channel_name: str, video_id: str, published_at: str, is_short: bool) -> bool:
"""Envoie le message Discord. Retourne True si l'envoi a réussi."""
from discordbot import bot from discordbot import bot
try: try:
discord_channel = bot.get_channel(notification.notify_channel) discord_channel = bot.get_channel(embed_config['notify_channel'])
if not discord_channel: if not discord_channel:
logger.error(f"Canal Discord {notification.notify_channel} introuvable") logger.error(f"Canal Discord {embed_config['notify_channel']} introuvable")
return return False
import discord embed_title_text = _format_embed_text(embed_config['embed_title'], channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if embed_config['embed_title'] else video_title
embed_description = _format_embed_text(embed_config['embed_description'], channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if embed_config['embed_description'] else None
embed_title = _format_embed_text(notification.embed_title, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if notification.embed_title else video_title
embed_description = _format_embed_text(notification.embed_description, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if notification.embed_description else None
try: try:
embed_color = int(notification.embed_color or 'FF0000', 16) embed_color = int(embed_config['embed_color'], 16)
except ValueError: except ValueError:
embed_color = 0xFF0000 embed_color = 0xFF0000
embed = discord.Embed( embed = discord.Embed(
title=embed_title, title=embed_title_text,
url=video_url, url=video_url,
color=embed_color color=embed_color
) )
@@ -217,19 +265,19 @@ async def _sendMessage(notification: YouTubeNotification, message: str, video_ur
if embed_description: if embed_description:
embed.description = embed_description embed.description = embed_description
author_name = _format_embed_text(notification.embed_author_name, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if notification.embed_author_name else channel_name author_name = _format_embed_text(embed_config['embed_author_name'], channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if embed_config['embed_author_name'] else channel_name
author_icon_raw = (notification.embed_author_icon or "").strip() author_icon_raw = embed_config['embed_author_icon']
author_icon = author_icon_raw if author_icon_raw.startswith(("http://", "https://")) else "https://www.youtube.com/img/desktop/yt_1200.png" author_icon = author_icon_raw if author_icon_raw.startswith(("http://", "https://")) else "https://www.youtube.com/img/desktop/yt_1200.png"
embed.set_author(name=author_name, icon_url=author_icon) embed.set_author(name=author_name, icon_url=author_icon)
if notification.embed_thumbnail and thumbnail: if embed_config['embed_thumbnail'] and thumbnail:
embed.set_thumbnail(url=thumbnail) embed.set_thumbnail(url=thumbnail)
if notification.embed_image and thumbnail: if embed_config['embed_image'] and thumbnail:
embed.set_image(url=thumbnail) embed.set_image(url=thumbnail)
if notification.embed_footer: if embed_config['embed_footer']:
footer_text = _format_embed_text(notification.embed_footer, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) footer_text = _format_embed_text(embed_config['embed_footer'], channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short)
if footer_text: if footer_text:
embed.set_footer(text=footer_text) embed.set_footer(text=footer_text)
@@ -238,6 +286,54 @@ async def _sendMessage(notification: YouTubeNotification, message: str, video_ur
else: else:
await discord_channel.send(embed=embed) await discord_channel.send(embed=embed)
logger.info(f"Notification YouTube envoyée avec succès") logger.info(f"Notification YouTube envoyée avec succès")
return True
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de l'envoi du message Discord: {e}") logger.error(f"Erreur lors de l'envoi du message Discord: {e}")
return False
async def _send_video_notification_async(history_id: int) -> tuple[bool, str]:
"""Force l'envoi d'une notification pour une vidéo de l'historique. Retourne (succès, message)."""
from database.models import YouTubeVideoHistory
with webapp.app_context():
history = YouTubeVideoHistory.query.get(history_id)
if not history:
return (False, "Vidéo introuvable dans l'historique.")
notification = YouTubeNotification.query.get(history.notification_id)
if not notification:
return (False, "Notification YouTube associée introuvable.")
embed_config = _extract_embed_config(notification)
video_data = {
'title': history.title or 'Sans titre',
'url': history.url or f"https://www.youtube.com/watch?v={history.video_id}",
'channel_name': history.channel_name or 'Inconnu',
'thumbnail': history.thumbnail or '',
'published': history.published_at or '',
'is_short': history.is_short,
}
success = await _notifyVideo(embed_config, video_data, history.video_id)
if success:
history.notified = True
db.session.commit()
return (True, "Notification envoyée sur Discord.")
else:
db.session.rollback()
return (False, "Échec de l'envoi sur Discord.")
def send_video_notification_sync(history_id: int) -> tuple[bool, str]:
"""Appel synchrone pour forcer une notification (depuis la webapp)."""
from discordbot import bot
try:
future = asyncio.run_coroutine_threadsafe(
_send_video_notification_async(history_id),
bot.loop,
)
return future.result(timeout=15)
except Exception as e:
logger.error(f"send_video_notification_sync: {e}")
return (False, str(e))
+123
View File
@@ -0,0 +1,123 @@
{% extends "template.html" %}
{% block content %}
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Historique des vidéos YouTube</h1>
<a href="{{ url_for('openYouTube') }}" class="px-4 py-2 bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 font-medium rounded-lg transition-colors text-sm 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="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
Retour
</a>
</div>
{% if msg %}
<div id="alert-msg" class="mb-4 p-4 rounded-lg {{ 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300' if msg_type == 'error' else 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-300' }}">
{{ msg }}
</div>
<script>
setTimeout(function() {
var el = document.getElementById('alert-msg');
if (el) el.style.display = 'none';
}, 5000);
</script>
{% endif %}
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
<p class="text-gray-700 dark:text-gray-300">
Historique des vidéos détectées par le bot. Les vidéos non notifiées peuvent être envoyées manuellement sur Discord.
</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ total }} vidéo{{ 's' if total > 1 else '' }} au total.</p>
</div>
</div>
{% if history %}
<div class="space-y-4">
{% for entry in history %}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-md transition-shadow">
<div class="flex flex-col sm:flex-row">
{% if entry.thumbnail %}
<a href="{{ entry.url }}" target="_blank" class="shrink-0 sm:w-48 h-28 overflow-hidden bg-gray-100 dark:bg-gray-700">
<img src="{{ entry.thumbnail }}" alt="" class="w-full h-full object-cover">
</a>
{% endif %}
<div class="flex-1 p-4 flex flex-col justify-between min-w-0">
<div>
<div class="flex items-start justify-between gap-3 mb-1">
<a href="{{ entry.url }}" target="_blank" class="text-base font-semibold text-gray-900 dark:text-white hover:text-red-600 dark:hover:text-red-400 transition-colors truncate">
{{ entry.title or 'Sans titre' }}
</a>
<div class="shrink-0 flex items-center gap-2">
{% if entry.is_short %}
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300">Short</span>
{% endif %}
{% if entry.notified %}
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 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="M5 13l4 4L19 7"></path></svg>
Notifié
</span>
{% else %}
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 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="M6 18L18 6M6 6l12 12"></path></svg>
Non notifié
</span>
{% endif %}
</div>
</div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400">
<span>{{ entry.channel_name or 'Inconnu' }}</span>
{% if entry.published_at %}
<span>{{ entry.published_at[:10] }}</span>
{% endif %}
{% if notification_map.get(entry.notification_id) %}
<span class="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded font-mono">{{ notification_map[entry.notification_id].channel_id }}</span>
{% endif %}
</div>
</div>
<div class="flex items-center gap-2 mt-3">
<a href="{{ entry.url }}" target="_blank" class="px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors">
Voir sur YouTube
</a>
<form action="{{ url_for('forceYouTubeNotify', history_id=entry.id) }}" method="POST" class="inline"
onsubmit="return confirm('Envoyer la notification Discord pour cette vidéo ?')">
<button type="submit" class="px-3 py-1.5 text-xs font-medium rounded-lg transition-colors flex items-center gap-1
{% if entry.notified %}
text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700
{% else %}
text-white bg-red-600 hover:bg-red-700
{% endif %}">
<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="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
{{ 'Re-notifier' if entry.notified else 'Forcer la notification' }}
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% if total_pages > 1 %}
<div class="mt-8 flex items-center justify-center gap-2">
{% if page > 1 %}
<a href="{{ url_for('youtubeHistory', page=page-1) }}" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg transition-colors text-sm">
Précédent
</a>
{% endif %}
<span class="px-4 py-2 text-sm text-gray-600 dark:text-gray-400">
Page {{ page }} / {{ total_pages }}
</span>
{% if page < total_pages %}
<a href="{{ url_for('youtubeHistory', page=page+1) }}" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg transition-colors text-sm">
Suivant
</a>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-8 text-center">
<svg class="w-12 h-12 mx-auto text-gray-400 dark:text-gray-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
<p class="text-gray-500 dark:text-gray-400">Aucune vidéo détectée pour le moment. L'historique se remplira au fur et à mesure des vérifications.</p>
</div>
{% endif %}
{% endblock %}
+5 -1
View File
@@ -16,12 +16,16 @@
</script> </script>
{% endif %} {% endif %}
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4"> <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 flex items-center justify-between">
<p class="text-gray-700 dark:text-gray-300"> <p class="text-gray-700 dark:text-gray-300">
Liste des chaînes YouTube surveillées pour les notifications de nouvelles vidéos. Liste des chaînes YouTube surveillées pour les notifications de nouvelles vidéos.
Le bot vérifie toutes les 5 minutes les nouvelles vidéos des chaînes en dessous. Le bot vérifie toutes les 5 minutes les nouvelles vidéos des chaînes en dessous.
Quand une nouvelle vidéo est détectée, le bot enverra une notification sur Discord. Quand une nouvelle vidéo est détectée, le bot enverra une notification sur Discord.
</p> </p>
<a href="{{ url_for('youtubeHistory') }}" class="ml-4 shrink-0 px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-lg transition-colors 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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Historique
</a>
</div> </div>
</div> </div>
+43 -1
View File
@@ -5,7 +5,7 @@ from flask import render_template, request, redirect, url_for
from webapp import webapp from webapp import webapp
from webapp.auth import require_page, can_write_page from webapp.auth import require_page, can_write_page
from database import db from database import db
from database.models import YouTubeNotification from database.models import YouTubeNotification, YouTubeVideoHistory
from discordbot import bot from discordbot import bot
@@ -190,6 +190,48 @@ def delYouTube(id):
if not can_write_page("youtube"): if not can_write_page("youtube"):
return render_template("403.html"), 403 return render_template("403.html"), 403
notification = YouTubeNotification.query.get_or_404(id) notification = YouTubeNotification.query.get_or_404(id)
YouTubeVideoHistory.query.filter_by(notification_id=id).delete()
db.session.delete(notification) db.session.delete(notification)
db.session.commit() db.session.commit()
return redirect(url_for("openYouTube")) return redirect(url_for("openYouTube"))
@webapp.route("/youtube/history")
@require_page("youtube")
def youtubeHistory():
page = request.args.get('page', 1, type=int)
per_page = 20
history_query = YouTubeVideoHistory.query.order_by(YouTubeVideoHistory.detected_at.desc())
total = history_query.count()
history = history_query.offset((page - 1) * per_page).limit(per_page).all()
total_pages = (total + per_page - 1) // per_page
notification_map = {}
for entry in history:
if entry.notification_id not in notification_map:
notif = YouTubeNotification.query.get(entry.notification_id)
notification_map[entry.notification_id] = notif
msg = request.args.get('msg')
msg_type = request.args.get('type', 'info')
return render_template(
"youtube-history.html",
history=history,
notification_map=notification_map,
page=page,
total_pages=total_pages,
total=total,
msg=msg,
msg_type=msg_type,
)
@webapp.route("/youtube/notify/<int:history_id>", methods=['POST'])
@require_page("youtube")
def forceYouTubeNotify(history_id):
if not can_write_page("youtube"):
return render_template("403.html"), 403
from discordbot.youtube import send_video_notification_sync
success, message = send_video_notification_sync(history_id)
msg_type = 'success' if success else 'error'
return redirect(url_for("youtubeHistory") + "?" + urlencode({'msg': message, 'type': msg_type}))