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:
@@ -195,6 +195,21 @@ class YouTubeNotification(db.Model):
|
||||
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):
|
||||
__tablename__ = 'freeloot_entry'
|
||||
entry_id = db.Column(db.String(256), primary_key=True)
|
||||
|
||||
@@ -178,6 +178,21 @@ CREATE TABLE IF NOT EXISTS `webapp_page_permission` (
|
||||
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` (
|
||||
entry_id VARCHAR(256) PRIMARY KEY
|
||||
);
|
||||
|
||||
+147
-51
@@ -2,6 +2,7 @@ import logging
|
||||
import asyncio
|
||||
import xml.etree.ElementTree as ET
|
||||
import requests
|
||||
import discord
|
||||
|
||||
from database import db
|
||||
from database.models import YouTubeNotification
|
||||
@@ -24,14 +25,32 @@ async def checkYouTubeVideos():
|
||||
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}")
|
||||
db.session.rollback()
|
||||
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}")
|
||||
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):
|
||||
@@ -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()):
|
||||
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':
|
||||
videos.append((video_id, {
|
||||
'title': video_title,
|
||||
'url': video_url,
|
||||
'published': published_at,
|
||||
'channel_name': channel_name,
|
||||
'thumbnail': thumbnail,
|
||||
'is_short': is_short
|
||||
}))
|
||||
videos.append((video_id, video_data))
|
||||
elif notification.video_type == 'short' and is_short:
|
||||
videos.append((video_id, {
|
||||
'title': video_title,
|
||||
'url': video_url,
|
||||
'published': published_at,
|
||||
'channel_name': channel_name,
|
||||
'thumbnail': thumbnail,
|
||||
'is_short': is_short
|
||||
}))
|
||||
videos.append((video_id, video_data))
|
||||
elif notification.video_type == 'video' and not is_short:
|
||||
videos.append((video_id, {
|
||||
'title': video_title,
|
||||
'url': video_url,
|
||||
'published': published_at,
|
||||
'channel_name': channel_name,
|
||||
'thumbnail': thumbnail,
|
||||
'is_short': is_short
|
||||
}))
|
||||
videos.append((video_id, video_data))
|
||||
|
||||
videos.sort(key=lambda x: x[1]['published'], reverse=True)
|
||||
|
||||
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}")
|
||||
_save_video_history(notification.id, latest_video_id, latest_video, notified=False)
|
||||
notification.last_video_id = latest_video_id
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
# Vérifications normales ensuite
|
||||
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
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
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}")
|
||||
await _notifyVideo(notification, latest_video, latest_video_id)
|
||||
notification.last_video_id = latest_video_id
|
||||
db.session.commit()
|
||||
embed_config = _extract_embed_config(notification)
|
||||
success = await _notifyVideo(embed_config, latest_video, latest_video_id)
|
||||
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:
|
||||
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
|
||||
try:
|
||||
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', '')
|
||||
is_short = video_data.get('is_short', False)
|
||||
|
||||
message_template = embed_config.get('message_template', '')
|
||||
try:
|
||||
message = notification.message.format(
|
||||
message = message_template.format(
|
||||
channel_name=channel_name or 'Inconnu',
|
||||
video_title=video_title or 'Sans titre',
|
||||
video_url=video_url,
|
||||
@@ -161,15 +209,16 @@ async def _notifyVideo(notification: YouTubeNotification, video_data: dict, vide
|
||||
published_at=published_at or '',
|
||||
is_short=is_short
|
||||
)
|
||||
except KeyError as e:
|
||||
logger.error(f"Variable manquante dans le message de notification: {e}")
|
||||
except (KeyError, AttributeError, ValueError) as e:
|
||||
logger.error(f"Erreur de formatage du message: {e}")
|
||||
message = f"🎥 Nouvelle vidéo de {channel_name}: [{video_title}]({video_url})"
|
||||
|
||||
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:
|
||||
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:
|
||||
@@ -190,26 +239,25 @@ def _format_embed_text(text: str, channel_name: str, video_title: str, video_url
|
||||
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
|
||||
try:
|
||||
discord_channel = bot.get_channel(notification.notify_channel)
|
||||
discord_channel = bot.get_channel(embed_config['notify_channel'])
|
||||
if not discord_channel:
|
||||
logger.error(f"Canal Discord {notification.notify_channel} introuvable")
|
||||
return
|
||||
logger.error(f"Canal Discord {embed_config['notify_channel']} introuvable")
|
||||
return False
|
||||
|
||||
import discord
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
try:
|
||||
embed_color = int(notification.embed_color or 'FF0000', 16)
|
||||
embed_color = int(embed_config['embed_color'], 16)
|
||||
except ValueError:
|
||||
embed_color = 0xFF0000
|
||||
|
||||
embed = discord.Embed(
|
||||
title=embed_title,
|
||||
title=embed_title_text,
|
||||
url=video_url,
|
||||
color=embed_color
|
||||
)
|
||||
@@ -217,19 +265,19 @@ async def _sendMessage(notification: YouTubeNotification, message: str, video_ur
|
||||
if 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_icon_raw = (notification.embed_author_icon or "").strip()
|
||||
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 = 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"
|
||||
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)
|
||||
|
||||
if notification.embed_image and thumbnail:
|
||||
if embed_config['embed_image'] and thumbnail:
|
||||
embed.set_image(url=thumbnail)
|
||||
|
||||
if notification.embed_footer:
|
||||
footer_text = _format_embed_text(notification.embed_footer, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short)
|
||||
if embed_config['embed_footer']:
|
||||
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:
|
||||
embed.set_footer(text=footer_text)
|
||||
|
||||
@@ -238,6 +286,54 @@ async def _sendMessage(notification: YouTubeNotification, message: str, video_ur
|
||||
else:
|
||||
await discord_channel.send(embed=embed)
|
||||
logger.info(f"Notification YouTube envoyée avec succès")
|
||||
return True
|
||||
|
||||
except Exception as 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))
|
||||
|
||||
@@ -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 %}
|
||||
@@ -16,12 +16,16 @@
|
||||
</script>
|
||||
{% 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">
|
||||
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.
|
||||
Quand une nouvelle vidéo est détectée, le bot enverra une notification sur Discord.
|
||||
</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>
|
||||
|
||||
|
||||
+43
-1
@@ -5,7 +5,7 @@ from flask import render_template, request, redirect, url_for
|
||||
from webapp import webapp
|
||||
from webapp.auth import require_page, can_write_page
|
||||
from database import db
|
||||
from database.models import YouTubeNotification
|
||||
from database.models import YouTubeNotification, YouTubeVideoHistory
|
||||
from discordbot import bot
|
||||
|
||||
|
||||
@@ -190,6 +190,48 @@ def delYouTube(id):
|
||||
if not can_write_page("youtube"):
|
||||
return render_template("403.html"), 403
|
||||
notification = YouTubeNotification.query.get_or_404(id)
|
||||
YouTubeVideoHistory.query.filter_by(notification_id=id).delete()
|
||||
db.session.delete(notification)
|
||||
db.session.commit()
|
||||
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}))
|
||||
|
||||
Reference in New Issue
Block a user