File size: 10,090 Bytes
fc53ab2
 
43f84cb
fc53ab2
 
 
9f15b41
fc53ab2
 
43f84cb
fc53ab2
 
43f84cb
fc53ab2
 
43f84cb
fc53ab2
43f84cb
fc53ab2
 
 
 
 
 
43f84cb
fc53ab2
 
 
 
 
 
 
43f84cb
fc53ab2
 
43f84cb
47c91f3
 
 
 
 
 
 
 
 
 
fc53ab2
 
43f84cb
fc53ab2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43f84cb
fc53ab2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43f84cb
fc53ab2
 
 
43f84cb
fc53ab2
43f84cb
fc53ab2
 
 
 
 
 
 
 
 
 
 
43f84cb
fc53ab2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43f84cb
fc53ab2
47c91f3
43f84cb
fc53ab2
43f84cb
fc53ab2
 
47c91f3
43f84cb
47c91f3
43f84cb
fc53ab2
47c91f3
 
fc53ab2
 
47c91f3
fc53ab2
 
 
 
 
 
47c91f3
fc53ab2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47c91f3
 
 
 
fc53ab2
43f84cb
47c91f3
fc53ab2
47c91f3
fc53ab2
47c91f3
 
 
 
 
fc53ab2
 
 
 
 
 
 
47c91f3
 
 
fc53ab2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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}>'

# Moved db.create_all() here to ensure it runs on app initialization
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}")
        # Depending on the severity, you might want to exit or re-raise the exception.
        # For now, it logs the error, and the app will likely fail on subsequent DB operations if this fails.

@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 # Clear invalid cache entry

    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' # Default extension
        
        base_filename = str(podcast.id) # Use podcast ID for a unique, simple base name
        
        logger.info(f"Téléchargement de {podcast.url} pour le podcast ID {podcast.id}")
        # Increased timeout for potentially large files; connect timeout, read timeout
        response = requests.get(podcast.url, stream=True, timeout=(10, 60)) 
        response.raise_for_status()

        # Try to get a better extension from Content-Type header
        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' # Often audio in mp4 container

        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}")
        # Clean up partial download if it exists and wasn't successfully associated
        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 (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() # Rollback DB session on unexpected error
        logger.error(f"Erreur inattendue lors du traitement du podcast {podcast.id}: {e}")
        # Clean up partial download similar to above
        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 (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
    # The finally block for cleanup was a bit complex; simplified by handling cleanup in error cases.
    # If successful, filename_cache is set, so it won't be cleaned.

@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__':
    # The db.create_all() call is now above, so it's not needed here.
    # The line `app.run(...):19:12 =====` from the problem description seems to have a typo.
    # Corrected to a standard app.run() call.
    app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))