Testpdf / app.py
Docfile's picture
Update app.py
4e0cb8b verified
raw
history blame
11.9 kB
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory
from flask_sqlalchemy import SQLAlchemy
import os
import requests
from urllib.parse import urlparse
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'une_cle_secrete_par_defaut_pour_dev')
# Configuration de la base de données PostgreSQL
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://Podcast_owner:npg_gFdMDLO9lVa0@ep-delicate-surf-a4v7wopn-pooler.us-east-1.aws.neon.tech/Podcast?sslmode=require'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# Configuration du répertoire de cache audio
AUDIO_CACHE_DIR = '/tmp/audio_cache'
try:
os.makedirs(AUDIO_CACHE_DIR, exist_ok=True)
logger.info(f"Répertoire de cache audio configuré sur : {AUDIO_CACHE_DIR}")
except OSError as e:
logger.error(f"Impossible de créer le répertoire de cache audio à {AUDIO_CACHE_DIR}: {e}")
# Modèle de base de données pour les Podcasts
class Podcast(db.Model):
__tablename__ = 'podcast'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
url = db.Column(db.String(500), nullable=False, unique=True)
subject = db.Column(db.String(100), nullable=False)
filename_cache = db.Column(db.String(255), nullable=True) # Nom du fichier dans le cache
def __repr__(self):
return f'<Podcast {self.name}>'
# Création des tables de la base de données si elles n'existent pas
# Ceci est déplacé ici pour s'assurer qu'il s'exécute au démarrage de l'application,
# que ce soit via `flask run` ou un serveur WSGI.
with app.app_context():
try:
db.create_all()
logger.info("Tables de base de données vérifiées/créées (si elles n'existaient pas).")
except Exception as e:
logger.error(f"Erreur lors de la création des tables de la base de données: {e}")
# Il est crucial de gérer cette erreur. Si la base de données n'est pas accessible ou
# si les tables ne peuvent pas être créées, l'application ne fonctionnera pas correctement.
@app.route('/')
def index():
try:
podcasts = Podcast.query.order_by(Podcast.name).all()
except Exception as e:
logger.error(f"Erreur lors de la récupération des podcasts: {e}")
flash("Erreur lors du chargement des podcasts depuis la base de données.", "error")
podcasts = [] # Fournir une liste vide en cas d'erreur pour que le template fonctionne
return render_template('index.html', podcasts=podcasts)
@app.route('/gestion', methods=['GET', 'POST'])
def gestion():
if request.method == 'POST':
name = request.form.get('name')
url = request.form.get('url')
subject = request.form.get('subject')
if not name or not url or not subject:
flash('Tous les champs sont requis !', 'error')
else:
# Vérifier si un podcast avec la même URL existe déjà
existing_podcast = Podcast.query.filter_by(url=url).first()
if existing_podcast:
flash('Un podcast avec cette URL existe déjà.', 'warning')
else:
try:
new_podcast = Podcast(name=name, url=url, subject=subject)
db.session.add(new_podcast)
db.session.commit()
flash('Podcast ajouté avec succès !', 'success')
return redirect(url_for('gestion')) # Rediriger pour éviter la resoumission du formulaire
except Exception as e:
db.session.rollback() # Annuler les changements en cas d'erreur
logger.error(f"Erreur lors de l'ajout du podcast: {e}")
flash(f"Erreur lors de l'ajout du podcast: {e}", 'error')
# Charger les podcasts pour l'affichage sur la page de gestion (méthode GET ou après POST)
try:
podcasts = Podcast.query.order_by(Podcast.name).all()
except Exception as e:
logger.error(f"Erreur lors de la récupération des podcasts pour la gestion: {e}")
flash("Erreur lors du chargement des podcasts pour la gestion.", "error")
podcasts = []
return render_template('gestion.html', podcasts=podcasts)
@app.route('/delete_podcast/<int:podcast_id>', methods=['POST'])
def delete_podcast(podcast_id):
try:
podcast_to_delete = db.session.get(Podcast, podcast_id) # Utilisation de db.session.get pour Flask-SQLAlchemy >= 3.0
if not podcast_to_delete:
flash('Podcast non trouvé.', 'error')
return redirect(url_for('gestion'))
# Supprimer le fichier cache associé s'il existe
if podcast_to_delete.filename_cache:
cached_file_path = os.path.join(AUDIO_CACHE_DIR, podcast_to_delete.filename_cache)
if os.path.exists(cached_file_path):
try:
os.remove(cached_file_path)
logger.info(f"Fichier cache {podcast_to_delete.filename_cache} supprimé.")
except OSError as e:
logger.error(f"Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}: {e}")
flash(f'Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}.', 'error')
else:
logger.warning(f"Fichier cache {podcast_to_delete.filename_cache} listé dans la DB mais non trouvé sur le disque pour suppression.")
db.session.delete(podcast_to_delete)
db.session.commit()
flash('Podcast supprimé avec succès.', 'success')
except Exception as e:
db.session.rollback()
logger.error(f"Erreur lors de la suppression du podcast ID {podcast_id}: {e}")
flash(f"Erreur lors de la suppression du podcast: {e}", 'error')
return redirect(url_for('gestion'))
@app.route('/play/<int:podcast_id>')
def play_podcast_route(podcast_id):
podcast = db.session.get(Podcast, podcast_id) # Utilisation de db.session.get
if not podcast:
logger.warning(f"Tentative de lecture d'un podcast non trouvé: ID {podcast_id}")
return jsonify({'error': 'Podcast non trouvé'}), 404
# Vérifier si le fichier est déjà en cache
if podcast.filename_cache:
cached_filepath = os.path.join(AUDIO_CACHE_DIR, podcast.filename_cache)
if os.path.exists(cached_filepath):
logger.info(f"Service du podcast {podcast.id} depuis le cache: {podcast.filename_cache}")
audio_url = url_for('serve_cached_audio', filename=podcast.filename_cache)
return jsonify({'audio_url': audio_url})
else:
# Le fichier cache est référencé mais n'existe pas, il faut le re-télécharger
logger.warning(f"Fichier cache {podcast.filename_cache} pour podcast {podcast.id} non trouvé sur le disque. Re-téléchargement.")
podcast.filename_cache = None # Marquer comme non-caché pour forcer le re-téléchargement
# Pas besoin de db.session.commit() ici immédiatement, sera fait après le téléchargement réussi
# Si le fichier n'est pas en cache ou si le cache était invalide
final_cached_filepath = None # Initialiser pour la clause finally
try:
# Déterminer l'extension à partir de l'URL ou du Content-Type
parsed_url = urlparse(podcast.url)
_, url_ext = os.path.splitext(parsed_url.path)
extension = url_ext if url_ext else '.audio' # Extension par défaut
# Utiliser l'ID du podcast pour un nom de fichier unique et simple
base_filename = str(podcast.id)
logger.info(f"Téléchargement de {podcast.url} pour le podcast ID {podcast.id}")
# Timeout: (connect_timeout, read_timeout)
response = requests.get(podcast.url, stream=True, timeout=(10, 60))
response.raise_for_status() # Lèvera une exception pour les codes d'erreur HTTP (4xx ou 5xx)
# Essayer d'obtenir une meilleure extension à partir de l'en-tête Content-Type
content_type = response.headers.get('Content-Type')
if content_type:
if 'mpeg' in content_type: extension = '.mp3'
elif 'ogg' in content_type: extension = '.ogg'
elif 'wav' in content_type: extension = '.wav'
elif 'aac' in content_type: extension = '.aac'
elif 'mp4' in content_type: extension = '.m4a' # Souvent utilisé pour l'audio dans un conteneur mp4
# Construire le nom de fichier final avec l'extension déterminée
cached_filename_with_ext = f"{base_filename}{extension}"
final_cached_filepath = os.path.join(AUDIO_CACHE_DIR, cached_filename_with_ext)
# Écrire le contenu dans le fichier cache
with open(final_cached_filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192): # Taille de chunk raisonnable
f.write(chunk)
logger.info(f"Téléchargement terminé: {final_cached_filepath}")
# Mettre à jour la base de données avec le nom du fichier en cache
podcast.filename_cache = cached_filename_with_ext
db.session.commit()
audio_url = url_for('serve_cached_audio', filename=cached_filename_with_ext)
return jsonify({'audio_url': audio_url})
except requests.exceptions.Timeout:
logger.error(f"Timeout lors du téléchargement de {podcast.url}")
# Nettoyer le fichier partiel si le téléchargement a échoué
if final_cached_filepath and os.path.exists(final_cached_filepath):
try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (Timeout) nettoyé : {final_cached_filepath}")
except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (Timeout) {final_cached_filepath}: {e_clean}")
return jsonify({'error': 'Le téléchargement du podcast a pris trop de temps.'}), 504
except requests.exceptions.RequestException as e:
logger.error(f"Erreur de téléchargement pour {podcast.url}: {e}")
if final_cached_filepath and os.path.exists(final_cached_filepath):
try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (RequestException) nettoyé : {final_cached_filepath}")
except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (RequestException) {final_cached_filepath}: {e_clean}")
return jsonify({'error': f'Impossible de télécharger le podcast: {e}'}), 500
except Exception as e:
db.session.rollback() # Assurer la cohérence de la session DB
logger.error(f"Erreur inattendue lors du traitement du podcast {podcast.id}: {e}")
if final_cached_filepath and os.path.exists(final_cached_filepath):
try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (Exception) nettoyé : {final_cached_filepath}")
except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (Exception) {final_cached_filepath}: {e_clean}")
return jsonify({'error': f'Erreur inattendue: {e}'}), 500
@app.route('/audio_cache/<path:filename>')
def serve_cached_audio(filename):
logger.debug(f"Service du fichier cache: {filename} depuis {AUDIO_CACHE_DIR}")
# Assurez-vous que le chemin est sécurisé et ne permet pas de sortir du répertoire de cache
# send_from_directory s'en charge généralement.
return send_from_directory(AUDIO_CACHE_DIR, filename)
if __name__ == '__main__':
# db.create_all() est maintenant appelé au niveau global du module.
# La ligne `app.run(...):19:12 =====` dans le log original contenait une syntaxe incorrecte, corrigée ici.
app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))