From 1f34d8146372dc496888984fd2f51ee841016ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:23:58 +0200 Subject: [PATCH 01/21] refactor: reconcile cache.py with file_manager.py - Unify URL deduplication: both systems now use SHA-256 - Remove dead code from cache.py (BOT_VERSION, AUTHORIZED_USER, get_cached_file_path) - Remove unused create_folders() from file_manager.py - Wire add_to_cache() into download.py and music.py so the cache is actually populated - Cache now tracks video and audio (#audio suffix) entries --- commands/download.py | 3 +++ commands/music.py | 3 +++ utils/cache.py | 34 ++++++---------------------------- utils/file_manager.py | 13 ------------- 4 files changed, 12 insertions(+), 41 deletions(-) diff --git a/commands/download.py b/commands/download.py index 2093a71..7e72d6a 100644 --- a/commands/download.py +++ b/commands/download.py @@ -1,9 +1,11 @@ # commands/download.py +import os import yt_dlp from utils.logger import console_logger from utils.file_manager import is_already_downloaded, save_download from utils.disk_manager import check_and_clean_if_needed from utils.retention import set_retention +from utils.cache import add_to_cache from utils.upload import upload_file def download(update, context): @@ -44,6 +46,7 @@ def download(update, context): filename = ydl.prepare_filename(info) save_download(url) set_retention(filename) + add_to_cache(url, os.path.getsize(filename)) console_logger.info(f"[DOWNLOAD] Téléchargement terminé pour l'URL: {url} par {update.message.from_user.username}. Envoi du fichier...") upload_file(update, filename, context) break diff --git a/commands/music.py b/commands/music.py index e5fe788..56c3b26 100644 --- a/commands/music.py +++ b/commands/music.py @@ -5,6 +5,7 @@ from utils.logger import console_logger from utils.file_manager import is_already_downloaded, save_download from utils.retention import set_retention +from utils.cache import add_to_cache from utils.upload import upload_file from config import FFMPEG_PATH @@ -40,6 +41,7 @@ def music(update, context): info = ydl.extract_info(url, download=True) video_file = ydl.prepare_filename(info) save_download(url) + add_to_cache(url, os.path.getsize(video_file)) console_logger.info(f"[MUSIC] Téléchargement terminé: {video_file} par {update.message.from_user.username}") break except Exception as e: @@ -59,6 +61,7 @@ def music(update, context): stream = ffmpeg.input(video_file) stream = ffmpeg.output(stream, audio_file, format='mp3', acodec='libmp3lame', audio_bitrate='192k') ffmpeg.run(stream, cmd=FFMPEG_PATH, quiet=True) + add_to_cache(url + "#audio", os.path.getsize(audio_file)) console_logger.info(f"[MUSIC] Conversion terminée: {audio_file} pour {update.message.from_user.username}") except Exception as e: update.message.reply_text("Erreur lors de la conversion en audio.") diff --git a/utils/cache.py b/utils/cache.py index 11f5808..4614bad 100644 --- a/utils/cache.py +++ b/utils/cache.py @@ -4,15 +4,9 @@ import hashlib from utils.logger import console_logger -# Bot version related constants (assuming these are defined in main.py or a config file) -# For now, hardcoding as example: -BOT_VERSION = "V9.1" - -# Cache configuration -SMALL_FILE_THRESHOLD = 5 * 1024 * 1024 # 5 MB -LONG_TTL = 24 * 3600 # 24 hours for files ≤5MB -STANDARD_TTL = 1 * 3600 # 1 hour for files >5MB -AUTHORIZED_USER = "overstylefr" +SMALL_FILE_THRESHOLD = 5 * 1024 * 1024 +LONG_TTL = 24 * 3600 +STANDARD_TTL = 1 * 3600 CACHE_FILE = "download_temp/cache_metadata.json" download_cache = {} @@ -35,14 +29,12 @@ def load_cache(): def save_cache(): try: - # Ensure the directory exists cache_dir = os.path.dirname(CACHE_FILE) if cache_dir and not os.path.exists(cache_dir): os.makedirs(cache_dir) console_logger.info(f'Created cache directory: {cache_dir}') - with open(CACHE_FILE, 'w') as f: - json.dump(download_cache, f, indent=4) # Use indent for readability + json.dump(download_cache, f, indent=4) console_logger.info(f'Cache saved to {CACHE_FILE}') except Exception as e: console_logger.error(f'An error occurred saving cache to {CACHE_FILE}: {e}') @@ -54,26 +46,12 @@ def is_cache_valid(link_hash): if link_hash not in download_cache: return False timestamp, size = download_cache[link_hash] - current_time = time.time() ttl = get_ttl(size) - is_valid = (current_time - timestamp) < ttl - # console_logger.debug(f"Cache check for hash {link_hash}: valid={is_valid}, age={(current_time - timestamp):.0f}s, ttl={ttl}s") - return is_valid + return (time.time() - timestamp) < ttl def add_to_cache(link, file_size): - """Adds or updates an entry in the cache.""" - link_hash = hashlib.md5(link.encode()).hexdigest() + link_hash = hashlib.sha256(link.encode()).hexdigest() download_cache[link_hash] = (time.time(), file_size) save_cache() return link_hash -def get_cached_file_path(link_hash): - """Tries to find the actual file path from the hash. Assumes a known file structure.""" - # This is a placeholder, a more robust solution might be needed if extensions vary wildly - # or if files are stored with different naming conventions. - base_path = os.path.join("download_temp", link_hash) - for ext in ['.mp4', '.mkv', '.webm', '.avi', '.mov', '.mp3', '.m4a', '.ogg', '.wav']: - if os.path.exists(base_path + ext): - return base_path + ext - return None - diff --git a/utils/file_manager.py b/utils/file_manager.py index 7e86bd0..ab988a8 100644 --- a/utils/file_manager.py +++ b/utils/file_manager.py @@ -1,23 +1,10 @@ import os import hashlib -import shutil from utils.logger import console_logger DOWNLOADS_DIR = "downloads" HASH_FILE = os.path.join(DOWNLOADS_DIR, "hashes.txt") -def create_folders(): - console_logger.info("[FILE_MANAGER] Réinitialisation du dossier downloads...") - if os.path.exists(DOWNLOADS_DIR): - shutil.rmtree(DOWNLOADS_DIR) - console_logger.info("[FILE_MANAGER] Dossier downloads supprimé.") - os.makedirs(DOWNLOADS_DIR) - console_logger.info("[FILE_MANAGER] Dossier downloads recréé.") - - if not os.path.exists("logs"): - os.makedirs("logs") - console_logger.info("[FILE_MANAGER] Dossier logs créé.") - def compute_hash(url): hash_value = hashlib.sha256(url.encode('utf-8')).hexdigest() console_logger.debug(f"[FILE_MANAGER] Hash calculé pour l'URL: {url} -> {hash_value}") From 21ad3dd71055d389380bf25794cf449fbd33f194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:24:11 +0200 Subject: [PATCH 02/21] feat: enhance ProgressFile with optional progress callback - Add callback parameter called on each progress interval - Reduce default progress_interval from 20% to 10% --- utils/progress_file.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/utils/progress_file.py b/utils/progress_file.py index 67f020a..15b5bb2 100644 --- a/utils/progress_file.py +++ b/utils/progress_file.py @@ -3,13 +3,14 @@ from utils.logger import console_logger class ProgressFile: - def __init__(self, filename, progress_interval=20): + def __init__(self, filename, progress_interval=10, callback=None): self.filename = filename self.f = open(filename, "rb") self.total = os.path.getsize(filename) self.read_bytes = 0 self.last_percent = 0 - self.progress_interval = progress_interval # Intervalle de 20% par défaut + self.progress_interval = progress_interval + self.callback = callback def read(self, size=-1): data = self.f.read(size) @@ -19,11 +20,12 @@ def read(self, size=-1): if percent - self.last_percent >= self.progress_interval: self.last_percent = percent console_logger.info(f"[UPLOAD] Envoi du fichier {self.filename} : {percent}% complété.") + if self.callback: + self.callback(percent) return data def close(self): self.f.close() def __getattr__(self, attr): - # Permet d'accéder aux autres attributs du fichier return getattr(self.f, attr) From 0c25829e8f39460220e29424df41d47bd665ffac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:24:35 +0200 Subject: [PATCH 03/21] feat(upload): integrate progress messages with ProgressFile - Add progress_msg_id parameter to upload_file for editing progress message - Use ProgressFile wrapper for Telegram direct uploads with 10% updates - Adapt curl upload callback to use provided progress_msg_id - Delete progress message after successful send --- utils/upload.py | 81 +++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/utils/upload.py b/utils/upload.py index 4c044b1..d7123b8 100644 --- a/utils/upload.py +++ b/utils/upload.py @@ -1,67 +1,82 @@ # utils/upload.py import os from utils.logger import console_logger +from utils.progress_file import ProgressFile -def upload_file(update, file_path, context): - """ - Envoie le fichier via Telegram si sa taille est < 35 Mo. - Sinon, le fichier est uploadé via curl.libriciel.fr à l'aide de - upload_large_file_via_curl() et l'URL de téléchargement est renvoyée à l'utilisateur. - Un callback de progression met à jour un message Telegram tous les 10% avec l'emoji ⏳. - """ + +def _edit_progress(bot, chat_id, msg_id, text): + try: + bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=text) + except Exception: + pass + + +def upload_file(update, file_path, context, progress_msg_id=None): if not os.path.exists(file_path): update.message.reply_text("Erreur : Fichier non trouvé.") console_logger.error(f"[UPLOAD] Fichier non trouvé: {file_path}") return - MAX_FILE_SIZE = 35 * 1024 * 1024 # 35 Mo + chat_id = update.message.chat_id + bot = context.bot + + MAX_FILE_SIZE = 35 * 1024 * 1024 file_size = os.path.getsize(file_path) + if file_size > MAX_FILE_SIZE: console_logger.info(f"[UPLOAD] Fichier '{file_path}' trop volumineux ({file_size} octets). Upload externe via curl.libriciel.fr.") - progress_msg = update.message.reply_text("Upload externe en cours : 0% ⏳") - + if progress_msg_id is None: + progress_msg = update.message.reply_text("Upload externe en cours : 0% ⏳") + progress_msg_id = progress_msg.message_id + else: + _edit_progress(bot, chat_id, progress_msg_id, "Upload externe en cours : 0% ⏳") + def progress_callback(percent): - try: - context.bot.edit_message_text( - chat_id=update.message.chat_id, - message_id=progress_msg.message_id, - text=f"Upload externe en cours : {percent}% ⏳" - ) - except Exception: - pass + _edit_progress(bot, chat_id, progress_msg_id, + f"Upload externe en cours : {percent}% ⏳") try: from utils.curl_uploader import upload_large_file_via_curl shareable_url = upload_large_file_via_curl(file_path, progress_callback=progress_callback) - context.bot.delete_message(chat_id=update.message.chat_id, - message_id=progress_msg.message_id) + bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) update.message.reply_text( f"Le fichier est trop volumineux pour être envoyé directement par Telegram.\n" f"Veuillez le télécharger ici : {shareable_url}" ) console_logger.info(f"[UPLOAD] Upload externe réussi pour '{file_path}' -> {shareable_url}") except Exception as e: - context.bot.delete_message(chat_id=update.message.chat_id, - message_id=progress_msg.message_id) + bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) update.message.reply_text( "Erreur lors de l'upload externe du fichier.\nVeuillez uploader manuellement via https://curl.libriciel.fr/" ) console_logger.error(f"[UPLOAD] Erreur upload externe pour '{file_path}': {str(e)}") return - # Sinon, envoyer le fichier via l'API Telegram + # Envoi direct via Telegram avec progression ext = os.path.splitext(file_path)[1].lower() + if progress_msg_id is not None: + _edit_progress(bot, chat_id, progress_msg_id, "📤 Envoi en cours... 0%") + try: - with open(file_path, "rb") as f: - if ext in [".mp4", ".mkv", ".avi"]: - update.message.reply_video(video=f, reply_to_message_id=update.message.message_id) - console_logger.info(f"[UPLOAD] Vidéo envoyée : {file_path}") - elif ext in [".mp3", ".wav"]: - update.message.reply_audio(audio=f, reply_to_message_id=update.message.message_id) - console_logger.info(f"[UPLOAD] Audio envoyé : {file_path}") - else: - update.message.reply_document(document=f, reply_to_message_id=update.message.message_id) - console_logger.info(f"[UPLOAD] Document envoyé : {file_path}") + progress_file = ProgressFile( + file_path, + progress_interval=10, + callback=lambda p: _edit_progress(bot, chat_id, progress_msg_id, + f"📤 Envoi en cours... {p}%") if progress_msg_id else None + ) + if ext in [".mp4", ".mkv", ".avi"]: + update.message.reply_video(video=progress_file, reply_to_message_id=update.message.message_id) + console_logger.info(f"[UPLOAD] Vidéo envoyée : {file_path}") + elif ext in [".mp3", ".wav"]: + update.message.reply_audio(audio=progress_file, reply_to_message_id=update.message.message_id) + console_logger.info(f"[UPLOAD] Audio envoyé : {file_path}") + else: + update.message.reply_document(document=progress_file, reply_to_message_id=update.message.message_id) + console_logger.info(f"[UPLOAD] Document envoyé : {file_path}") + if progress_msg_id is not None: + bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) except Exception as e: + if progress_msg_id is not None: + bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) update.message.reply_text("Erreur lors de l'envoi du fichier.") console_logger.error(f"[UPLOAD] Erreur lors de l'envoi du fichier '{file_path}': {str(e)}") From 4f8a78e371ed39a3f55cea00c5468985f77056b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:25:00 +0200 Subject: [PATCH 04/21] feat(download): add progress messages and fix retention bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Send '⏳ Téléchargement en cours...' message before download - Edit to '📤 Envoi en cours... X%' during upload via progress_msg_id - Add set_retention() call in already-downloaded path - Handle missing physical file despite hash present (retry download) --- commands/download.py | 71 ++++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/commands/download.py b/commands/download.py index 7e72d6a..7bbd769 100644 --- a/commands/download.py +++ b/commands/download.py @@ -8,6 +8,14 @@ from utils.cache import add_to_cache from utils.upload import upload_file + +def _edit_progress(bot, chat_id, msg_id, text): + try: + bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=text) + except Exception: + pass + + def download(update, context): args = context.args if not args: @@ -16,42 +24,53 @@ def download(update, context): return url = args[0] + chat_id = update.message.chat_id + bot = context.bot - # Vérification de l'espace disque avant téléchargement check_and_clean_if_needed() console_logger.info(f"[DOWNLOAD] Traitement de l'URL: {url} par {update.message.from_user.username}") + + progress_msg = update.message.reply_text("⏳ Téléchargement en cours...") + progress_msg_id = progress_msg.message_id ydl_opts = {'outtmpl': 'downloads/%(title)s.%(ext)s'} + should_download = True + filename = None + if is_already_downloaded(url): - console_logger.info(f"[DOWNLOAD] Fichier déjà téléchargé pour l'URL: {url} par {update.message.from_user.username}. Récupération du fichier...") + console_logger.info(f"[DOWNLOAD] Fichier déjà téléchargé pour l'URL: {url} par {update.message.from_user.username}. Vérification du fichier...") try: with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=False) filename = ydl.prepare_filename(info) - upload_file(update, filename, context) - console_logger.info(f"[DOWNLOAD] Fichier envoyé pour l'URL: {url} par {update.message.from_user.username}") + if os.path.exists(filename): + should_download = False + set_retention(filename) + else: + console_logger.warning(f"[DOWNLOAD] Fichier manquant malgré hash pour l'URL: {url}. Retéléchargement...") except Exception as e: - update.message.reply_text("Erreur lors de la récupération du fichier.") - console_logger.error(f"[DOWNLOAD] Erreur récupération fichier pour l'URL: {url} par {update.message.from_user.username} - {str(e)}") - return + console_logger.error(f"[DOWNLOAD] Erreur récupération infos pour l'URL: {url} - {str(e)}") - max_attempts = 3 - attempts = 0 - while attempts < max_attempts: - try: - console_logger.info(f"[DOWNLOAD] Tentative {attempts + 1} de téléchargement pour l'URL: {url} par {update.message.from_user.username}") - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - info = ydl.extract_info(url, download=True) - filename = ydl.prepare_filename(info) - save_download(url) - set_retention(filename) - add_to_cache(url, os.path.getsize(filename)) - console_logger.info(f"[DOWNLOAD] Téléchargement terminé pour l'URL: {url} par {update.message.from_user.username}. Envoi du fichier...") - upload_file(update, filename, context) - break - except Exception as e: - attempts += 1 - console_logger.error(f"[DOWNLOAD] Tentative {attempts} échouée pour l'URL: {url} par {update.message.from_user.username} - {str(e)}") - if attempts >= max_attempts: - update.message.reply_text("Erreur lors du téléchargement après plusieurs tentatives.") + if should_download: + max_attempts = 3 + attempts = 0 + while attempts < max_attempts: + try: + console_logger.info(f"[DOWNLOAD] Tentative {attempts + 1} de téléchargement pour l'URL: {url} par {update.message.from_user.username}") + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=True) + filename = ydl.prepare_filename(info) + save_download(url) + set_retention(filename) + add_to_cache(url, os.path.getsize(filename)) + break + except Exception as e: + attempts += 1 + console_logger.error(f"[DOWNLOAD] Tentative {attempts} échouée pour l'URL: {url} par {update.message.from_user.username} - {str(e)}") + if attempts >= max_attempts: + _edit_progress(bot, chat_id, progress_msg_id, "❌ Échec du téléchargement après plusieurs tentatives.") + return + + _edit_progress(bot, chat_id, progress_msg_id, "📤 Envoi en cours... 0%") + upload_file(update, filename, context, progress_msg_id=progress_msg_id) From 213677f38972aa4cfadba9684174a6b943631419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:25:30 +0200 Subject: [PATCH 05/21] feat(music): add progress messages and fix retention bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add progress message flow: download → conversion → upload with X% - Call set_retention(audio_file) after ffmpeg conversion - Call set_retention(video_file) in already-downloaded path - Handle missing video file despite hash present (retry download) - Pass progress_msg_id to upload_file --- commands/music.py | 53 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/commands/music.py b/commands/music.py index 56c3b26..ded8a02 100644 --- a/commands/music.py +++ b/commands/music.py @@ -9,6 +9,14 @@ from utils.upload import upload_file from config import FFMPEG_PATH + +def _edit_progress(bot, chat_id, msg_id, text): + try: + bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=text) + except Exception: + pass + + def music(update, context): args = context.args if not args: @@ -17,60 +25,71 @@ def music(update, context): return url = args[0] + chat_id = update.message.chat_id + bot = context.bot + console_logger.info(f"[MUSIC] Traitement de l'URL: {url} par {update.message.from_user.username}") + + progress_msg = update.message.reply_text("⏳ Téléchargement vidéo en cours...") + progress_msg_id = progress_msg.message_id ydl_opts = {'outtmpl': 'downloads/%(title)s.%(ext)s'} + should_download = True + video_file = None + if is_already_downloaded(url): - console_logger.info(f"[MUSIC] Vidéo déjà téléchargée pour l'URL: {url} par {update.message.from_user.username}. Récupération du fichier...") + console_logger.info(f"[MUSIC] Vidéo déjà téléchargée pour l'URL: {url} par {update.message.from_user.username}. Vérification du fichier...") try: with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=False) video_file = ydl.prepare_filename(info) - console_logger.info(f"[MUSIC] Vidéo trouvée: {video_file}") + if os.path.exists(video_file): + should_download = False + set_retention(video_file) + else: + console_logger.warning(f"[MUSIC] Vidéo manquante malgré hash pour l'URL: {url}. Retéléchargement...") except Exception as e: - update.message.reply_text("Erreur lors de la récupération du fichier vidéo.") - console_logger.error(f"[MUSIC] Erreur récupération vidéo pour l'URL: {url} par {update.message.from_user.username} - {str(e)}") + console_logger.error(f"[MUSIC] Erreur récupération infos pour l'URL: {url} - {str(e)}") + _edit_progress(bot, chat_id, progress_msg_id, "❌ Erreur lors de la récupération de la vidéo.") return - else: + + if should_download: max_attempts = 3 attempts = 0 while attempts < max_attempts: try: - console_logger.info(f"[MUSIC] Tentative {attempts + 1} de téléchargement de la vidéo pour l'URL: {url} par {update.message.from_user.username}") + console_logger.info(f"[MUSIC] Tentative {attempts + 1} de téléchargement pour l'URL: {url} par {update.message.from_user.username}") with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=True) video_file = ydl.prepare_filename(info) save_download(url) + set_retention(video_file) add_to_cache(url, os.path.getsize(video_file)) - console_logger.info(f"[MUSIC] Téléchargement terminé: {video_file} par {update.message.from_user.username}") break except Exception as e: attempts += 1 console_logger.error(f"[MUSIC] Tentative {attempts} échouée pour l'URL: {url} par {update.message.from_user.username} - {str(e)}") if attempts >= max_attempts: - update.message.reply_text("Erreur lors du téléchargement de la vidéo après plusieurs tentatives.") + _edit_progress(bot, chat_id, progress_msg_id, "❌ Échec du téléchargement après plusieurs tentatives.") return - # Conversion en audio MP3 + _edit_progress(bot, chat_id, progress_msg_id, "🔄 Conversion audio...") + audio_file = os.path.splitext(video_file)[0] + ".mp3" if os.path.exists(audio_file): console_logger.info(f"[MUSIC] Fichier audio déjà converti: {audio_file}") else: try: - console_logger.info(f"[MUSIC] Conversion de {video_file} en audio {audio_file} via ffmpeg pour {update.message.from_user.username}...") stream = ffmpeg.input(video_file) stream = ffmpeg.output(stream, audio_file, format='mp3', acodec='libmp3lame', audio_bitrate='192k') ffmpeg.run(stream, cmd=FFMPEG_PATH, quiet=True) + set_retention(audio_file) add_to_cache(url + "#audio", os.path.getsize(audio_file)) console_logger.info(f"[MUSIC] Conversion terminée: {audio_file} pour {update.message.from_user.username}") except Exception as e: - update.message.reply_text("Erreur lors de la conversion en audio.") + _edit_progress(bot, chat_id, progress_msg_id, "❌ Erreur lors de la conversion en audio.") console_logger.error(f"[MUSIC] Erreur conversion en audio pour {video_file} par {update.message.from_user.username} - {str(e)}") return - try: - console_logger.info(f"[MUSIC] Envoi du fichier audio: {audio_file} pour {update.message.from_user.username}") - upload_file(update, audio_file, context) - except Exception as e: - update.message.reply_text("Erreur lors de l'envoi du fichier audio.") - console_logger.error(f"[MUSIC] Erreur envoi audio pour {audio_file} par {update.message.from_user.username} - {str(e)}") + _edit_progress(bot, chat_id, progress_msg_id, "📤 Envoi en cours... 0%") + upload_file(update, audio_file, context, progress_msg_id=progress_msg_id) From e4c16c0424531e411ed028305cc7b93b059066f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:26:03 +0200 Subject: [PATCH 06/21] fix: curl_uploader finally block and Dockerfile setuptools - Initialize f=None before try block to avoid NameError in finally - Add setuptools<71 pin in Dockerfile to prevent pkg_resources error --- Dockerfile | 3 ++- utils/curl_uploader.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9cb87b3..250299e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,8 @@ COPY --from=ffmpeg /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg COPY --from=ffmpeg /usr/local/bin/ffprobe /usr/local/bin/ffprobe RUN chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe -RUN pip install --no-cache /wheels/* +RUN pip install --no-cache /wheels/* && \ + pip install --no-cache "setuptools<71" COPY . . diff --git a/utils/curl_uploader.py b/utils/curl_uploader.py index 6a0dd9d..93f0b94 100644 --- a/utils/curl_uploader.py +++ b/utils/curl_uploader.py @@ -44,6 +44,7 @@ def upload_large_file_via_curl(file_path, progress_callback=None): last_reported = [0] while attempts < 3: c = pycurl.Curl() + f = None try: c.setopt(c.URL, target_url) c.setopt(c.UPLOAD, 1) @@ -82,6 +83,10 @@ def progress(download_total, download_now, upload_total, upload_now): finally: try: c.close() - f.close() except Exception: pass + if f is not None: + try: + f.close() + except Exception: + pass From ef26bed2069a884f687c329cf1e3d2dfbfbe6507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:26:51 +0200 Subject: [PATCH 07/21] chore: cleanup dead imports and standardize version - stats.py: move get_ttl import to top, use SMALL_FILE_THRESHOLD constant, remove dead cache_misses and unused hashlib import - start.py: remove unused ParseMode import - config.py: default VERSION to V9.2 (was V.8-7) - token_loader.py: default VERSION to V9.2 (was V.8-7) --- commands/start.py | 3 +-- commands/stats.py | 7 ++----- config.py | 2 +- utils/token_loader.py | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/commands/start.py b/commands/start.py index 5c574b3..ec5cc73 100644 --- a/commands/start.py +++ b/commands/start.py @@ -1,4 +1,3 @@ -from telegram import ParseMode from utils.logger import console_logger def start(update, context): @@ -8,5 +7,5 @@ def start(update, context): "Je suis un bot qui permet de télécharger des vidéos/musiques via des liens de réseaux sociaux (principalement YouTube & TikTok)." ) - update.message.reply_text(welcome_message, parse_mode=ParseMode.HTML) + update.message.reply_text(welcome_message) console_logger.info(f"[START] Commande /start exécutée par {update.message.from_user.username}") diff --git a/commands/stats.py b/commands/stats.py index c1680a4..3d11de6 100644 --- a/commands/stats.py +++ b/commands/stats.py @@ -1,10 +1,9 @@ from utils.logger import console_logger -from utils.cache import download_cache +from utils.cache import download_cache, get_ttl, SMALL_FILE_THRESHOLD from utils.disk_manager import get_free_space_mb from config import VERSION, DEVELOPED_BY import os import time -import hashlib import psutil AUTHORIZED_USER = "overstylefr" @@ -25,20 +24,18 @@ def stats(update, context): # --- Cache Stats --- cache_entries = len(download_cache) cache_hits = 0 - cache_misses = 0 cache_expired = 0 cache_total_size = 0 cache_small_files = 0 cache_large_files = 0 for link_hash, (timestamp, file_size) in download_cache.items(): - from utils.cache import get_ttl ttl = get_ttl(file_size) age = time.time() - timestamp if age < ttl: cache_hits += 1 cache_total_size += file_size - if file_size <= 5 * 1024 * 1024: + if file_size <= SMALL_FILE_THRESHOLD: cache_small_files += 1 else: cache_large_files += 1 diff --git a/config.py b/config.py index 3b3a300..2b2548e 100644 --- a/config.py +++ b/config.py @@ -4,7 +4,7 @@ load_dotenv(".env") -VERSION = os.getenv("VERSION", "V.8-7") +VERSION = os.getenv("VERSION", "V9.2") DEVELOPED_BY = os.getenv("DEVELOPED_BY", "Tom V. | OverStyleFR") FFMPEG_PATH = os.getenv("FFMPEG_PATH", "ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg") # Change cette valeur si nécessaire (chemin complet vers l'exécutable ffmpeg) diff --git a/utils/token_loader.py b/utils/token_loader.py index e9fe805..21b0cb5 100644 --- a/utils/token_loader.py +++ b/utils/token_loader.py @@ -10,7 +10,7 @@ def get_token(): f.write("# === Configuration du bot Telegram ===\n") f.write("BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN_HERE\n\n") f.write("# === Configuration générale ===\n") - f.write("VERSION=V.8-7\n") + f.write("VERSION=V9.2\n") f.write("DEVELOPED_BY=Tom V. | OverStyleFR\n") f.write("FFMPEG_PATH=ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg\n") print(f"Le fichier {env_file} a été créé. Veuillez y renseigner votre token Telegram (BOT_TOKEN).") From da1895bdbf8bc63ee0dd86e03d1f6d398bee9bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:27:24 +0200 Subject: [PATCH 08/21] chore: add .env to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 70aa2a4..05c4f6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ logs/ token.txt downloads/ +.env .env/ .venv/ __pycache__/ From 0e1b66e90430fb30cb6a816484b8c53515b0bab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:28:12 +0200 Subject: [PATCH 09/21] chore: add imghdr.py to gitignore for Python 3.13+ compatibility --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 05c4f6d..8bd7ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ downloads/ .env .env/ .venv/ +imghdr.py __pycache__/ commands/__pycache__/ utils/__pycache__/* From 48fd8422b93404ad66ca1f4bc6ca323689c60abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:36:03 +0200 Subject: [PATCH 10/21] fix: cache stats showing 0, progress message replies, remove message deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix cache.py load_cache() to mutate dict in-place instead of reassigning, so stats.py sees the populated cache - Add explicit reply_to_message_id to progress messages - Replace progress message deletion with final status edit ('✅ Terminé !') to avoid 'Message deleted' artifact on Matrix bridge - For curl uploads, edit progress message with download link directly --- commands/download.py | 5 ++++- commands/music.py | 5 ++++- utils/cache.py | 10 ++++++---- utils/upload.py | 13 +++++-------- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/commands/download.py b/commands/download.py index 7bbd769..6f0f690 100644 --- a/commands/download.py +++ b/commands/download.py @@ -31,7 +31,10 @@ def download(update, context): console_logger.info(f"[DOWNLOAD] Traitement de l'URL: {url} par {update.message.from_user.username}") - progress_msg = update.message.reply_text("⏳ Téléchargement en cours...") + progress_msg = update.message.reply_text( + "⏳ Téléchargement en cours...", + reply_to_message_id=update.message.message_id + ) progress_msg_id = progress_msg.message_id ydl_opts = {'outtmpl': 'downloads/%(title)s.%(ext)s'} diff --git a/commands/music.py b/commands/music.py index ded8a02..44a1e86 100644 --- a/commands/music.py +++ b/commands/music.py @@ -30,7 +30,10 @@ def music(update, context): console_logger.info(f"[MUSIC] Traitement de l'URL: {url} par {update.message.from_user.username}") - progress_msg = update.message.reply_text("⏳ Téléchargement vidéo en cours...") + progress_msg = update.message.reply_text( + "⏳ Téléchargement vidéo en cours...", + reply_to_message_id=update.message.message_id + ) progress_msg_id = progress_msg.message_id ydl_opts = {'outtmpl': 'downloads/%(title)s.%(ext)s'} diff --git a/utils/cache.py b/utils/cache.py index 4614bad..e933c91 100644 --- a/utils/cache.py +++ b/utils/cache.py @@ -15,17 +15,19 @@ def load_cache(): global download_cache try: with open(CACHE_FILE, 'r') as f: - download_cache = json.load(f) + data = json.load(f) + download_cache.clear() + download_cache.update(data) console_logger.info(f'Cache loaded from {CACHE_FILE}') except FileNotFoundError: console_logger.warning(f'Cache file {CACHE_FILE} not found. Initializing empty cache.') - download_cache = {} + download_cache.clear() except json.JSONDecodeError: console_logger.error(f'Error decoding JSON from {CACHE_FILE}. Initializing empty cache.') - download_cache = {} + download_cache.clear() except Exception as e: console_logger.error(f'An unexpected error occurred loading cache: {e}') - download_cache = {} + download_cache.clear() def save_cache(): try: diff --git a/utils/upload.py b/utils/upload.py index d7123b8..09b6e54 100644 --- a/utils/upload.py +++ b/utils/upload.py @@ -38,14 +38,11 @@ def progress_callback(percent): try: from utils.curl_uploader import upload_large_file_via_curl shareable_url = upload_large_file_via_curl(file_path, progress_callback=progress_callback) - bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) - update.message.reply_text( - f"Le fichier est trop volumineux pour être envoyé directement par Telegram.\n" - f"Veuillez le télécharger ici : {shareable_url}" - ) + _edit_progress(bot, chat_id, progress_msg_id, + f"✅ Fichier disponible ici : {shareable_url}") console_logger.info(f"[UPLOAD] Upload externe réussi pour '{file_path}' -> {shareable_url}") except Exception as e: - bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) + _edit_progress(bot, chat_id, progress_msg_id, "❌ Échec de l'upload externe.") update.message.reply_text( "Erreur lors de l'upload externe du fichier.\nVeuillez uploader manuellement via https://curl.libriciel.fr/" ) @@ -74,9 +71,9 @@ def progress_callback(percent): update.message.reply_document(document=progress_file, reply_to_message_id=update.message.message_id) console_logger.info(f"[UPLOAD] Document envoyé : {file_path}") if progress_msg_id is not None: - bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) + _edit_progress(bot, chat_id, progress_msg_id, "✅ Terminé !") except Exception as e: if progress_msg_id is not None: - bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) + _edit_progress(bot, chat_id, progress_msg_id, "❌ Erreur lors de l'envoi.") update.message.reply_text("Erreur lors de l'envoi du fichier.") console_logger.error(f"[UPLOAD] Erreur lors de l'envoi du fichier '{file_path}': {str(e)}") From 26c03ace9a7e335184860761128c476785c95469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:46:08 +0200 Subject: [PATCH 11/21] feat: session-only cache with hit tracking, cache usage indicators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cache is now session-only (no disk persistence, resets on restart) - Track cache hits and bytes_saved per entry (hit_count in entry list) - stats.py uses cache_stats() for cleaner stats with 'Hits cache: X (Y Mo économisés)' - download/music show '📦 Utilisation du cache...' when serving from cache - Upload adds '📦 Envoyé depuis le cache' caption when from_cache=True - Progress messages are deleted at end (clean UX on Telegram) --- commands/download.py | 9 ++++- commands/music.py | 9 ++++- commands/stats.py | 37 +++++-------------- utils/cache.py | 87 ++++++++++++++++++++++++++------------------ utils/upload.py | 28 +++++++++----- 5 files changed, 95 insertions(+), 75 deletions(-) diff --git a/commands/download.py b/commands/download.py index 6f0f690..17e8781 100644 --- a/commands/download.py +++ b/commands/download.py @@ -5,7 +5,7 @@ from utils.file_manager import is_already_downloaded, save_download from utils.disk_manager import check_and_clean_if_needed from utils.retention import set_retention -from utils.cache import add_to_cache +from utils.cache import add_to_cache, record_cache_hit from utils.upload import upload_file @@ -39,6 +39,7 @@ def download(update, context): ydl_opts = {'outtmpl': 'downloads/%(title)s.%(ext)s'} should_download = True + from_cache = False filename = None if is_already_downloaded(url): @@ -49,7 +50,11 @@ def download(update, context): filename = ydl.prepare_filename(info) if os.path.exists(filename): should_download = False + from_cache = True set_retention(filename) + add_to_cache(url, os.path.getsize(filename)) + record_cache_hit(url) + _edit_progress(bot, chat_id, progress_msg_id, "📦 Utilisation du cache...") else: console_logger.warning(f"[DOWNLOAD] Fichier manquant malgré hash pour l'URL: {url}. Retéléchargement...") except Exception as e: @@ -76,4 +81,4 @@ def download(update, context): return _edit_progress(bot, chat_id, progress_msg_id, "📤 Envoi en cours... 0%") - upload_file(update, filename, context, progress_msg_id=progress_msg_id) + upload_file(update, filename, context, progress_msg_id=progress_msg_id, from_cache=from_cache) diff --git a/commands/music.py b/commands/music.py index 44a1e86..8726abb 100644 --- a/commands/music.py +++ b/commands/music.py @@ -5,7 +5,7 @@ from utils.logger import console_logger from utils.file_manager import is_already_downloaded, save_download from utils.retention import set_retention -from utils.cache import add_to_cache +from utils.cache import add_to_cache, record_cache_hit from utils.upload import upload_file from config import FFMPEG_PATH @@ -38,6 +38,7 @@ def music(update, context): ydl_opts = {'outtmpl': 'downloads/%(title)s.%(ext)s'} should_download = True + from_cache = False video_file = None if is_already_downloaded(url): @@ -48,7 +49,11 @@ def music(update, context): video_file = ydl.prepare_filename(info) if os.path.exists(video_file): should_download = False + from_cache = True set_retention(video_file) + add_to_cache(url, os.path.getsize(video_file)) + record_cache_hit(url) + _edit_progress(bot, chat_id, progress_msg_id, "📦 Utilisation du cache...") else: console_logger.warning(f"[MUSIC] Vidéo manquante malgré hash pour l'URL: {url}. Retéléchargement...") except Exception as e: @@ -95,4 +100,4 @@ def music(update, context): return _edit_progress(bot, chat_id, progress_msg_id, "📤 Envoi en cours... 0%") - upload_file(update, audio_file, context, progress_msg_id=progress_msg_id) + upload_file(update, audio_file, context, progress_msg_id=progress_msg_id, from_cache=from_cache) diff --git a/commands/stats.py b/commands/stats.py index 3d11de6..fcc1bb3 100644 --- a/commands/stats.py +++ b/commands/stats.py @@ -1,5 +1,5 @@ from utils.logger import console_logger -from utils.cache import download_cache, get_ttl, SMALL_FILE_THRESHOLD +from utils.cache import cache_stats from utils.disk_manager import get_free_space_mb from config import VERSION, DEVELOPED_BY import os @@ -22,25 +22,7 @@ def stats(update, context): return # --- Cache Stats --- - cache_entries = len(download_cache) - cache_hits = 0 - cache_expired = 0 - cache_total_size = 0 - cache_small_files = 0 - cache_large_files = 0 - - for link_hash, (timestamp, file_size) in download_cache.items(): - ttl = get_ttl(file_size) - age = time.time() - timestamp - if age < ttl: - cache_hits += 1 - cache_total_size += file_size - if file_size <= SMALL_FILE_THRESHOLD: - cache_small_files += 1 - else: - cache_large_files += 1 - else: - cache_expired += 1 + cs = cache_stats() # --- Disk Stats --- downloads_dir = "downloads" @@ -99,13 +81,14 @@ def stats(update, context): f"`Version:` {VERSION}\n" f"`Développé par:` {DEVELOPED_BY}\n\n" - f"🗂️ *Cache*\n" - f"`Entrées totales:` {cache_entries}\n" - f"`Entrées valides:` {cache_hits}\n" - f"`Entrées expirées:` {cache_expired}\n" - f"`Petits fichiers (≤5Mo):` {cache_small_files}\n" - f"`Gros fichiers (>5Mo):` {cache_large_files}\n" - f"`Taille totale cache:` {cache_total_size / (1024 * 1024):.2f} Mo\n\n" + f"🗂️ *Cache (session)*\n" + f"`Entrées totales:` {cs['total_entries']}\n" + f"`Entrées valides:` {cs['valid']}\n" + f"`Entrées expirées:` {cs['expired']}\n" + f"`Petits fichiers (≤5Mo):` {cs['small']}\n" + f"`Gros fichiers (>5Mo):` {cs['large']}\n" + f"`Taille totale cache:` {cs['total_size'] / (1024 * 1024):.2f} Mo\n" + f"`Hits cache:` {cs['total_hits']} ({cs['bytes_saved'] / (1024 * 1024):.2f} Mo économisés)\n\n" f"💾 *Disque*\n" f"`Espace libre:` {free_space_mb:.2f} Mo\n" diff --git a/utils/cache.py b/utils/cache.py index e933c91..709a6ce 100644 --- a/utils/cache.py +++ b/utils/cache.py @@ -1,5 +1,3 @@ -import os -import json import time import hashlib from utils.logger import console_logger @@ -7,53 +5,72 @@ SMALL_FILE_THRESHOLD = 5 * 1024 * 1024 LONG_TTL = 24 * 3600 STANDARD_TTL = 1 * 3600 -CACHE_FILE = "download_temp/cache_metadata.json" download_cache = {} + def load_cache(): global download_cache - try: - with open(CACHE_FILE, 'r') as f: - data = json.load(f) - download_cache.clear() - download_cache.update(data) - console_logger.info(f'Cache loaded from {CACHE_FILE}') - except FileNotFoundError: - console_logger.warning(f'Cache file {CACHE_FILE} not found. Initializing empty cache.') - download_cache.clear() - except json.JSONDecodeError: - console_logger.error(f'Error decoding JSON from {CACHE_FILE}. Initializing empty cache.') - download_cache.clear() - except Exception as e: - console_logger.error(f'An unexpected error occurred loading cache: {e}') - download_cache.clear() - -def save_cache(): - try: - cache_dir = os.path.dirname(CACHE_FILE) - if cache_dir and not os.path.exists(cache_dir): - os.makedirs(cache_dir) - console_logger.info(f'Created cache directory: {cache_dir}') - with open(CACHE_FILE, 'w') as f: - json.dump(download_cache, f, indent=4) - console_logger.info(f'Cache saved to {CACHE_FILE}') - except Exception as e: - console_logger.error(f'An error occurred saving cache to {CACHE_FILE}: {e}') + download_cache.clear() + console_logger.info("Cache initialisé (session en mémoire).") + def get_ttl(file_size): return LONG_TTL if file_size <= SMALL_FILE_THRESHOLD else STANDARD_TTL + def is_cache_valid(link_hash): if link_hash not in download_cache: return False - timestamp, size = download_cache[link_hash] - ttl = get_ttl(size) - return (time.time() - timestamp) < ttl + timestamp, size, _hits = download_cache[link_hash] + return (time.time() - timestamp) < get_ttl(size) + def add_to_cache(link, file_size): link_hash = hashlib.sha256(link.encode()).hexdigest() - download_cache[link_hash] = (time.time(), file_size) - save_cache() + download_cache[link_hash] = [time.time(), file_size, 0] return link_hash + +def record_cache_hit(link): + link_hash = hashlib.sha256(link.encode()).hexdigest() + if link_hash in download_cache: + download_cache[link_hash][2] += 1 + return True + return False + + +def cache_stats(): + total_entries = len(download_cache) + hits = 0 + expired = 0 + total_size = 0 + small = 0 + large = 0 + total_hits = 0 + bytes_saved = 0 + + for timestamp, file_size, hit_count in download_cache.values(): + age = time.time() - timestamp + total_hits += hit_count + bytes_saved += file_size * hit_count + if age < get_ttl(file_size): + hits += 1 + total_size += file_size + if file_size <= SMALL_FILE_THRESHOLD: + small += 1 + else: + large += 1 + else: + expired += 1 + + return { + "total_entries": total_entries, + "valid": hits, + "expired": expired, + "small": small, + "large": large, + "total_size": total_size, + "total_hits": total_hits, + "bytes_saved": bytes_saved, + } \ No newline at end of file diff --git a/utils/upload.py b/utils/upload.py index 09b6e54..9756b3f 100644 --- a/utils/upload.py +++ b/utils/upload.py @@ -11,7 +11,7 @@ def _edit_progress(bot, chat_id, msg_id, text): pass -def upload_file(update, file_path, context, progress_msg_id=None): +def upload_file(update, file_path, context, progress_msg_id=None, from_cache=False): if not os.path.exists(file_path): update.message.reply_text("Erreur : Fichier non trouvé.") console_logger.error(f"[UPLOAD] Fichier non trouvé: {file_path}") @@ -22,6 +22,7 @@ def upload_file(update, file_path, context, progress_msg_id=None): MAX_FILE_SIZE = 35 * 1024 * 1024 file_size = os.path.getsize(file_path) + caption = "📦 Envoyé depuis le cache" if from_cache else None if file_size > MAX_FILE_SIZE: console_logger.info(f"[UPLOAD] Fichier '{file_path}' trop volumineux ({file_size} octets). Upload externe via curl.libriciel.fr.") @@ -38,11 +39,14 @@ def progress_callback(percent): try: from utils.curl_uploader import upload_large_file_via_curl shareable_url = upload_large_file_via_curl(file_path, progress_callback=progress_callback) - _edit_progress(bot, chat_id, progress_msg_id, - f"✅ Fichier disponible ici : {shareable_url}") + bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) + update.message.reply_text( + f"Le fichier est trop volumineux pour être envoyé directement par Telegram.\n" + f"Veuillez le télécharger ici : {shareable_url}" + ) console_logger.info(f"[UPLOAD] Upload externe réussi pour '{file_path}' -> {shareable_url}") except Exception as e: - _edit_progress(bot, chat_id, progress_msg_id, "❌ Échec de l'upload externe.") + bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) update.message.reply_text( "Erreur lors de l'upload externe du fichier.\nVeuillez uploader manuellement via https://curl.libriciel.fr/" ) @@ -62,18 +66,24 @@ def progress_callback(percent): f"📤 Envoi en cours... {p}%") if progress_msg_id else None ) if ext in [".mp4", ".mkv", ".avi"]: - update.message.reply_video(video=progress_file, reply_to_message_id=update.message.message_id) + update.message.reply_video(video=progress_file, + caption=caption, + reply_to_message_id=update.message.message_id) console_logger.info(f"[UPLOAD] Vidéo envoyée : {file_path}") elif ext in [".mp3", ".wav"]: - update.message.reply_audio(audio=progress_file, reply_to_message_id=update.message.message_id) + update.message.reply_audio(audio=progress_file, + caption=caption, + reply_to_message_id=update.message.message_id) console_logger.info(f"[UPLOAD] Audio envoyé : {file_path}") else: - update.message.reply_document(document=progress_file, reply_to_message_id=update.message.message_id) + update.message.reply_document(document=progress_file, + caption=caption, + reply_to_message_id=update.message.message_id) console_logger.info(f"[UPLOAD] Document envoyé : {file_path}") if progress_msg_id is not None: - _edit_progress(bot, chat_id, progress_msg_id, "✅ Terminé !") + bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) except Exception as e: if progress_msg_id is not None: - _edit_progress(bot, chat_id, progress_msg_id, "❌ Erreur lors de l'envoi.") + bot.delete_message(chat_id=chat_id, message_id=progress_msg_id) update.message.reply_text("Erreur lors de l'envoi du fichier.") console_logger.error(f"[UPLOAD] Erreur lors de l'envoi du fichier '{file_path}': {str(e)}") From e42f96ba910afa49e3fd2039a7b49d3571264240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:51:57 +0200 Subject: [PATCH 12/21] chore(stats): display free space in GB, add French number formatting --- commands/stats.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/commands/stats.py b/commands/stats.py index fcc1bb3..38560ff 100644 --- a/commands/stats.py +++ b/commands/stats.py @@ -7,7 +7,11 @@ import psutil AUTHORIZED_USER = "overstylefr" -AUTHORIZED_IDS = {5092023723} # ID Telegram de @overstylefr +AUTHORIZED_IDS = {5092023723} + + +def _fmt_fr(value, decimals=2): + return f"{value:.{decimals}f}".replace(".", ",") def stats(update, context): @@ -46,7 +50,7 @@ def stats(update, context): total_temp_size += os.path.getsize(fp) total_temp_files += 1 - free_space_mb = get_free_space_mb() + free_space_gb = get_free_space_mb() / 1024 # --- Logs Stats --- logs_dir = "logs" @@ -64,8 +68,8 @@ def stats(update, context): uptime_str = f"{int(uptime_seconds // 3600)}h {int((uptime_seconds % 3600) // 60)}m" cpu_percent = psutil.cpu_percent(interval=0.1) memory = psutil.virtual_memory() - memory_used_mb = memory.used / (1024 * 1024) - memory_total_mb = memory.total / (1024 * 1024) + memory_used_gb = memory.used / (1024 ** 3) + memory_total_gb = memory.total / (1024 ** 3) memory_percent = memory.percent # --- Hash Stats --- @@ -91,7 +95,7 @@ def stats(update, context): f"`Hits cache:` {cs['total_hits']} ({cs['bytes_saved'] / (1024 * 1024):.2f} Mo économisés)\n\n" f"💾 *Disque*\n" - f"`Espace libre:` {free_space_mb:.2f} Mo\n" + f"`Espace libre:` {_fmt_fr(free_space_gb)} Go\n" f"`Fichiers downloads:` {total_downloads_files} ({total_downloads_size / (1024 * 1024):.2f} Mo)\n" f"`Fichiers temp:` {total_temp_files} ({total_temp_size / (1024 * 1024):.2f} Mo)\n" f"`URLs enregistrées:` {total_hashes}\n\n" @@ -103,7 +107,7 @@ def stats(update, context): f"🖥️ *Système*\n" f"`Uptime:` {uptime_str}\n" f"`CPU:` {cpu_percent:.1f}%\n" - f"`RAM:` {memory_used_mb:.1f}/{memory_total_mb:.1f} Mo ({memory_percent}%)\n" + f"`RAM:` {_fmt_fr(memory_used_gb, 1)}/{_fmt_fr(memory_total_gb, 1)} Go ({memory_percent}%)\n" ) update.message.reply_text(msg, parse_mode="Markdown") From 4328183bb089f4c34ca9af1dfea8291b5759d96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:52:34 +0200 Subject: [PATCH 13/21] chore(stats): remove download_temp from stats display --- commands/stats.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/commands/stats.py b/commands/stats.py index 38560ff..b176a19 100644 --- a/commands/stats.py +++ b/commands/stats.py @@ -30,11 +30,8 @@ def stats(update, context): # --- Disk Stats --- downloads_dir = "downloads" - download_temp_dir = "download_temp" total_downloads_size = 0 total_downloads_files = 0 - total_temp_size = 0 - total_temp_files = 0 if os.path.exists(downloads_dir): for root, dirs, files in os.walk(downloads_dir): @@ -43,13 +40,6 @@ def stats(update, context): total_downloads_size += os.path.getsize(fp) total_downloads_files += 1 - if os.path.exists(download_temp_dir): - for root, dirs, files in os.walk(download_temp_dir): - for f in files: - fp = os.path.join(root, f) - total_temp_size += os.path.getsize(fp) - total_temp_files += 1 - free_space_gb = get_free_space_mb() / 1024 # --- Logs Stats --- @@ -97,7 +87,6 @@ def stats(update, context): f"💾 *Disque*\n" f"`Espace libre:` {_fmt_fr(free_space_gb)} Go\n" f"`Fichiers downloads:` {total_downloads_files} ({total_downloads_size / (1024 * 1024):.2f} Mo)\n" - f"`Fichiers temp:` {total_temp_files} ({total_temp_size / (1024 * 1024):.2f} Mo)\n" f"`URLs enregistrées:` {total_hashes}\n\n" f"📝 *Logs*\n" From bf625359e9534302dc2be0cab7dd65521af6f69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 10:55:10 +0200 Subject: [PATCH 14/21] fix: clear downloads folder at startup - Replace check_and_clean_if_needed with clear_downloads on startup - Preserves hashes.txt for deduplication across restarts - Removes unused imports (os, check_and_clean_if_needed) --- main.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index 1352d7d..3137414 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,4 @@ # main.py -import os import threading import time from dotenv import load_dotenv @@ -14,7 +13,7 @@ from utils.cache import load_cache from utils.token_loader import get_token from config import CLEANUP_INTERVAL_HOURS -from utils.disk_manager import clear_downloads, check_and_clean_if_needed +from utils.disk_manager import clear_downloads from utils.logger import console_logger load_dotenv(".env") @@ -34,12 +33,10 @@ def scheduled_cleanup(): def main(): - console_logger.info("[INIT] Début de la réinitialisation des dossiers...") + console_logger.info("[INIT] Nettoyage du dossier downloads...") + clear_downloads() load_cache() - # Vérification de l'espace disque au démarrage - check_and_clean_if_needed() - token = get_token() updater = Updater(token, use_context=True) dp = updater.dispatcher From 5b45c8d50148c69b283aba268a52f8da0fc4204a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 11:13:41 +0200 Subject: [PATCH 15/21] deploy: optimisation Docker, CI/CD, standalone et retention - Supprime egg-socialvideodownload.json (deprecated, .env remplace token.txt) - Ajoute .dockerignore (exclut ffmpeg/ statique, .kilo/, *.md, etc.) - Dockerfile : base slim-bookworm, supprime setuptools<71, supprime wheels apres install, conserve setuptools systeme (pkg_resources) - Ajoute docker-compose.yml (volumes .env, downloads, logs) - Ajoute setup.sh (venv + pip install + copie .env.example) - CI/CD : job validate (import main), version detection depuis config.py, QEMU + Buildx, multi-arch amd64/arm64 - config.py : FFMPEG_PATH fallback vers 'ffmpeg' (PATH) si le chemin configure n'existe pas - disk_manager.py : retention via mtime, suppression de _remove_from_hashes() (bug: filename vs hash) - retention.py : ajoute is_file_expired() - main.py : scheduled_cleanup() utilise cleanup_by_retention() --- .dockerignore | 14 +++++++++ .github/workflows/deploy.yml | 34 ++++++++++++--------- Dockerfile | 8 ++--- config.py | 5 +++- docker-compose.yml | 9 ++++++ egg-socialvideodownload.json | 43 --------------------------- main.py | 8 ++--- setup.sh | 28 ++++++++++++++++++ utils/disk_manager.py | 57 ++++++++++++++++++++++++------------ utils/retention.py | 11 +++++++ 10 files changed, 131 insertions(+), 86 deletions(-) create mode 100644 .dockerignore create mode 100644 docker-compose.yml delete mode 100644 egg-socialvideodownload.json create mode 100755 setup.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..95368e8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git/ +.gitignore +.env +.env.example +logs/ +downloads/ +__pycache__/ +**/__pycache__/ +*.pyc +imghdr.py +.kilo/ +AGENTS.md +*.md +ffmpeg/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2e0f64c..3657eed 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,7 +8,20 @@ on: - develop jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.x' + - name: Install dependencies + run: pip install -r requirements.txt + - name: Verify imports + run: python -c "import main" + build-and-push: + needs: validate runs-on: ubuntu-latest permissions: contents: read @@ -17,18 +30,11 @@ jobs: - name: Check out the repo uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.x' - - - name: Install dependencies - run: pip install -r requirements.txt + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - name: Run tests - run: | - # Add your test commands here - echo "No tests to run" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Packages uses: docker/login-action@v4 @@ -46,9 +52,8 @@ jobs: run: | REPO="ghcr.io/${{ env.repo_name }}" if [ "${{ github.ref }}" = "refs/heads/main" ]; then - BOT_VERSION=$(grep 'BOT_VERSION = ' main.py | sed 's/.*"\(.*\)".*/\1/') - VERSION_SHORT=$(echo "$BOT_VERSION" | sed -E 's/^((V[0-9]+)(\.[0-9]+)?).*/\1/') - echo "tags=${REPO}:latest,${REPO}:${VERSION_SHORT},${REPO}:${BOT_VERSION}" >> $GITHUB_OUTPUT + VERSION=$(grep '^VERSION' config.py | head -1 | sed 's/.*, "\(.*\)").*/\1/') + echo "tags=${REPO}:latest,${REPO}:${VERSION}" >> $GITHUB_OUTPUT elif [ "${{ github.ref }}" = "refs/heads/develop" ]; then echo "tags=${REPO}:dev" >> $GITHUB_OUTPUT else @@ -62,6 +67,7 @@ jobs: context: . push: true tags: ${{ steps.tags.outputs.tags }} + platforms: linux/amd64,linux/arm64 - name: Image digest run: echo ${{ steps.build-and-push.outputs.digest }} diff --git a/Dockerfile b/Dockerfile index 250299e..5cd6f0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,8 @@ - # --- FFmpeg Stage --- FROM ghcr.io/linuxserver/ffmpeg:latest AS ffmpeg # --- Build Stage --- -FROM python:3.11-slim-bullseye AS builder +FROM python:3.11-slim-bookworm AS builder WORKDIR /app @@ -16,7 +15,7 @@ RUN pip wheel --no-cache-dir --wheel-dir /app/wheels -r requirements.txt # --- Final Stage --- -FROM python:3.11-slim-bullseye +FROM python:3.11-slim-bookworm WORKDIR /app @@ -25,8 +24,7 @@ COPY --from=ffmpeg /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg COPY --from=ffmpeg /usr/local/bin/ffprobe /usr/local/bin/ffprobe RUN chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe -RUN pip install --no-cache /wheels/* && \ - pip install --no-cache "setuptools<71" +RUN pip install --no-cache $(ls /wheels/*.whl | grep -v setuptools) && rm -rf /wheels COPY . . diff --git a/config.py b/config.py index 2b2548e..c9dd9d2 100644 --- a/config.py +++ b/config.py @@ -6,7 +6,10 @@ VERSION = os.getenv("VERSION", "V9.2") DEVELOPED_BY = os.getenv("DEVELOPED_BY", "Tom V. | OverStyleFR") -FFMPEG_PATH = os.getenv("FFMPEG_PATH", "ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg") # Change cette valeur si nécessaire (chemin complet vers l'exécutable ffmpeg) +_FFMPEG_DEFAULT = "ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg" +FFMPEG_PATH = os.getenv("FFMPEG_PATH", _FFMPEG_DEFAULT) +if not os.path.exists(FFMPEG_PATH): + FFMPEG_PATH = "ffmpeg" # Disk Management Configuration MIN_FREE_SPACE_MB = int(os.getenv("MIN_FREE_SPACE_MB", 500)) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a36e13d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + bot: + build: . + container_name: socialvideodownload + volumes: + - ./.env:/app/.env + - ./downloads:/app/downloads + - ./logs:/app/logs + restart: unless-stopped diff --git a/egg-socialvideodownload.json b/egg-socialvideodownload.json deleted file mode 100644 index 95e0f86..0000000 --- a/egg-socialvideodownload.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "_comment": "Egg SocialVideoDownload.py pour Pelican / Pterodactyl", - "meta": { - "version": "PTDL_v2", - "update_url": null - }, - "exported_at": "2026-06-11T12:00:00+00:00", - "name": "SocialVideoDownload.py", - "author": "Tom V. | OverStyleFR", - "description": "Bot Telegram qui télécharge des vidéos/musiques depuis les réseaux sociaux (YouTube, TikTok, etc.) via yt-dlp.\r\nUtilise l'image Docker construite par le CI/CD GitHub Actions.\r\nAu runtime, les fichiers sont copiés dans le volume persistant du serveur.", - "features": null, - "docker_images": { - "Latest (main)": "ghcr.io/overstylefr/socialvideodownload.py:latest", - "Develop": "ghcr.io/overstylefr/socialvideodownload.py:dev" - }, - "file_denylist": [], - "startup": "echo \"{{TELEGRAM_TOKEN}}\" > /home/container/token.txt && cd /home/container && exec python main.py", - "config": { - "files": "{}", - "startup": "{\n \"done\": [\n \"Le bot a démarré avec succès!\"\n ],\n \"userInteraction\": []\n}", - "logs": "{}", - "stop": "^C" - }, - "scripts": { - "installation": { - "script": "#!/bin/bash\nset -e\n\nmkdir -p /mnt/server\ncp -r /app/* /mnt/server/\n\necho \"[INSTALL] Fichiers copiés depuis l'image Docker.\"\nexit 0", - "container": "ghcr.io/overstylefr/socialvideodownload.py:latest", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Token Telegram", - "description": "Token du bot Telegram (@BotFather).\r\nSera écrit automatiquement dans token.txt au démarrage.", - "env_variable": "TELEGRAM_TOKEN", - "default_value": "", - "user_viewable": false, - "user_editable": true, - "rules": "required|string|max:128", - "field_type": "password" - } - ] -} diff --git a/main.py b/main.py index 3137414..5aaa0ff 100644 --- a/main.py +++ b/main.py @@ -13,22 +13,22 @@ from utils.cache import load_cache from utils.token_loader import get_token from config import CLEANUP_INTERVAL_HOURS -from utils.disk_manager import clear_downloads +from utils.disk_manager import clear_downloads, cleanup_by_retention from utils.logger import console_logger load_dotenv(".env") def scheduled_cleanup(): - """Thread de nettoyage périodique du dossier downloads.""" + """Thread de nettoyage périodique — respecte la rétention (mtimes).""" interval_seconds = CLEANUP_INTERVAL_HOURS * 3600 console_logger.info( f"[CLEANUP] Rotation planifiée activée — nettoyage toutes les {CLEANUP_INTERVAL_HOURS}h." ) while True: time.sleep(interval_seconds) - console_logger.info("[CLEANUP] Nettoyage périodique du dossier downloads...") - clear_downloads() + console_logger.info("[CLEANUP] Nettoyage périodique (rétention)...") + cleanup_by_retention() console_logger.info("[CLEANUP] Nettoyage périodique terminé.") diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..5aaa19f --- /dev/null +++ b/setup.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -e + +echo "=== SocialVideoDownload.py — Installation autonome ===" +echo "" + +# Création de l'environnement virtuel +python3 -m venv .venv +source .venv/bin/activate + +# Installation des dépendances +pip install -r requirements.txt + +# Création du fichier .env +if [ ! -f .env ]; then + cp .env.example .env + echo "Fichier .env créé à partir de .env.example." +else + echo "Fichier .env déjà existant, aucun changement." +fi + +echo "" +echo "Installation terminée." +echo "" +echo "Configurez votre token Telegram dans .env (BOT_TOKEN), puis lancez :" +echo " source .venv/bin/activate && python main.py" +echo "" +echo "Assurez-vous que ffmpeg est accessible dans votre PATH ou défini via FFMPEG_PATH dans .env." diff --git a/utils/disk_manager.py b/utils/disk_manager.py index 5287111..901812a 100644 --- a/utils/disk_manager.py +++ b/utils/disk_manager.py @@ -2,49 +2,68 @@ import shutil from config import MIN_FREE_SPACE_MB from utils.logger import console_logger +from utils.retention import is_file_expired DOWNLOADS_DIR = "downloads" +HASH_FILE = os.path.join(DOWNLOADS_DIR, "hashes.txt") def get_free_space_mb() -> float: - """Retourne l'espace disque libre en Mo sur la partition du dossier downloads.""" stat = shutil.disk_usage(DOWNLOADS_DIR if os.path.exists(DOWNLOADS_DIR) else ".") return stat.free / (1024 * 1024) def clear_downloads(): - """Vide le dossier downloads et recrée sa structure (conserve hashes.txt).""" - hash_file = os.path.join(DOWNLOADS_DIR, "hashes.txt") - hashes_backup = None - - # Sauvegarde des hashes avant suppression pour éviter les re-téléchargements - if os.path.exists(hash_file): - with open(hash_file, "r") as f: - hashes_backup = f.read() - + """Vidage complet du dossier downloads (fichiers + hashes.txt). Démarrage frais.""" if os.path.exists(DOWNLOADS_DIR): shutil.rmtree(DOWNLOADS_DIR) - console_logger.info("[DISK_MANAGER] Dossier downloads vidé.") + console_logger.info("[DISK_MANAGER] Dossier downloads entièrement supprimé.") + os.makedirs(DOWNLOADS_DIR, exist_ok=True) + + +def cleanup_by_retention(): + """Supprime les fichiers dont la rétention est expirée et nettoie hashes.txt.""" + if not os.path.exists(DOWNLOADS_DIR): + os.makedirs(DOWNLOADS_DIR, exist_ok=True) + return - os.makedirs(DOWNLOADS_DIR) + removed = 0 + for entry in os.listdir(DOWNLOADS_DIR): + file_path = os.path.join(DOWNLOADS_DIR, entry) + if entry == "hashes.txt" or not os.path.isfile(file_path): + continue + if is_file_expired(file_path): + try: + os.remove(file_path) + console_logger.info(f"[DISK_MANAGER] Fichier expiré supprimé : {file_path}") + removed += 1 + except Exception as e: + console_logger.error(f"[DISK_MANAGER] Erreur suppression {file_path}: {e}") - if hashes_backup is not None: - with open(hash_file, "w") as f: - f.write(hashes_backup) - console_logger.info("[DISK_MANAGER] Fichier hashes.txt restauré après nettoyage.") + if removed: + console_logger.info(f"[DISK_MANAGER] Nettoyage par rétention terminé — {removed} fichier(s) supprimé(s).") + else: + console_logger.debug("[DISK_MANAGER] Aucun fichier expiré trouvé.") def check_and_clean_if_needed(): - """Vérifie l'espace libre et vide le dossier downloads si le seuil est atteint.""" + """Vérifie l'espace libre. Nettoie par rétention d'abord, sinon vidage complet.""" free_mb = get_free_space_mb() console_logger.debug(f"[DISK_MANAGER] Espace libre : {free_mb:.1f} Mo (seuil : {MIN_FREE_SPACE_MB} Mo)") if free_mb < MIN_FREE_SPACE_MB: console_logger.warning( f"[DISK_MANAGER] Espace libre insuffisant ({free_mb:.1f} Mo < {MIN_FREE_SPACE_MB} Mo). " - "Nettoyage d'urgence du dossier downloads..." + "Nettoyage par rétention..." ) - clear_downloads() + cleanup_by_retention() + free_mb = get_free_space_mb() + if free_mb < MIN_FREE_SPACE_MB: + console_logger.warning( + f"[DISK_MANAGER] Toujours insuffisant après rétention ({free_mb:.1f} Mo). " + "Vidage complet du dossier downloads..." + ) + clear_downloads() console_logger.info("[DISK_MANAGER] Nettoyage d'urgence terminé.") return True return False diff --git a/utils/retention.py b/utils/retention.py index 74689b1..9a6d015 100644 --- a/utils/retention.py +++ b/utils/retention.py @@ -1,4 +1,5 @@ import os +import time from datetime import datetime, timedelta from utils.logger import console_logger @@ -31,3 +32,13 @@ def set_retention(file_path: str): console_logger.debug(f"[RETENTION] Set future mtime for {file_path} ({minutes} min)") except Exception as e: console_logger.error(f"[RETENTION] Failed to set mtime for {file_path}: {e}") + +def is_file_expired(file_path: str) -> bool: + """Check if a file's retention period has expired. + The file's mtime was set to (now + retention) by set_retention(), + so if mtime < now, the retention has elapsed. + """ + if not os.path.exists(file_path): + return True + mtime = os.path.getmtime(file_path) + return mtime < time.time() From 4f31ca49a548b0c7b6540ff4a6b3294bb3bd5c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 11:14:49 +0200 Subject: [PATCH 16/21] fix: commit imghdr.py shim for Python 3.13+ compatibility imghdr a ete supprime de la stdlib en Python 3.13. Le module python-telegram-bot v13.7 l'importe encore. Le shim imghdr.py etait gitignored donc absent du CI (Python 3.14). --- .gitignore | 1 - imghdr.py | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 imghdr.py diff --git a/.gitignore b/.gitignore index 8bd7ae8..05c4f6d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ downloads/ .env .env/ .venv/ -imghdr.py __pycache__/ commands/__pycache__/ utils/__pycache__/* diff --git a/imghdr.py b/imghdr.py new file mode 100644 index 0000000..f2584ed --- /dev/null +++ b/imghdr.py @@ -0,0 +1,17 @@ +import struct + +def what(filename, h=None): + if h is None: + with open(filename, 'rb') as f: + h = f.read(32) + if h is None or len(h) < 8: + return None + if h.startswith(b'\x89PNG\r\n\x1a\n'): + return 'png' + if h.startswith(b'\xff\xd8'): + return 'jpeg' + if h.startswith(b'GIF87a') or h.startswith(b'GIF89a'): + return 'gif' + if h.startswith(b'RIFF') and h[8:12] == b'WEBP': + return 'webp' + return None From 524ba563cfe6fe19814ed9aa8d8dcfbd2c0e0204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 11:16:00 +0200 Subject: [PATCH 17/21] fix: add urllib3 to requirements.txt for PTB v13.7 fallback PTB v13.7 vendored urllib3 but its vendored six.moves est incompatible avec Python 3.14. Le fallback import urllib3 echouait car urllib3 n'etait pas declare dans requirements. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 8811541..aff643e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ python-telegram-bot==13.7 +urllib3>=1.26,<3 yt-dlp>=2021.12.1 ffmpeg-python>=0.2.0 pycurl>=7.43.0.6 From 410a3736b0f0435af92890ed2d5670504c094b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 11:17:53 +0200 Subject: [PATCH 18/21] fix: pin urllib3<2 and CI Python to 3.11 for PTB v13.7 compat PTB v13.7 utilise urllib3.contrib.appengine qui a ete supprime en urllib3 2.x. Pin urllib3>=1.26,<2 pour conserver la compat. Le CI validate utilisait Python 3.x (3.14) trop recent pour cette ancienne lib. Pin a 3.11, la cible du projet. --- .github/workflows/deploy.yml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3657eed..999a2fe 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.x' + python-version: '3.11' - name: Install dependencies run: pip install -r requirements.txt - name: Verify imports diff --git a/requirements.txt b/requirements.txt index aff643e..5005b5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ python-telegram-bot==13.7 -urllib3>=1.26,<3 +urllib3>=1.26,<2 yt-dlp>=2021.12.1 ffmpeg-python>=0.2.0 pycurl>=7.43.0.6 From 48cd1fbc3fdfed6920634fe1139bb5f2d426928e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 11:22:50 +0200 Subject: [PATCH 19/21] ci: bump setup-qemu and setup-buildx actions to v4 v3 utilisait Node.js 20 (deprecated). v4 utilise Node.js 24. Warning: 'docker/setup-buildx-action@v3, docker/setup-qemu-action@v3 target Node.js 20 but are forced to run on Node.js 24'. --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 999a2fe..3328824 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,10 +31,10 @@ jobs: uses: actions/checkout@v6 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Packages uses: docker/login-action@v4 From 5134809d9c942d8ff97b224abe316a3fca76dc4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= Date: Fri, 19 Jun 2026 11:25:08 +0200 Subject: [PATCH 20/21] docs: update AGENTS.md and .env.example after deploy optimisations AGENTS.md reflete desormais : - architecture modulaire (commands/, utils/) - retention via mtime et cleanup_by_retention() - FFMPEG_PATH fallback vers PATH - urllib3<2 et imghdr.py pour PTB compat - CI validate + multi-arch + Python 3.11 - setup.sh, docker-compose.yml, .dockerignore - VERSION dans config.py (plus BOT_VERSION dans main.py) - egg-socialvideodownload.json supprime .env.example : VERSION V9.2 (sync avec config.py) --- .env.example | 2 +- AGENTS.md | 195 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 126 insertions(+), 71 deletions(-) diff --git a/.env.example b/.env.example index be65f66..55a3b7e 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN_HERE # === Configuration générale === -VERSION=V9.0 +VERSION=V9.2 DEVELOPED_BY=Tom V. | OverStyleFR FFMPEG_PATH=ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg diff --git a/AGENTS.md b/AGENTS.md index 5528346..54d77f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,114 +1,168 @@ # Agent Guidelines: SocialVideoDownload.py ## Project Overview -A **Telegram bot** (single-file Python application) that downloads videos and music from social media links (YouTube, TikTok, etc.) and sends them back to users. Deployed as a Docker container to GitHub Packages. +A **Telegram bot** (modular Python application) that downloads videos and music from social media links (YouTube, TikTok, etc.) and sends them back to users. Deployed as a Docker container to GitHub Container Registry. -- **Language**: Python 3.11 (target), Python 3.x (CI) -- **Primary file**: `main.py` (monolith, ~450 lines) -- **Framework**: `python-telegram-bot==12.8` — **critical**: this is the old v12 synchronous API (`Updater`, `Dispatcher`, `use_context=True`). Do NOT use modern v20+ async patterns (`Application`, `ContextTypes`, etc.); they are incompatible. -- **Deployment target**: Docker image → `ghcr.io/...` (GitHub Container Registry) +- **Language**: Python 3.11 (target) +- **Architecture**: Modular (commands/, utils/ packages) with entry point `main.py` +- **Framework**: `python-telegram-bot==13.7` — **critical**: this is the old v12 synchronous API (`Updater`, `Dispatcher`, `use_context=True`). Do NOT use modern v20+ async patterns; they are incompatible. +- **Deployment**: Docker image → `ghcr.io/OverStyleFR/SocialVideoDownload.py` +- **CI/CD**: GitHub Actions with multi-arch (`linux/amd64`, `linux/arm64`) ## Essential Commands | Command | Purpose | |---------|---------| -| `python main.py` | Run the bot locally | +| `python main.py` | Run the bot locally (requires `.env`) | +| `bash setup.sh` | One-time local setup (venv + pip install + .env) | +| `docker compose up -d` | Run the Docker container locally | | `docker build -t socialvideodownload .` | Build Docker image | -| `docker run -v $(pwd)/token.txt:/app/token.txt socialvideodownload` | Run container (needs `token.txt` mounted) | +| `docker run -v $(pwd)/.env:/app/.env socialvideodownload` | Run container | | `pip install -r requirements.txt` | Install dependencies | | `echo "No tests to run"` | Current test suite (there are **no tests**) | **No test framework is configured.** The CI workflow explicitly skips tests with a placeholder `echo`. +## Project Structure + +``` +. +├── main.py # Entry point +├── config.py # Configuration from .env +├── commands/ +│ ├── start.py # /start handler +│ ├── help.py # /help handler +│ ├── download.py # /download handler +│ ├── music.py # /music handler +│ ├── stats.py # /stats handler +│ ├── auto_download.py # Auto-download via text messages +│ └── upload.py # Upload helper (now in utils/) +├── utils/ +│ ├── cache.py # Cache hit tracking +│ ├── disk_manager.py # Downloads cleanup (retention + emergency) +│ ├── file_manager.py # Hash-based dedup (sha256 of URL) +│ ├── logger.py # Console + file logging +│ ├── progress_file.py # Progress-aware file wrapper for uploads +│ ├── retention.py # File retention via mtime +│ ├── token_loader.py # .env token loading +│ ├── curl_uploader.py # External upload via curl.libriciel.fr +│ └── upload.py # Telegram file upload with progress +├── imghdr.py # Compatibility shim (removed from stdlib 3.13+) +├── Dockerfile # Multi-stage: bookworm base +├── docker-compose.yml # Local dev container +├── setup.sh # Standalone setup script +├── .dockerignore # Excludes ffmpeg/, .kilo/, *.md, etc. +├── .env.example # Environment template +├── .gitignore +├── requirements.txt +└── AGENTS.md +``` + ## Architecture & Data Flow ``` Telegram Message ↓ - python-telegram-bot v12 handlers (main.py) + python-telegram-bot v12 handlers (commands/*.py) ↓ - Subprocess: ./yt-dlp [options] -o download_temp/.mp4 + yt-dlp (Python package, not subprocess) → downloads/.<ext> ↓ - [Video path] → Check file size (≤ 50MB) → bot.send_video() - [Music path] → ffmpeg extract-audio → .mp3 → bot.send_audio() + [Video] → check retention → bot.send_video() + [Music] → ffmpeg extract-audio → .mp3 → retention → bot.send_audio() ``` -- **Single entry point**: `main.py` contains everything — handlers, retry logic, logging, token reading, startup cleanup. -- **No modules/packages**: All logic is in one file. There is no `src/` or package structure. -- **Subprocess-heavy**: Both video (`./yt-dlp`) and audio (`ffmpeg`) pipelines spawn external binaries. -- **Synchronous**: The entire bot is sync. All handlers block on I/O (download, ffmpeg, Telegram upload). Do not introduce `async`/`await` unless you are migrating the entire framework. +- **Modules**: Logic is split into `commands/` (handlers) and `utils/` (infrastructure). +- **yt-dlp**: Used as a **Python package** (`import yt_dlp`), not a subprocess binary. +- **Synchronous**: The entire bot is sync. All handlers block on I/O. Do not introduce `async`/`await` unless migrating the entire framework. + +### Caching & Dedup +- **Hash-based**: URL → SHA-256 → stored in `downloads/hashes.txt`. Before downloading, `is_already_downloaded()` checks if the hash exists. +- **File check**: Even if the hash exists, the bot verifies the file still exists on disk (yt-dlp's `prepare_filename`). If missing, it re-downloads and the hash line persists (harmless, duplicates are per-session only). -### Caching -Downloaded files are cached in `download_temp/` using an **MD5 hash of the URL** as the filename (`<hash>.mp4` or `<hash>.mp3`). The bot checks for existence before re-downloading. +### Retention Policy +- Files get their **mtime set to `now + retention`** after download via `set_retention()`. +- Small files (< `SMALL_FILE_SIZE_MB`) and mp3s: retention = `RETENTION_SMALL_HOURS` (default 24h). +- Large files: retention = `RETENTION_LARGE_HOURS` (default 2h). +- `cleanup_by_retention()` removes files whose mtime < now (expired retention). +- `check_and_clean_if_needed()` tries retention first, then full clear if still low on space. ### Startup Behavior (`main()`) -On every start, the bot **empties** `download_temp/` (deletes all files inside) if it exists, or creates it if missing. This means the cache is not persistent across restarts. +1. `clear_downloads()` — deletes everything in `downloads/` (fresh start). +2. `load_cache()` — loads cache tracking from disk. +3. Background thread: `scheduled_cleanup()` runs `cleanup_by_retention()` every `CLEANUP_INTERVAL_HOURS`. ### File-Size Guard -Telegram bot API limits: the bot hardcodes a **50 MB** ceiling (`max_size_bytes = 50 * 1024 * 1024`). Files exceeding this are rejected with a French error message. - -## External Dependencies (Binaries) - -| Binary | Expected Location | Used For | -|--------|-------------------|----------| -| `yt-dlp` | `./yt-dlp` (cwd) | Video/audio downloading | -| `ffmpeg` | `ffmpeg-6.1-amd64-static/ffmpeg` | Audio extraction (music command) | - -- `yt-dlp` appears to be a vendored/committed binary in the repo root (not a Python package). -- `ffmpeg` is bundled as a static build directory (`ffmpeg-6.1-amd64-static/`). The music command hardcodes this relative path. -- In Docker, FFmpeg is copied from a multi-stage `ghcr.io/linuxserver/ffmpeg:latest` image into `/usr/local/bin/`. The local static path is only relevant for local runs. - -## Configuration & Secrets - -- **Token source**: `token.txt` (plain text file, one line, **not** an env variable). It is `.gitignore`d. -- **No `.env` library**: The project does not load environment variables. `token.txt` is read directly in `read_token()`. -- **Bot metadata**: `BOT_VERSION = "V0.7-3"` and `YOUR_NAME = "Tom V. | OverStyleFR"` are hardcoded constants near the top of `main.py`. +Telegram bot API limits: the bot hardcodes a **35 MB** ceiling (`MAX_FILE_SIZE = 35 * 1024 * 1024` in `utils/upload.py`). Files exceeding this are uploaded externally via `curl.libriciel.fr`. + +## External Dependencies + +| Dependency | Type | Used For | +|------------|------|----------| +| `yt-dlp` | Python package (pip) | Video/audio downloading | +| `ffmpeg` | System binary | Audio extraction (music command) | + +- **FFmpeg** is resolved via `config.FFMPEG_PATH`: + - Default: `ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg` (local dev) + - If the configured path doesn't exist: falls back to `"ffmpeg"` (system PATH) + - In Docker: copied from `ghcr.io/linuxserver/ffmpeg:latest` into `/usr/local/bin/` +- **yt-dlp** is a Python dependency (not a vendored binary). + +## Configuration & Secrets (`.env`) + +| Variable | Default | Description | +|----------|---------|-------------| +| `BOT_TOKEN` | *(required)* | Telegram bot token | +| `VERSION` | `V9.2` | Bot version (used for Docker tags) | +| `DEVELOPED_BY` | `Tom V. \| OverStyleFR` | Author credit | +| `FFMPEG_PATH` | see above | Path to ffmpeg binary | +| `MIN_FREE_SPACE_MB` | `500` | Min free space before emergency cleanup | +| `CLEANUP_INTERVAL_HOURS` | `24` | Interval between scheduled cleanups | +| `SMALL_FILE_SIZE_MB` | `4` | Threshold for small/large file retention | +| `RETENTION_SMALL_HOURS` | `24` | Retention for small files + mp3 | +| `RETENTION_LARGE_HOURS` | `2` | Retention for large files | + +- **Token source**: `.env` file (read via `python-dotenv`). If missing, `token_loader.py` creates a template and exits. +- **`token.txt`** is deprecated (was used by the old egg-pterodactyl setup). Now `.env` is the sole config source. ## Code Patterns & Conventions -- **Language**: UI strings and comments are in **French** (e.g., "Téléchargement en cours", "Veuillez patienter..."). Maintain this for user-facing messages. -- **Logging**: Two loggers: - - Root logger → `logs/bot_YYYYMMDD_HHMMSS.log` (files) - - `console_logger` ("console_logger") → both file and `StreamHandler` - Format: `'%(asctime)s - %(levelname)s - %(message)s'` -- **Retry logic**: Downloads use a `while current_retry < max_retries` loop with `max_retries = 3`. -- **Error handling pattern**: Broad `except Exception` + `except urllib3.exceptions.HTTPError` + `except subprocess.CalledProcessError`. Sometimes `break` on generic errors, sometimes continue retrying. -- **Resource management**: Files are `open()`ed and manually `.close()`d. There is no `with` statement for file handles. -- **Result persistence**: `save_result_to_file()` writes yt-dlp stdout/stderr to `download_temp/download_result/result_YYYYMMDD_HHMMSS.txt`. +- **Language**: UI strings and comments are in **French** (e.g., "Téléchargement en cours"). Maintain this for user-facing messages. +- **Logging**: Single `console_logger` ("TelegramBot") with colored console output + daily file logs in `logs/`. +- **Retry logic**: Downloads use a `while attempts < max_attempts` loop with `max_attempts = 3`. +- **Error handling**: Broad `except Exception` with logging. Some paths use `try/except` inside retry loops. ## Important Gotchas -1. **Old Telegram API**: If you add new handlers, use v12 semantics: +1. **Old Telegram API**: Use v12 semantics: - `CommandHandler("cmd", func, pass_args=True)` for arguments - `MessageHandler(Filters.text & ~Filters.command, func)` for plain text - - `update.message.chat_id`, `context.bot.send_video(...)` — **not** `update.effective_chat.id` or `context.bot.send_video(...)` with v20 kwargs. + - `update.message.chat_id`, `context.bot.send_video(...)` — **not** v20 patterns. -2. **Missing file handles**: Several code paths `open()` files but do not wrap them in `try/finally` or `with`, risking leaks on exceptions. +2. **`python-telegram-bot==13.7` + Python ≥3.13**: The vendored urllib3 in PTB breaks on Python ≥3.13. A local `imghdr.py` shim is committed for Python 3.13+ stdlib changes. `urllib3<2` is pinned in `requirements.txt` to avoid removal of `urllib3.contrib.appengine`. -3. **Cleanup logic is asymmetric**: - - `/download` attempts `os.remove(video_path)` in `finally` **only if the file does NOT exist** (`if video_path and not os.path.exists(video_path): os.remove(video_path)`). This is likely a bug — it means successful files are never cleaned up, but failed paths throw `FileNotFoundError` silently. - - `/music` does the same inverted check. - - `handle_text_messages` (auto-download) has **no cleanup at all**. +3. **CI validate job**: Runs on Python 3.11 (target version). Using newer Python (3.13+) will fail due to PTB compatibility issues. -4. **No dependency management for yt-dlp**: The binary in the repo root may become outdated. The Dockerfile does not fetch a fresh `yt-dlp` during build; it relies on whatever is committed. +4. **Cleanup on startup**: `clear_downloads()` wipes `downloads/` entirely (including `hashes.txt`). This means the hash cache is not persistent across restarts. -5. **50 MB limit**: Telegram’s bot API file-size cap is enforced client-side. If Telegram raises the limit, this constant must be updated manually. +5. **`egg-socialvideodownload.json`**: **Deleted** and deprecated. Was used for Pterodactyl/Pelican panel integration with `token.txt`. The bot now uses `.env` exclusively. 6. **Branch-based image tags** (`ghcr.io/...`): - - `main` → `latest` + SHA - - `develop` → `dev` + SHA - - Other branches → branch name + SHA + - `main` → `latest` + VERSION tag + - `develop` → `dev` + +7. **CI skips tests**: The workflow's "Run tests" is a placeholder. Adding tests requires updating `.github/workflows/deploy.yml`. -7. **CI skips tests**: The GitHub Actions workflow has a placeholder `echo "No tests to run"`. Adding tests requires updating `.github/workflows/deploy.yml`. +8. **Version**: Stored in `config.py` as `VERSION = os.getenv("VERSION", "V9.2")`. The CI reads it via `grep` to tag Docker images. ## Docker Notes -- Multi-stage build: - 1. `ffmpeg` stage — copies binaries from `linuxserver/ffmpeg` - 2. `builder` stage — runs `pip wheel` to create wheels - 3. Final stage — installs wheels, copies FFmpeg, copies source, runs `python main.py` -- Base image: `python:3.11-slim-bullseye` -- The `ffmpeg-6.1-amd64-static/` directory is copied into the image but the Dockerfile does **not** use it; it prefers the stage-copied `/usr/local/bin/ffmpeg`. +- **Multi-stage build**: + 1. `ffmpeg` stage — copies binaries from `linuxserver/ffmpeg:latest` + 2. `builder` stage — `pip wheel` on `python:3.11-slim-bookworm` + 3. Final stage — installs wheels (excluding setuptools — kept from base image), copies FFmpeg, `COPY . .`, runs `python main.py` +- **Base image**: `python:3.11-slim-bookworm` +- **`.dockerignore`** excludes `ffmpeg/`, `.kilo/`, `*.md`, `.env`, etc. to minimize image size. +- **`docker-compose.yml`** mounts `.env`, `downloads/`, and `logs/` as volumes. ## Git & Branches @@ -118,9 +172,10 @@ Telegram bot API limits: the bot hardcodes a **50 MB** ceiling (`max_size_bytes ## When Modifying This Codebase -- Keep everything in `main.py` unless the change is large enough to justify splitting (the project has no import structure). -- Preserve French user-facing strings. -- Do not upgrade `python-telegram-bot` without rewriting all handler signatures and startup logic. -- If adding a new command, remember to `dp.add_handler(...)` in `main()` before `updater.start_polling()`. -- If you introduce `async`, you must rewrite the entire bot (handlers, dispatcher, updater → ApplicationBuilder). Prefer sync additions to avoid a full migration. -- Update `BOT_VERSION` when shipping meaningful changes. +- **Preserve French** user-facing strings. +- **Do not upgrade** `python-telegram-bot` without rewriting all handler signatures (v12 → v20 is a full rewrite). +- If adding a new command, create the handler in `commands/`, add `dp.add_handler(...)` in `main()` before `updater.start_polling()`. +- If you introduce `async`, you must rewrite the entire bot (handlers, dispatcher, updater → ApplicationBuilder). Prefer sync additions. +- **Update `VERSION`** in `.env` / `config.py` default when shipping meaningful changes. +- **New dependencies**: If a dependency requires a Python feature removed in 3.13+ (like `imghdr`), provide a compatibility shim and commit it (do NOT gitignore). +- **`urllib3` pin**: Keep `urllib3<2` pinned — PTB v13.7 uses `urllib3.contrib.appengine` which was removed in urllib3 2.x. From 11aa20c809af43eee342ef11e3564fbab247cdd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20V=2E=20=7C=20Ov=E1=B4=87=CA=80Styl=E1=B4=87FR?= <personnal@tomv.ovh> Date: Fri, 19 Jun 2026 11:27:34 +0200 Subject: [PATCH 21/21] fix: VERSION sourcee depuis .env/.env.example, pas config.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit config.py : VERSION = os.getenv('VERSION', 'unknown') — plus de fallback hardcode. .env est la source de verite. CI : lit VERSION depuis .env.example (commite) pour les tags Docker. AGENTS.md : documente .env comme source de verite pour la version. --- .github/workflows/deploy.yml | 2 +- AGENTS.md | 3 ++- config.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3328824..79f3d9e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -52,7 +52,7 @@ jobs: run: | REPO="ghcr.io/${{ env.repo_name }}" if [ "${{ github.ref }}" = "refs/heads/main" ]; then - VERSION=$(grep '^VERSION' config.py | head -1 | sed 's/.*, "\(.*\)").*/\1/') + VERSION=$(grep '^VERSION' .env.example | head -1 | sed 's/.*=\(.*\)/\1/') echo "tags=${REPO}:latest,${REPO}:${VERSION}" >> $GITHUB_OUTPUT elif [ "${{ github.ref }}" = "refs/heads/develop" ]; then echo "tags=${REPO}:dev" >> $GITHUB_OUTPUT diff --git a/AGENTS.md b/AGENTS.md index 54d77f4..a451bf3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,7 +112,7 @@ Telegram bot API limits: the bot hardcodes a **35 MB** ceiling (`MAX_FILE_SIZE = | Variable | Default | Description | |----------|---------|-------------| | `BOT_TOKEN` | *(required)* | Telegram bot token | -| `VERSION` | `V9.2` | Bot version (used for Docker tags) | +| `VERSION` | `V9.2` | Bot version (used for Docker tags, /stats, /help) — source de vérité : `.env` / `.env.example` | | `DEVELOPED_BY` | `Tom V. \| OverStyleFR` | Author credit | | `FFMPEG_PATH` | see above | Path to ffmpeg binary | | `MIN_FREE_SPACE_MB` | `500` | Min free space before emergency cleanup | @@ -123,6 +123,7 @@ Telegram bot API limits: the bot hardcodes a **35 MB** ceiling (`MAX_FILE_SIZE = - **Token source**: `.env` file (read via `python-dotenv`). If missing, `token_loader.py` creates a template and exits. - **`token.txt`** is deprecated (was used by the old egg-pterodactyl setup). Now `.env` is the sole config source. +- **Version** : la source de vérité est `.env` (via `VERSION=`). `.env.example` est le template commité. La CI lit `VERSION` depuis `.env.example` pour les tags Docker. `config.py` n'a plus de fallback hardcodé — si `.env` manque, la version affichée est `"unknown"`. ## Code Patterns & Conventions diff --git a/config.py b/config.py index c9dd9d2..7411745 100644 --- a/config.py +++ b/config.py @@ -4,7 +4,7 @@ load_dotenv(".env") -VERSION = os.getenv("VERSION", "V9.2") +VERSION = os.getenv("VERSION", "unknown") # Source de verite : .env / .env.example DEVELOPED_BY = os.getenv("DEVELOPED_BY", "Tom V. | OverStyleFR") _FFMPEG_DEFAULT = "ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg" FFMPEG_PATH = os.getenv("FFMPEG_PATH", _FFMPEG_DEFAULT)