|
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') |
|
|
|
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) |
|
|
|
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}") |
|
|
|
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) |
|
|
|
def __repr__(self): |
|
return f'<Podcast {self.name}>' |
|
|
|
@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 = [] |
|
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: |
|
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')) |
|
except Exception as e: |
|
db.session.rollback() |
|
logger.error(f"Erreur lors de l'ajout du podcast: {e}") |
|
flash(f"Erreur lors de l'ajout du podcast: {e}", 'error') |
|
|
|
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) |
|
if not podcast_to_delete: |
|
flash('Podcast non trouvé.', 'error') |
|
return redirect(url_for('gestion')) |
|
|
|
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') |
|
|
|
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) |
|
|
|
if not podcast: |
|
logger.warning(f"Tentative de lecture d'un podcast non trouvé: ID {podcast_id}") |
|
return jsonify({'error': 'Podcast non trouvé'}), 404 |
|
|
|
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: |
|
logger.warning(f"Fichier cache {podcast.filename_cache} pour podcast {podcast.id} non trouvé. Re-téléchargement.") |
|
podcast.filename_cache = None |
|
|
|
final_cached_filepath = None |
|
try: |
|
parsed_url = urlparse(podcast.url) |
|
path_parts = os.path.splitext(parsed_url.path) |
|
extension = path_parts[1] if path_parts[1] else '.audio' |
|
|
|
base_filename = str(podcast.id) |
|
|
|
logger.info(f"Téléchargement de {podcast.url} pour le podcast ID {podcast.id}") |
|
response = requests.get(podcast.url, stream=True, timeout=(10, 60)) |
|
response.raise_for_status() |
|
|
|
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' |
|
|
|
cached_filename_with_ext = f"{base_filename}{extension}" |
|
final_cached_filepath = os.path.join(AUDIO_CACHE_DIR, cached_filename_with_ext) |
|
|
|
with open(final_cached_filepath, 'wb') as f: |
|
for chunk in response.iter_content(chunk_size=8192): |
|
f.write(chunk) |
|
logger.info(f"Téléchargement terminé: {final_cached_filepath}") |
|
|
|
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}") |
|
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}") |
|
return jsonify({'error': f'Impossible de télécharger le podcast: {e}'}), 500 |
|
except Exception as e: |
|
db.session.rollback() |
|
logger.error(f"Erreur inattendue lors du traitement du podcast {podcast.id}: {e}") |
|
return jsonify({'error': f'Erreur inattendue: {e}'}), 500 |
|
finally: |
|
if final_cached_filepath and os.path.exists(final_cached_filepath) and (not podcast or podcast.filename_cache != os.path.basename(final_cached_filepath)): |
|
try: |
|
os.remove(final_cached_filepath) |
|
logger.info(f"Fichier partiel nettoyé : {final_cached_filepath}") |
|
except OSError as e_clean: |
|
logger.error(f"Erreur lors du nettoyage du fichier partiel {final_cached_filepath}: {e_clean}") |
|
|
|
@app.route('/audio_cache/<path:filename>') |
|
def serve_cached_audio(filename): |
|
logger.debug(f"Service du fichier cache: {filename} depuis {AUDIO_CACHE_DIR}") |
|
return send_from_directory(AUDIO_CACHE_DIR, filename) |
|
|
|
if __name__ == '__main__': |
|
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}") |
|
logger.error("Assurez-vous que votre serveur PostgreSQL est en cours d'exécution et que la base de données existe.") |
|
app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 5000))) |