Update app.py
Browse files
app.py
CHANGED
@@ -1,367 +1,192 @@
|
|
1 |
-
from flask import Flask, render_template, request,
|
2 |
-
from
|
3 |
-
from google.genai import types
|
4 |
-
from PIL import Image
|
5 |
-
import subprocess
|
6 |
-
import tempfile
|
7 |
import os
|
8 |
-
import
|
9 |
-
import
|
10 |
-
import
|
11 |
|
12 |
-
|
13 |
-
|
14 |
-
# --- Cognvstantes ---
|
15 |
-
MODEL_SINGLE_GENERATION = "gemini-2.5-pro-exp-03-25" # Modèle avec "thinking" pour la génération complète
|
16 |
-
LATEX_MENTION = r"\vspace{1cm}\noindent\textit{Ce devoir a été généré par Mariam AI. \url{https://mariam-241.vercel.app}}"
|
17 |
-
|
18 |
-
# --- Configuration des secrets ---
|
19 |
-
# Dans un environnement de production, utilisez les variables d'environnement ou un fichier .env
|
20 |
-
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY', '')
|
21 |
-
|
22 |
-
# --- Fonctions Utilitaires ---
|
23 |
-
|
24 |
-
def check_latex_installation():
|
25 |
-
"""Vérifie si pdflatex est accessible dans le PATH système."""
|
26 |
-
try:
|
27 |
-
subprocess.run(['pdflatex', '--version'], capture_output=True, check=True, timeout=10)
|
28 |
-
return True, "✅ Installation LaTeX (pdflatex) trouvée."
|
29 |
-
except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError) as e:
|
30 |
-
return False, f"❌ pdflatex introuvable ou ne répond pas. Veuillez installer une distribution LaTeX (comme TeX Live ou MiKTeX) et vous assurer qu'elle est dans le PATH système. Erreur: {e}"
|
31 |
-
|
32 |
-
def initialize_genai_client():
|
33 |
-
"""Initialise et retourne le client Google GenAI."""
|
34 |
-
try:
|
35 |
-
if not GOOGLE_API_KEY:
|
36 |
-
return None, "Clé API Google non trouvée dans les variables d'environnement."
|
37 |
-
|
38 |
-
client = genai.Client(api_key=GOOGLE_API_KEY)
|
39 |
-
return client, "✅ Client Mariam AI initialisé."
|
40 |
-
except Exception as e:
|
41 |
-
return None, f"❌ Erreur lors de l'initialisation du client: {str(e)}"
|
42 |
|
43 |
-
|
44 |
-
|
45 |
-
if not isinstance(raw_latex_text, str):
|
46 |
-
return "" # Retourne une chaîne vide si l'entrée n'est pas une chaîne
|
47 |
-
|
48 |
-
cleaned = raw_latex_text.strip()
|
49 |
|
50 |
-
|
51 |
-
if cleaned.startswith("```latex"):
|
52 |
-
cleaned = cleaned[8:]
|
53 |
-
elif cleaned.startswith("```"):
|
54 |
-
# Gère le cas où il y a un saut de ligne après ```
|
55 |
-
lines = cleaned.split('\n', 1)
|
56 |
-
if len(lines) > 1:
|
57 |
-
cleaned = lines[1]
|
58 |
-
else:
|
59 |
-
cleaned = cleaned[3:] # Si pas de saut de ligne, juste enlever ```
|
60 |
|
61 |
-
|
62 |
-
if cleaned.endswith("```"):
|
63 |
-
cleaned = cleaned[:-3].strip() # Enlève ``` et les espaces potentiels avant
|
64 |
|
65 |
-
|
66 |
-
if not cleaned.startswith("\\documentclass"):
|
67 |
-
# On pourrait choisir de lever une erreur ou de tenter une correction
|
68 |
-
pass
|
69 |
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
# Ajoute seulement si \documentclass est présent pour éviter de créer un doc invalide
|
77 |
-
if cleaned.strip().startswith("\\documentclass"):
|
78 |
-
cleaned += f"\n{end_doc_tag}"
|
79 |
|
80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
|
82 |
-
|
|
|
83 |
|
84 |
-
|
85 |
-
|
86 |
-
Génère une solution complète en LaTeX à partir de l'image en une seule étape.
|
87 |
-
"""
|
88 |
try:
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
# TÂCHE PRINCIPALE
|
106 |
-
|
107 |
-
1. **Analyse :** Lis et comprends parfaitement l'énoncé de l'exercice fourni.
|
108 |
-
2. **Résolution :** Résous l'exercice de manière exhaustive, en détaillant chaque étape de raisonnement et de calcul.
|
109 |
-
3. **Rédaction du Code Source :** Rédige la solution complète *directement* sous forme de code source. Ce code doit être structuré pour produire un document final qui respecte les standards de qualité décrits ci-dessous.
|
110 |
-
|
111 |
-
# QUALITÉ ET PRÉSENTATION ATTENDUES DU DOCUMENT FINAL
|
112 |
-
|
113 |
-
* **Structure Professionnelle :**
|
114 |
-
* Le document doit avoir une présentation initiale claire (titre de l'exercice, sous-titre, date).
|
115 |
-
* Une table des matières doit être incluse si la solution est un peu longue.
|
116 |
-
* Les titres des sections et sous-sections doivent être clairs et bien mis en forme.
|
117 |
-
* **Mise en Valeur Pédagogique :**
|
118 |
-
* L'énoncé original de l'exercice doit être clairement rappelé au début.
|
119 |
-
* Utilise des mises en forme distinctes (par exemple, des boîtes ou des styles spécifiques) pour :
|
120 |
-
* Les définitions ou théorèmes importants.
|
121 |
-
* Les résultats finaux ou les conclusions clés de chaque question.
|
122 |
-
* Les remarques, astuces méthodologiques, ou points d'attention.
|
123 |
-
* Les exemples illustratifs, si besoin.
|
124 |
-
* **Clarté Mathématique :**
|
125 |
-
* Les formules mathématiques doivent être écrites correctement, alignées et faciles à lire.
|
126 |
-
* Utilise des notations mathématiques standard et cohérentes (par exemple, pour les ensembles de nombres $\mathbb{R}, \mathbb{N}$, les limites, les dérivées, etc.).
|
127 |
-
* **Lisibilité Générale :**
|
128 |
-
* Le texte doit être en français, clair et précis.
|
129 |
-
* La mise en page doit être aérée (paragraphes distincts, espacements).
|
130 |
-
* **Graphiques (si pertinent) :**
|
131 |
-
* Si l'exercice implique une étude de fonction ou de la géométrie, inclus des graphiques propres et bien légendés (axes, courbes, points importants, asymptotes). Les couleurs doivent être utilisées judicieusement pour améliorer la compréhension.
|
132 |
-
|
133 |
-
# CONTENU PÉDAGOGIQUE DE LA SOLUTION
|
134 |
-
|
135 |
-
1. **Rigueur et Justification :** Justifie *chaque* étape de calcul ou de raisonnement.
|
136 |
-
2. **Clarté des Explications :** Explique les "pourquoi" derrière les méthodes utilisées. Utilise un langage mathématique précis.
|
137 |
-
3. **Complétude :** Traite toutes les questions de l'énoncé.
|
138 |
-
4. **Conclusion :** Si pertinent, conclus en résumant les points essentiels.
|
139 |
-
|
140 |
-
# CONTRAINTES STRICTES
|
141 |
-
|
142 |
-
* Le seul output doit être le code source brut permettant de générer le document décrit.
|
143 |
-
* Aucun texte avant la première ligne du code source ou après la dernière ligne (sauf la mention spéciale ci-dessous).
|
144 |
-
* Inclus la ligne `{LATEX_MENTION}` juste avant la toute fin du code source.
|
145 |
-
|
146 |
-
"""
|
147 |
-
|
148 |
-
response = client.models.generate_content(
|
149 |
-
model=MODEL_SINGLE_GENERATION,
|
150 |
-
contents=[image, prompt],
|
151 |
-
config=genai.types.GenerateContentConfig(
|
152 |
-
temperature=1,
|
153 |
-
),
|
154 |
-
|
155 |
-
)
|
156 |
-
|
157 |
-
latex_content_raw = None
|
158 |
-
thinking_content = None
|
159 |
-
|
160 |
-
# Extrait le contenu LaTeX et le raisonnement (thought)
|
161 |
-
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
|
162 |
-
for part in response.candidates[0].content.parts:
|
163 |
-
if getattr(part, 'thought', None):
|
164 |
-
thinking_content = part.text
|
165 |
-
else:
|
166 |
-
latex_content_raw = part.text
|
167 |
-
|
168 |
-
if latex_content_raw:
|
169 |
-
latex_content_cleaned = clean_latex(latex_content_raw)
|
170 |
-
|
171 |
-
# Vérification supplémentaire : est-ce que la mention obligatoire est présente ?
|
172 |
-
if LATEX_MENTION not in latex_content_cleaned:
|
173 |
-
# Tente de l'insérer avant \end{document}
|
174 |
-
end_doc_tag = "\\end{document}"
|
175 |
-
if end_doc_tag in latex_content_cleaned:
|
176 |
-
latex_content_cleaned = latex_content_cleaned.replace(end_doc_tag, f"{LATEX_MENTION}\n{end_doc_tag}")
|
177 |
-
else:
|
178 |
-
# Si \end{document} n'est pas là, ajoute les deux à la fin
|
179 |
-
latex_content_cleaned += f"\n{LATEX_MENTION}\n{end_doc_tag}"
|
180 |
-
|
181 |
-
return latex_content_cleaned, thinking_content, None
|
182 |
else:
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
except Exception as e:
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
def compile_latex_to_pdf(latex_content):
|
193 |
-
"""
|
194 |
-
Compile une chaîne de caractères contenant du code LaTeX en fichier PDF.
|
195 |
-
Utilise un répertoire temporaire pour les fichiers intermédiaires.
|
196 |
-
"""
|
197 |
-
if not latex_content:
|
198 |
-
return None, "Impossible de compiler : contenu LaTeX vide."
|
199 |
-
|
200 |
-
# Utilise un répertoire temporaire sécurisé qui sera nettoyé automatiquement
|
201 |
-
with tempfile.TemporaryDirectory() as temp_dir:
|
202 |
-
tex_filename = "solution.tex"
|
203 |
-
pdf_filename = "solution.pdf"
|
204 |
-
tex_filepath = os.path.join(temp_dir, tex_filename)
|
205 |
-
pdf_filepath = os.path.join(temp_dir, pdf_filename)
|
206 |
-
log_filepath = os.path.join(temp_dir, "solution.log") # Pour le débogage
|
207 |
-
|
208 |
-
# Écrit le contenu LaTeX dans le fichier .tex avec encodage UTF-8
|
209 |
-
try:
|
210 |
-
with open(tex_filepath, "w", encoding="utf-8") as f:
|
211 |
-
f.write(latex_content)
|
212 |
-
except IOError as e:
|
213 |
-
return None, f"❌ Erreur lors de l'écriture du fichier .tex temporaire: {str(e)}"
|
214 |
-
|
215 |
-
# Exécute pdflatex
|
216 |
-
command = [
|
217 |
-
"pdflatex",
|
218 |
-
"-interaction=nonstopmode",
|
219 |
-
f"-output-directory={temp_dir}",
|
220 |
-
f"-jobname={os.path.splitext(pdf_filename)[0]}", # Nom sans extension
|
221 |
-
tex_filepath
|
222 |
-
]
|
223 |
-
|
224 |
-
pdf_generated = False
|
225 |
-
compilation_output = ""
|
226 |
|
227 |
-
|
228 |
-
try:
|
229 |
-
# Augmentation du timeout pour les compilations potentiellement longues
|
230 |
-
result = subprocess.run(command, capture_output=True, check=True, text=True, encoding='utf-8', errors='replace', timeout=60)
|
231 |
-
compilation_output += f"Passe {pass_num}: Succès\n"
|
232 |
-
|
233 |
-
# Vérifie si le PDF a été créé après la première passe (au moins)
|
234 |
-
if os.path.exists(pdf_filepath):
|
235 |
-
pdf_generated = True
|
236 |
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
|
|
|
|
|
|
|
|
244 |
try:
|
245 |
-
|
246 |
-
|
247 |
-
except
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
267 |
else:
|
268 |
-
|
269 |
-
|
270 |
-
try:
|
271 |
-
with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
|
272 |
-
error_message += f"Extrait du fichier log (solution.log) après échec de génération PDF:\n{log_file.read()[-2000:]}"
|
273 |
-
except IOError:
|
274 |
-
error_message += "Impossible de lire le fichier log de LaTeX."
|
275 |
-
return None, error_message
|
276 |
-
|
277 |
-
# --- Routes Flask ---
|
278 |
|
279 |
-
|
280 |
-
def index():
|
281 |
-
return render_template('index.html')
|
282 |
-
|
283 |
-
@app.route('/check-latex', methods=['GET'])
|
284 |
-
def check_latex():
|
285 |
-
latex_installed, message = check_latex_installation()
|
286 |
-
return jsonify({
|
287 |
-
'success': latex_installed,
|
288 |
-
'message': message
|
289 |
-
})
|
290 |
-
|
291 |
-
@app.route('/check-api', methods=['GET'])
|
292 |
-
def check_api():
|
293 |
-
client, message = initialize_genai_client()
|
294 |
-
return jsonify({
|
295 |
-
'success': client is not None,
|
296 |
-
'message': message
|
297 |
-
})
|
298 |
-
|
299 |
-
@app.route('/process', methods=['POST'])
|
300 |
-
def process():
|
301 |
-
if 'image' not in request.files:
|
302 |
-
return jsonify({'success': False, 'message': 'Aucune image téléchargée'})
|
303 |
-
|
304 |
-
file = request.files['image']
|
305 |
-
if file.filename == '':
|
306 |
-
return jsonify({'success': False, 'message': 'Aucun fichier sélectionné'})
|
307 |
-
|
308 |
-
# Lire l'image
|
309 |
-
image_bytes = file.read()
|
310 |
-
|
311 |
-
# Initialiser le client
|
312 |
-
client, message = initialize_genai_client()
|
313 |
-
if client is None:
|
314 |
-
return jsonify({'success': False, 'message': message})
|
315 |
-
|
316 |
-
# Générer le LaTeX
|
317 |
-
latex_content, thinking_content, error = generate_complete_latex(client, image_bytes)
|
318 |
-
if error:
|
319 |
-
return jsonify({'success': False, 'message': error})
|
320 |
-
|
321 |
-
# Compiler en PDF
|
322 |
-
pdf_data, message = compile_latex_to_pdf(latex_content)
|
323 |
-
if pdf_data is None:
|
324 |
-
return jsonify({
|
325 |
-
'success': False,
|
326 |
-
'message': message,
|
327 |
-
'latex': latex_content,
|
328 |
-
'thinking': thinking_content
|
329 |
-
})
|
330 |
-
|
331 |
-
# Encoder le PDF en base64 pour le retourner au front-end
|
332 |
-
pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
|
333 |
-
|
334 |
-
# Retourner les données
|
335 |
-
return jsonify({
|
336 |
-
'success': True,
|
337 |
-
'message': 'PDF généré avec succès',
|
338 |
-
'latex': latex_content,
|
339 |
-
'thinking': thinking_content,
|
340 |
-
'pdf_base64': pdf_base64
|
341 |
-
})
|
342 |
-
|
343 |
-
@app.route('/download-pdf', methods=['POST'])
|
344 |
-
def download_pdf():
|
345 |
-
if 'pdf_data' not in request.json:
|
346 |
-
return jsonify({'success': False, 'message': 'Données PDF manquantes'})
|
347 |
-
|
348 |
try:
|
349 |
-
|
|
|
|
|
350 |
|
351 |
-
|
352 |
-
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
|
353 |
-
temp_file.write(pdf_data)
|
354 |
-
temp_path = temp_file.name
|
355 |
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
363 |
except Exception as e:
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory
|
2 |
+
from flask_sqlalchemy import SQLAlchemy
|
|
|
|
|
|
|
|
|
3 |
import os
|
4 |
+
import requests
|
5 |
+
from urllib.parse import urlparse
|
6 |
+
import logging
|
7 |
|
8 |
+
logging.basicConfig(level=logging.INFO)
|
9 |
+
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
+
app = Flask(__name__)
|
12 |
+
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'une_cle_secrete_par_defaut_pour_dev')
|
|
|
|
|
|
|
|
|
13 |
|
14 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://Podcast_owner:npg_gFdMDLO9lVa0@ep-delicate-surf-a4v7wopn-pooler.us-east-1.aws.neon.tech/Podcast?sslmode=require'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
|
|
|
17 |
|
18 |
+
db = SQLAlchemy(app)
|
|
|
|
|
|
|
19 |
|
20 |
+
AUDIO_CACHE_DIR = '/tmp/audio_cache'
|
21 |
+
try:
|
22 |
+
os.makedirs(AUDIO_CACHE_DIR, exist_ok=True)
|
23 |
+
logger.info(f"Répertoire de cache audio configuré sur : {AUDIO_CACHE_DIR}")
|
24 |
+
except OSError as e:
|
25 |
+
logger.error(f"Impossible de créer le répertoire de cache audio à {AUDIO_CACHE_DIR}: {e}")
|
|
|
|
|
|
|
26 |
|
27 |
+
class Podcast(db.Model):
|
28 |
+
__tablename__ = 'podcast'
|
29 |
+
id = db.Column(db.Integer, primary_key=True)
|
30 |
+
name = db.Column(db.String(200), nullable=False)
|
31 |
+
url = db.Column(db.String(500), nullable=False, unique=True)
|
32 |
+
subject = db.Column(db.String(100), nullable=False)
|
33 |
+
filename_cache = db.Column(db.String(255), nullable=True)
|
34 |
|
35 |
+
def __repr__(self):
|
36 |
+
return f'<Podcast {self.name}>'
|
37 |
|
38 |
+
@app.route('/')
|
39 |
+
def index():
|
|
|
|
|
40 |
try:
|
41 |
+
podcasts = Podcast.query.order_by(Podcast.name).all()
|
42 |
+
except Exception as e:
|
43 |
+
logger.error(f"Erreur lors de la récupération des podcasts: {e}")
|
44 |
+
flash("Erreur lors du chargement des podcasts depuis la base de données.", "error")
|
45 |
+
podcasts = []
|
46 |
+
return render_template('index.html', podcasts=podcasts)
|
47 |
+
|
48 |
+
@app.route('/gestion', methods=['GET', 'POST'])
|
49 |
+
def gestion():
|
50 |
+
if request.method == 'POST':
|
51 |
+
name = request.form.get('name')
|
52 |
+
url = request.form.get('url')
|
53 |
+
subject = request.form.get('subject')
|
54 |
+
|
55 |
+
if not name or not url or not subject:
|
56 |
+
flash('Tous les champs sont requis !', 'error')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
else:
|
58 |
+
existing_podcast = Podcast.query.filter_by(url=url).first()
|
59 |
+
if existing_podcast:
|
60 |
+
flash('Un podcast avec cette URL existe déjà.', 'warning')
|
61 |
+
else:
|
62 |
+
try:
|
63 |
+
new_podcast = Podcast(name=name, url=url, subject=subject)
|
64 |
+
db.session.add(new_podcast)
|
65 |
+
db.session.commit()
|
66 |
+
flash('Podcast ajouté avec succès !', 'success')
|
67 |
+
return redirect(url_for('gestion'))
|
68 |
+
except Exception as e:
|
69 |
+
db.session.rollback()
|
70 |
+
logger.error(f"Erreur lors de l'ajout du podcast: {e}")
|
71 |
+
flash(f"Erreur lors de l'ajout du podcast: {e}", 'error')
|
72 |
+
|
73 |
+
try:
|
74 |
+
podcasts = Podcast.query.order_by(Podcast.name).all()
|
75 |
except Exception as e:
|
76 |
+
logger.error(f"Erreur lors de la récupération des podcasts pour la gestion: {e}")
|
77 |
+
flash("Erreur lors du chargement des podcasts pour la gestion.", "error")
|
78 |
+
podcasts = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
|
80 |
+
return render_template('gestion.html', podcasts=podcasts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
|
82 |
+
@app.route('/delete_podcast/<int:podcast_id>', methods=['POST'])
|
83 |
+
def delete_podcast(podcast_id):
|
84 |
+
try:
|
85 |
+
podcast_to_delete = db.session.get(Podcast, podcast_id)
|
86 |
+
if not podcast_to_delete:
|
87 |
+
flash('Podcast non trouvé.', 'error')
|
88 |
+
return redirect(url_for('gestion'))
|
89 |
+
|
90 |
+
if podcast_to_delete.filename_cache:
|
91 |
+
cached_file_path = os.path.join(AUDIO_CACHE_DIR, podcast_to_delete.filename_cache)
|
92 |
+
if os.path.exists(cached_file_path):
|
93 |
try:
|
94 |
+
os.remove(cached_file_path)
|
95 |
+
logger.info(f"Fichier cache {podcast_to_delete.filename_cache} supprimé.")
|
96 |
+
except OSError as e:
|
97 |
+
logger.error(f"Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}: {e}")
|
98 |
+
flash(f'Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}.', 'error')
|
99 |
+
|
100 |
+
db.session.delete(podcast_to_delete)
|
101 |
+
db.session.commit()
|
102 |
+
flash('Podcast supprimé avec succès.', 'success')
|
103 |
+
except Exception as e:
|
104 |
+
db.session.rollback()
|
105 |
+
logger.error(f"Erreur lors de la suppression du podcast ID {podcast_id}: {e}")
|
106 |
+
flash(f"Erreur lors de la suppression du podcast: {e}", 'error')
|
107 |
+
return redirect(url_for('gestion'))
|
108 |
+
|
109 |
+
@app.route('/play/<int:podcast_id>')
|
110 |
+
def play_podcast_route(podcast_id):
|
111 |
+
podcast = db.session.get(Podcast, podcast_id)
|
112 |
+
|
113 |
+
if not podcast:
|
114 |
+
logger.warning(f"Tentative de lecture d'un podcast non trouvé: ID {podcast_id}")
|
115 |
+
return jsonify({'error': 'Podcast non trouvé'}), 404
|
116 |
+
|
117 |
+
if podcast.filename_cache:
|
118 |
+
cached_filepath = os.path.join(AUDIO_CACHE_DIR, podcast.filename_cache)
|
119 |
+
if os.path.exists(cached_filepath):
|
120 |
+
logger.info(f"Service du podcast {podcast.id} depuis le cache: {podcast.filename_cache}")
|
121 |
+
audio_url = url_for('serve_cached_audio', filename=podcast.filename_cache)
|
122 |
+
return jsonify({'audio_url': audio_url})
|
123 |
else:
|
124 |
+
logger.warning(f"Fichier cache {podcast.filename_cache} pour podcast {podcast.id} non trouvé. Re-téléchargement.")
|
125 |
+
podcast.filename_cache = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
|
127 |
+
final_cached_filepath = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
128 |
try:
|
129 |
+
parsed_url = urlparse(podcast.url)
|
130 |
+
path_parts = os.path.splitext(parsed_url.path)
|
131 |
+
extension = path_parts[1] if path_parts[1] else '.audio'
|
132 |
|
133 |
+
base_filename = str(podcast.id)
|
|
|
|
|
|
|
134 |
|
135 |
+
logger.info(f"Téléchargement de {podcast.url} pour le podcast ID {podcast.id}")
|
136 |
+
response = requests.get(podcast.url, stream=True, timeout=(10, 60))
|
137 |
+
response.raise_for_status()
|
138 |
+
|
139 |
+
content_type = response.headers.get('Content-Type')
|
140 |
+
if content_type:
|
141 |
+
if 'mpeg' in content_type: extension = '.mp3'
|
142 |
+
elif 'ogg' in content_type: extension = '.ogg'
|
143 |
+
elif 'wav' in content_type: extension = '.wav'
|
144 |
+
elif 'aac' in content_type: extension = '.aac'
|
145 |
+
elif 'mp4' in content_type: extension = '.m4a'
|
146 |
+
|
147 |
+
cached_filename_with_ext = f"{base_filename}{extension}"
|
148 |
+
final_cached_filepath = os.path.join(AUDIO_CACHE_DIR, cached_filename_with_ext)
|
149 |
+
|
150 |
+
with open(final_cached_filepath, 'wb') as f:
|
151 |
+
for chunk in response.iter_content(chunk_size=8192):
|
152 |
+
f.write(chunk)
|
153 |
+
logger.info(f"Téléchargement terminé: {final_cached_filepath}")
|
154 |
+
|
155 |
+
podcast.filename_cache = cached_filename_with_ext
|
156 |
+
db.session.commit()
|
157 |
+
|
158 |
+
audio_url = url_for('serve_cached_audio', filename=cached_filename_with_ext)
|
159 |
+
return jsonify({'audio_url': audio_url})
|
160 |
+
|
161 |
+
except requests.exceptions.Timeout:
|
162 |
+
logger.error(f"Timeout lors du téléchargement de {podcast.url}")
|
163 |
+
return jsonify({'error': 'Le téléchargement du podcast a pris trop de temps.'}), 504
|
164 |
+
except requests.exceptions.RequestException as e:
|
165 |
+
logger.error(f"Erreur de téléchargement pour {podcast.url}: {e}")
|
166 |
+
return jsonify({'error': f'Impossible de télécharger le podcast: {e}'}), 500
|
167 |
except Exception as e:
|
168 |
+
db.session.rollback()
|
169 |
+
logger.error(f"Erreur inattendue lors du traitement du podcast {podcast.id}: {e}")
|
170 |
+
return jsonify({'error': f'Erreur inattendue: {e}'}), 500
|
171 |
+
finally:
|
172 |
+
if final_cached_filepath and os.path.exists(final_cached_filepath) and (not podcast or podcast.filename_cache != os.path.basename(final_cached_filepath)):
|
173 |
+
try:
|
174 |
+
os.remove(final_cached_filepath)
|
175 |
+
logger.info(f"Fichier partiel nettoyé : {final_cached_filepath}")
|
176 |
+
except OSError as e_clean:
|
177 |
+
logger.error(f"Erreur lors du nettoyage du fichier partiel {final_cached_filepath}: {e_clean}")
|
178 |
+
|
179 |
+
@app.route('/audio_cache/<path:filename>')
|
180 |
+
def serve_cached_audio(filename):
|
181 |
+
logger.debug(f"Service du fichier cache: {filename} depuis {AUDIO_CACHE_DIR}")
|
182 |
+
return send_from_directory(AUDIO_CACHE_DIR, filename)
|
183 |
+
|
184 |
+
if __name__ == '__main__':
|
185 |
+
with app.app_context():
|
186 |
+
try:
|
187 |
+
db.create_all()
|
188 |
+
logger.info("Tables de base de données vérifiées/créées (si elles n'existaient pas).")
|
189 |
+
except Exception as e:
|
190 |
+
logger.error(f"Erreur lors de la création des tables de la base de données: {e}")
|
191 |
+
logger.error("Assurez-vous que votre serveur PostgreSQL est en cours d'exécution et que la base de données existe.")
|
192 |
+
app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))
|