Docfile commited on
Commit
4e0cb8b
·
verified ·
1 Parent(s): 47c91f3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +53 -32
app.py CHANGED
@@ -11,11 +11,13 @@ logger = logging.getLogger(__name__)
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
  app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
16
 
17
  db = SQLAlchemy(app)
18
 
 
19
  AUDIO_CACHE_DIR = '/tmp/audio_cache'
20
  try:
21
  os.makedirs(AUDIO_CACHE_DIR, exist_ok=True)
@@ -23,26 +25,29 @@ try:
23
  except OSError as e:
24
  logger.error(f"Impossible de créer le répertoire de cache audio à {AUDIO_CACHE_DIR}: {e}")
25
 
 
26
  class Podcast(db.Model):
27
  __tablename__ = 'podcast'
28
  id = db.Column(db.Integer, primary_key=True)
29
  name = db.Column(db.String(200), nullable=False)
30
  url = db.Column(db.String(500), nullable=False, unique=True)
31
  subject = db.Column(db.String(100), nullable=False)
32
- filename_cache = db.Column(db.String(255), nullable=True)
33
 
34
  def __repr__(self):
35
  return f'<Podcast {self.name}>'
36
 
37
- # Moved db.create_all() here to ensure it runs on app initialization
 
 
38
  with app.app_context():
39
  try:
40
  db.create_all()
41
  logger.info("Tables de base de données vérifiées/créées (si elles n'existaient pas).")
42
  except Exception as e:
43
  logger.error(f"Erreur lors de la création des tables de la base de données: {e}")
44
- # Depending on the severity, you might want to exit or re-raise the exception.
45
- # For now, it logs the error, and the app will likely fail on subsequent DB operations if this fails.
46
 
47
  @app.route('/')
48
  def index():
@@ -51,7 +56,7 @@ def index():
51
  except Exception as e:
52
  logger.error(f"Erreur lors de la récupération des podcasts: {e}")
53
  flash("Erreur lors du chargement des podcasts depuis la base de données.", "error")
54
- podcasts = []
55
  return render_template('index.html', podcasts=podcasts)
56
 
57
  @app.route('/gestion', methods=['GET', 'POST'])
@@ -64,6 +69,7 @@ def gestion():
64
  if not name or not url or not subject:
65
  flash('Tous les champs sont requis !', 'error')
66
  else:
 
67
  existing_podcast = Podcast.query.filter_by(url=url).first()
68
  if existing_podcast:
69
  flash('Un podcast avec cette URL existe déjà.', 'warning')
@@ -73,12 +79,13 @@ def gestion():
73
  db.session.add(new_podcast)
74
  db.session.commit()
75
  flash('Podcast ajouté avec succès !', 'success')
76
- return redirect(url_for('gestion'))
77
  except Exception as e:
78
- db.session.rollback()
79
  logger.error(f"Erreur lors de l'ajout du podcast: {e}")
80
  flash(f"Erreur lors de l'ajout du podcast: {e}", 'error')
81
 
 
82
  try:
83
  podcasts = Podcast.query.order_by(Podcast.name).all()
84
  except Exception as e:
@@ -91,11 +98,12 @@ def gestion():
91
  @app.route('/delete_podcast/<int:podcast_id>', methods=['POST'])
92
  def delete_podcast(podcast_id):
93
  try:
94
- podcast_to_delete = db.session.get(Podcast, podcast_id)
95
  if not podcast_to_delete:
96
  flash('Podcast non trouvé.', 'error')
97
  return redirect(url_for('gestion'))
98
 
 
99
  if podcast_to_delete.filename_cache:
100
  cached_file_path = os.path.join(AUDIO_CACHE_DIR, podcast_to_delete.filename_cache)
101
  if os.path.exists(cached_file_path):
@@ -105,6 +113,9 @@ def delete_podcast(podcast_id):
105
  except OSError as e:
106
  logger.error(f"Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}: {e}")
107
  flash(f'Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}.', 'error')
 
 
 
108
 
109
  db.session.delete(podcast_to_delete)
110
  db.session.commit()
@@ -117,12 +128,13 @@ def delete_podcast(podcast_id):
117
 
118
  @app.route('/play/<int:podcast_id>')
119
  def play_podcast_route(podcast_id):
120
- podcast = db.session.get(Podcast, podcast_id)
121
 
122
  if not podcast:
123
  logger.warning(f"Tentative de lecture d'un podcast non trouvé: ID {podcast_id}")
124
  return jsonify({'error': 'Podcast non trouvé'}), 404
125
 
 
126
  if podcast.filename_cache:
127
  cached_filepath = os.path.join(AUDIO_CACHE_DIR, podcast.filename_cache)
128
  if os.path.exists(cached_filepath):
@@ -130,39 +142,47 @@ def play_podcast_route(podcast_id):
130
  audio_url = url_for('serve_cached_audio', filename=podcast.filename_cache)
131
  return jsonify({'audio_url': audio_url})
132
  else:
133
- logger.warning(f"Fichier cache {podcast.filename_cache} pour podcast {podcast.id} non trouvé. Re-téléchargement.")
134
- podcast.filename_cache = None # Clear invalid cache entry
 
 
135
 
136
- final_cached_filepath = None
 
137
  try:
 
138
  parsed_url = urlparse(podcast.url)
139
- path_parts = os.path.splitext(parsed_url.path)
140
- extension = path_parts[1] if path_parts[1] else '.audio' # Default extension
141
 
142
- base_filename = str(podcast.id) # Use podcast ID for a unique, simple base name
 
143
 
144
  logger.info(f"Téléchargement de {podcast.url} pour le podcast ID {podcast.id}")
145
- # Increased timeout for potentially large files; connect timeout, read timeout
146
  response = requests.get(podcast.url, stream=True, timeout=(10, 60))
147
- response.raise_for_status()
148
 
149
- # Try to get a better extension from Content-Type header
150
  content_type = response.headers.get('Content-Type')
151
  if content_type:
152
  if 'mpeg' in content_type: extension = '.mp3'
153
  elif 'ogg' in content_type: extension = '.ogg'
154
  elif 'wav' in content_type: extension = '.wav'
155
  elif 'aac' in content_type: extension = '.aac'
156
- elif 'mp4' in content_type: extension = '.m4a' # Often audio in mp4 container
157
 
 
158
  cached_filename_with_ext = f"{base_filename}{extension}"
159
  final_cached_filepath = os.path.join(AUDIO_CACHE_DIR, cached_filename_with_ext)
160
 
 
161
  with open(final_cached_filepath, 'wb') as f:
162
- for chunk in response.iter_content(chunk_size=8192):
163
  f.write(chunk)
164
  logger.info(f"Téléchargement terminé: {final_cached_filepath}")
165
 
 
166
  podcast.filename_cache = cached_filename_with_ext
167
  db.session.commit()
168
 
@@ -171,32 +191,33 @@ def play_podcast_route(podcast_id):
171
 
172
  except requests.exceptions.Timeout:
173
  logger.error(f"Timeout lors du téléchargement de {podcast.url}")
 
 
 
 
174
  return jsonify({'error': 'Le téléchargement du podcast a pris trop de temps.'}), 504
175
  except requests.exceptions.RequestException as e:
176
  logger.error(f"Erreur de téléchargement pour {podcast.url}: {e}")
177
- # Clean up partial download if it exists and wasn't successfully associated
178
- if final_cached_filepath and os.path.exists(final_cached_filepath) and (not podcast or podcast.filename_cache != os.path.basename(final_cached_filepath)):
179
- try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (RequestException) nettoyé : {final_cached_filepath}")
180
- except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (RequestException) {final_cached_filepath}: {e_clean}")
181
  return jsonify({'error': f'Impossible de télécharger le podcast: {e}'}), 500
182
  except Exception as e:
183
- db.session.rollback() # Rollback DB session on unexpected error
184
  logger.error(f"Erreur inattendue lors du traitement du podcast {podcast.id}: {e}")
185
- # Clean up partial download similar to above
186
- if final_cached_filepath and os.path.exists(final_cached_filepath) and (not podcast or podcast.filename_cache != os.path.basename(final_cached_filepath)):
187
  try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (Exception) nettoyé : {final_cached_filepath}")
188
  except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (Exception) {final_cached_filepath}: {e_clean}")
189
  return jsonify({'error': f'Erreur inattendue: {e}'}), 500
190
- # The finally block for cleanup was a bit complex; simplified by handling cleanup in error cases.
191
- # If successful, filename_cache is set, so it won't be cleaned.
192
 
193
  @app.route('/audio_cache/<path:filename>')
194
  def serve_cached_audio(filename):
195
  logger.debug(f"Service du fichier cache: {filename} depuis {AUDIO_CACHE_DIR}")
 
 
196
  return send_from_directory(AUDIO_CACHE_DIR, filename)
197
 
198
  if __name__ == '__main__':
199
- # The db.create_all() call is now above, so it's not needed here.
200
- # The line `app.run(...):19:12 =====` from the problem description seems to have a typo.
201
- # Corrected to a standard app.run() call.
202
  app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))
 
11
  app = Flask(__name__)
12
  app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'une_cle_secrete_par_defaut_pour_dev')
13
 
14
+ # Configuration de la base de données PostgreSQL
15
  app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://Podcast_owner:npg_gFdMDLO9lVa0@ep-delicate-surf-a4v7wopn-pooler.us-east-1.aws.neon.tech/Podcast?sslmode=require'
16
  app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
17
 
18
  db = SQLAlchemy(app)
19
 
20
+ # Configuration du répertoire de cache audio
21
  AUDIO_CACHE_DIR = '/tmp/audio_cache'
22
  try:
23
  os.makedirs(AUDIO_CACHE_DIR, exist_ok=True)
 
25
  except OSError as e:
26
  logger.error(f"Impossible de créer le répertoire de cache audio à {AUDIO_CACHE_DIR}: {e}")
27
 
28
+ # Modèle de base de données pour les Podcasts
29
  class Podcast(db.Model):
30
  __tablename__ = 'podcast'
31
  id = db.Column(db.Integer, primary_key=True)
32
  name = db.Column(db.String(200), nullable=False)
33
  url = db.Column(db.String(500), nullable=False, unique=True)
34
  subject = db.Column(db.String(100), nullable=False)
35
+ filename_cache = db.Column(db.String(255), nullable=True) # Nom du fichier dans le cache
36
 
37
  def __repr__(self):
38
  return f'<Podcast {self.name}>'
39
 
40
+ # Création des tables de la base de données si elles n'existent pas
41
+ # Ceci est déplacé ici pour s'assurer qu'il s'exécute au démarrage de l'application,
42
+ # que ce soit via `flask run` ou un serveur WSGI.
43
  with app.app_context():
44
  try:
45
  db.create_all()
46
  logger.info("Tables de base de données vérifiées/créées (si elles n'existaient pas).")
47
  except Exception as e:
48
  logger.error(f"Erreur lors de la création des tables de la base de données: {e}")
49
+ # Il est crucial de gérer cette erreur. Si la base de données n'est pas accessible ou
50
+ # si les tables ne peuvent pas être créées, l'application ne fonctionnera pas correctement.
51
 
52
  @app.route('/')
53
  def index():
 
56
  except Exception as e:
57
  logger.error(f"Erreur lors de la récupération des podcasts: {e}")
58
  flash("Erreur lors du chargement des podcasts depuis la base de données.", "error")
59
+ podcasts = [] # Fournir une liste vide en cas d'erreur pour que le template fonctionne
60
  return render_template('index.html', podcasts=podcasts)
61
 
62
  @app.route('/gestion', methods=['GET', 'POST'])
 
69
  if not name or not url or not subject:
70
  flash('Tous les champs sont requis !', 'error')
71
  else:
72
+ # Vérifier si un podcast avec la même URL existe déjà
73
  existing_podcast = Podcast.query.filter_by(url=url).first()
74
  if existing_podcast:
75
  flash('Un podcast avec cette URL existe déjà.', 'warning')
 
79
  db.session.add(new_podcast)
80
  db.session.commit()
81
  flash('Podcast ajouté avec succès !', 'success')
82
+ return redirect(url_for('gestion')) # Rediriger pour éviter la resoumission du formulaire
83
  except Exception as e:
84
+ db.session.rollback() # Annuler les changements en cas d'erreur
85
  logger.error(f"Erreur lors de l'ajout du podcast: {e}")
86
  flash(f"Erreur lors de l'ajout du podcast: {e}", 'error')
87
 
88
+ # Charger les podcasts pour l'affichage sur la page de gestion (méthode GET ou après POST)
89
  try:
90
  podcasts = Podcast.query.order_by(Podcast.name).all()
91
  except Exception as e:
 
98
  @app.route('/delete_podcast/<int:podcast_id>', methods=['POST'])
99
  def delete_podcast(podcast_id):
100
  try:
101
+ podcast_to_delete = db.session.get(Podcast, podcast_id) # Utilisation de db.session.get pour Flask-SQLAlchemy >= 3.0
102
  if not podcast_to_delete:
103
  flash('Podcast non trouvé.', 'error')
104
  return redirect(url_for('gestion'))
105
 
106
+ # Supprimer le fichier cache associé s'il existe
107
  if podcast_to_delete.filename_cache:
108
  cached_file_path = os.path.join(AUDIO_CACHE_DIR, podcast_to_delete.filename_cache)
109
  if os.path.exists(cached_file_path):
 
113
  except OSError as e:
114
  logger.error(f"Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}: {e}")
115
  flash(f'Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}.', 'error')
116
+ else:
117
+ logger.warning(f"Fichier cache {podcast_to_delete.filename_cache} listé dans la DB mais non trouvé sur le disque pour suppression.")
118
+
119
 
120
  db.session.delete(podcast_to_delete)
121
  db.session.commit()
 
128
 
129
  @app.route('/play/<int:podcast_id>')
130
  def play_podcast_route(podcast_id):
131
+ podcast = db.session.get(Podcast, podcast_id) # Utilisation de db.session.get
132
 
133
  if not podcast:
134
  logger.warning(f"Tentative de lecture d'un podcast non trouvé: ID {podcast_id}")
135
  return jsonify({'error': 'Podcast non trouvé'}), 404
136
 
137
+ # Vérifier si le fichier est déjà en cache
138
  if podcast.filename_cache:
139
  cached_filepath = os.path.join(AUDIO_CACHE_DIR, podcast.filename_cache)
140
  if os.path.exists(cached_filepath):
 
142
  audio_url = url_for('serve_cached_audio', filename=podcast.filename_cache)
143
  return jsonify({'audio_url': audio_url})
144
  else:
145
+ # Le fichier cache est référencé mais n'existe pas, il faut le re-télécharger
146
+ logger.warning(f"Fichier cache {podcast.filename_cache} pour podcast {podcast.id} non trouvé sur le disque. Re-téléchargement.")
147
+ podcast.filename_cache = None # Marquer comme non-caché pour forcer le re-téléchargement
148
+ # Pas besoin de db.session.commit() ici immédiatement, sera fait après le téléchargement réussi
149
 
150
+ # Si le fichier n'est pas en cache ou si le cache était invalide
151
+ final_cached_filepath = None # Initialiser pour la clause finally
152
  try:
153
+ # Déterminer l'extension à partir de l'URL ou du Content-Type
154
  parsed_url = urlparse(podcast.url)
155
+ _, url_ext = os.path.splitext(parsed_url.path)
156
+ extension = url_ext if url_ext else '.audio' # Extension par défaut
157
 
158
+ # Utiliser l'ID du podcast pour un nom de fichier unique et simple
159
+ base_filename = str(podcast.id)
160
 
161
  logger.info(f"Téléchargement de {podcast.url} pour le podcast ID {podcast.id}")
162
+ # Timeout: (connect_timeout, read_timeout)
163
  response = requests.get(podcast.url, stream=True, timeout=(10, 60))
164
+ response.raise_for_status() # Lèvera une exception pour les codes d'erreur HTTP (4xx ou 5xx)
165
 
166
+ # Essayer d'obtenir une meilleure extension à partir de l'en-tête Content-Type
167
  content_type = response.headers.get('Content-Type')
168
  if content_type:
169
  if 'mpeg' in content_type: extension = '.mp3'
170
  elif 'ogg' in content_type: extension = '.ogg'
171
  elif 'wav' in content_type: extension = '.wav'
172
  elif 'aac' in content_type: extension = '.aac'
173
+ elif 'mp4' in content_type: extension = '.m4a' # Souvent utilisé pour l'audio dans un conteneur mp4
174
 
175
+ # Construire le nom de fichier final avec l'extension déterminée
176
  cached_filename_with_ext = f"{base_filename}{extension}"
177
  final_cached_filepath = os.path.join(AUDIO_CACHE_DIR, cached_filename_with_ext)
178
 
179
+ # Écrire le contenu dans le fichier cache
180
  with open(final_cached_filepath, 'wb') as f:
181
+ for chunk in response.iter_content(chunk_size=8192): # Taille de chunk raisonnable
182
  f.write(chunk)
183
  logger.info(f"Téléchargement terminé: {final_cached_filepath}")
184
 
185
+ # Mettre à jour la base de données avec le nom du fichier en cache
186
  podcast.filename_cache = cached_filename_with_ext
187
  db.session.commit()
188
 
 
191
 
192
  except requests.exceptions.Timeout:
193
  logger.error(f"Timeout lors du téléchargement de {podcast.url}")
194
+ # Nettoyer le fichier partiel si le téléchargement a échoué
195
+ if final_cached_filepath and os.path.exists(final_cached_filepath):
196
+ try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (Timeout) nettoyé : {final_cached_filepath}")
197
+ except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (Timeout) {final_cached_filepath}: {e_clean}")
198
  return jsonify({'error': 'Le téléchargement du podcast a pris trop de temps.'}), 504
199
  except requests.exceptions.RequestException as e:
200
  logger.error(f"Erreur de téléchargement pour {podcast.url}: {e}")
201
+ if final_cached_filepath and os.path.exists(final_cached_filepath):
202
+ try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (RequestException) nettoyé : {final_cached_filepath}")
203
+ except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (RequestException) {final_cached_filepath}: {e_clean}")
 
204
  return jsonify({'error': f'Impossible de télécharger le podcast: {e}'}), 500
205
  except Exception as e:
206
+ db.session.rollback() # Assurer la cohérence de la session DB
207
  logger.error(f"Erreur inattendue lors du traitement du podcast {podcast.id}: {e}")
208
+ if final_cached_filepath and os.path.exists(final_cached_filepath):
 
209
  try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (Exception) nettoyé : {final_cached_filepath}")
210
  except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (Exception) {final_cached_filepath}: {e_clean}")
211
  return jsonify({'error': f'Erreur inattendue: {e}'}), 500
 
 
212
 
213
  @app.route('/audio_cache/<path:filename>')
214
  def serve_cached_audio(filename):
215
  logger.debug(f"Service du fichier cache: {filename} depuis {AUDIO_CACHE_DIR}")
216
+ # Assurez-vous que le chemin est sécurisé et ne permet pas de sortir du répertoire de cache
217
+ # send_from_directory s'en charge généralement.
218
  return send_from_directory(AUDIO_CACHE_DIR, filename)
219
 
220
  if __name__ == '__main__':
221
+ # db.create_all() est maintenant appelé au niveau global du module.
222
+ # La ligne `app.run(...):19:12 =====` dans le log original contenait une syntaxe incorrecte, corrigée ici.
 
223
  app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))