From 7050c6fe563d0aedc0cc08ee1eba6f8e7e0400ad Mon Sep 17 00:00:00 2001 From: Raynoxis Date: Sun, 16 Nov 2025 23:33:07 +0100 Subject: [PATCH] initial commit --- .dockerignore | 15 ++ .gitignore | 38 ++++ Dockerfile | 32 +++ LICENSE | 21 ++ README.md | 121 ++++++++++ app.py | 234 +++++++++++++++++++ docker-compose.yml | 20 ++ docs/INSTALLATION.md | 81 +++++++ docs/USAGE.md | 124 ++++++++++ templates/index.html | 522 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1208 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.py create mode 100644 docker-compose.yml create mode 100644 docs/INSTALLATION.md create mode 100644 docs/USAGE.md create mode 100644 templates/index.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a6c397b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.gitignore +*.md +downloads/ +__pycache__/ +*.pyc +.vscode/ +.idea/ +.env +docker-compose.yml +LICENSE +docs/ +*.mp4 +*.mkv +*.webm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99982f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +.venv +*.egg-info/ +dist/ +build/ + +# Downloads +downloads/ +*.mp4 +*.mkv +*.webm +*.m4a + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Environment +.env +.env.local diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..716523c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.11-slim + +# Installation des dépendances système +RUN apt-get update && apt-get install -y \ + ffmpeg \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Installation de yt-dlp +RUN wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp \ + && chmod +x /usr/local/bin/yt-dlp + +# Installation de Flask +RUN pip install --no-cache-dir flask flask-cors + +# Création des répertoires +WORKDIR /app +RUN mkdir -p /app/downloads /app/templates + +# Copie des fichiers de l'application +COPY app.py /app/ +COPY templates/index.html /app/templates/ + +# Exposition du port +EXPOSE 5000 + +# Variables d'environnement +ENV FLASK_APP=app.py +ENV PYTHONUNBUFFERED=1 + +# Commande de démarrage +CMD ["python", "-u", "app.py"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a6dc91f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 [Votre Nom] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d1f514 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# 🎬 yt-dlp Web Interface + +Interface web moderne pour télécharger des vidéos YouTube avec [yt-dlp](https://github.com/yt-dlp/yt-dlp), conteneurisée avec Docker/Podman. + +![License](https://img.shields.io/badge/license-MIT-blue.svg) +![Docker](https://img.shields.io/badge/docker-ready-blue.svg) +![Python](https://img.shields.io/badge/python-3.11-blue.svg) + +## ✨ Fonctionnalités + +- 🔍 **Analyse complète** des formats vidéo et audio disponibles +- 🎯 **Sélection précise** des formats (qualité, codec, bitrate) +- ⚙️ **Options de sortie** personnalisables (MP4, MKV, WebM) +- 🎵 **Transcodage audio** (AAC, MP3, Opus) avec contrôle du bitrate +- 📦 **Interface moderne** et responsive +- 🐳 **Conteneurisé** pour un déploiement facile +- 🧹 **Nettoyage automatique** des fichiers entre les téléchargements +- 📋 **Affichage de la commande** exécutée pour transparence + +## 🚀 Démarrage rapide + +### Avec Docker +```bash +docker pull votreusername/ytdlp-web:latest +docker run -d -p 5000:5000 --name ytdlp-web votreusername/ytdlp-web:latest +``` + +### Avec Podman +```bash +podman pull votreusername/ytdlp-web:latest +podman run -d -p 5000:5000 --name ytdlp-web votreusername/ytdlp-web:latest +``` + +### Avec Docker Compose +```bash +git clone https://github.com/votreusername/yt-dlp-web.git +cd yt-dlp-web +docker-compose up -d +``` + +Accédez à l'interface : **http://localhost:5000** + +## 📖 Documentation + +- [Installation détaillée](docs/INSTALLATION.md) +- [Guide d'utilisation](docs/USAGE.md) + +## 🛠️ Build depuis les sources +```bash +# Cloner le repo +git clone https://github.com/votreusername/yt-dlp-web.git +cd yt-dlp-web + +# Build avec Docker +docker build -t ytdlp-web . + +# Ou avec Podman +podman build -t ytdlp-web . + +# Lancer +docker run -d -p 5000:5000 --name ytdlp-web ytdlp-web +``` + +## 🎯 Utilisation + +1. Collez l'URL d'une vidéo YouTube +2. Cliquez sur "Analyser la vidéo" +3. Sélectionnez les formats vidéo et audio souhaités +4. Choisissez les options de sortie (conteneur, codec audio, bitrate) +5. Cliquez sur "Télécharger" +6. Téléchargez le fichier généré + +## 📸 Screenshots + +![Interface principale](docs/screenshots/main.png) +![Sélection des formats](docs/screenshots/formats.png) + +## 🔧 Configuration avancée + +### Volumes persistants +```bash +docker run -d \ + -p 5000:5000 \ + -v ./downloads:/app/downloads \ + --name ytdlp-web \ + votreusername/ytdlp-web:latest +``` + +### Variables d'environnement +```bash +docker run -d \ + -p 5000:5000 \ + -e FLASK_ENV=production \ + --name ytdlp-web \ + votreusername/ytdlp-web:latest +``` + +## 🤝 Contribution + +Les contributions sont les bienvenues ! N'hésitez pas à : + +1. Fork le projet +2. Créer une branche (`git checkout -b feature/amelioration`) +3. Commit vos changements (`git commit -am 'Ajout nouvelle fonctionnalité'`) +4. Push vers la branche (`git push origin feature/amelioration`) +5. Ouvrir une Pull Request + +## 📝 License + +Ce projet est sous licence MIT. Voir le fichier [LICENSE](LICENSE) pour plus de détails. + +## 🙏 Remerciements + +- [yt-dlp](https://github.com/yt-dlp/yt-dlp) - Le meilleur outil de téléchargement vidéo +- [Flask](https://flask.palletsprojects.com/) - Framework web Python +- [FFmpeg](https://ffmpeg.org/) - Traitement vidéo et audio + +## ⚠️ Avertissement + +Cet outil est destiné à un usage personnel et éducatif. Respectez les conditions d'utilisation de YouTube et les lois sur le droit d'auteur de votre pays. + diff --git a/app.py b/app.py new file mode 100644 index 0000000..635e283 --- /dev/null +++ b/app.py @@ -0,0 +1,234 @@ +from flask import Flask, render_template, request, jsonify, send_file +import subprocess +import json +import os +import re +from pathlib import Path + +app = Flask(__name__) +DOWNLOAD_DIR = Path('/app/downloads') +DOWNLOAD_DIR.mkdir(exist_ok=True) + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/api/analyze', methods=['POST']) +def analyze_video(): + """Analyse une URL YouTube et retourne les formats disponibles""" + try: + # Nettoyage des anciens fichiers lors d'une nouvelle analyse + for file in DOWNLOAD_DIR.glob('*'): + if file.is_file(): + try: + file.unlink() + except: + pass + + data = request.get_json() + url = data.get('url', '').strip() + + if not url: + return jsonify({'error': 'URL manquante'}), 400 + + # Commande yt-dlp pour lister les formats + cmd = ['yt-dlp', '-J', url] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + + if result.returncode != 0: + return jsonify({'error': f'Erreur yt-dlp: {result.stderr}'}), 400 + + video_info = json.loads(result.stdout) + + # Extraction des informations + title = video_info.get('title', 'Sans titre') + duration = video_info.get('duration', 0) + thumbnail = video_info.get('thumbnail', '') + + # Traitement des formats + formats = video_info.get('formats', []) + video_formats = [] + audio_formats = [] + + for fmt in formats: + format_id = fmt.get('format_id', '') + ext = fmt.get('ext', '') + resolution = fmt.get('resolution', 'audio only') + filesize = fmt.get('filesize', 0) or fmt.get('filesize_approx', 0) + vcodec = fmt.get('vcodec', 'none') + acodec = fmt.get('acodec', 'none') + fps = fmt.get('fps', 0) + tbr = fmt.get('tbr', 0) + + # Formats vidéo (avec vidéo) + if vcodec != 'none' and resolution != 'audio only': + size_mb = f"{filesize / (1024*1024):.1f} MB" if filesize else "N/A" + video_formats.append({ + 'id': format_id, + 'label': f"{resolution} - {ext} - {vcodec} - {fps}fps - {size_mb}", + 'ext': ext, + 'resolution': resolution, + 'vcodec': vcodec, + 'filesize': filesize + }) + + # Formats audio (sans vidéo) + if acodec != 'none' and vcodec == 'none': + abr = fmt.get('abr', 0) + size_mb = f"{filesize / (1024*1024):.1f} MB" if filesize else "N/A" + audio_formats.append({ + 'id': format_id, + 'label': f"{acodec} - {abr}kbps - {ext} - {size_mb}", + 'ext': ext, + 'acodec': acodec, + 'abr': abr, + 'filesize': filesize + }) + + # Tri par qualité + video_formats.sort(key=lambda x: x.get('filesize', 0), reverse=True) + audio_formats.sort(key=lambda x: x.get('filesize', 0), reverse=True) + + return jsonify({ + 'title': title, + 'duration': duration, + 'thumbnail': thumbnail, + 'video_formats': video_formats, + 'audio_formats': audio_formats + }) + + except subprocess.TimeoutExpired: + return jsonify({'error': 'Timeout lors de l\'analyse'}), 408 + except json.JSONDecodeError: + return jsonify({'error': 'Erreur de parsing JSON'}), 500 + except Exception as e: + return jsonify({'error': f'Erreur serveur: {str(e)}'}), 500 + +@app.route('/api/download', methods=['POST']) +def download_video(): + """Télécharge la vidéo avec les paramètres spécifiés""" + try: + # Nettoyage des anciens fichiers avant nouveau téléchargement + for file in DOWNLOAD_DIR.glob('*'): + if file.is_file(): + try: + file.unlink() + except: + pass + + data = request.get_json() + url = data.get('url', '').strip() + video_format = data.get('video_format', '') + audio_format = data.get('audio_format', '') + output_container = data.get('output_container', 'mp4') + audio_codec = data.get('audio_codec', 'aac') + audio_bitrate = data.get('audio_bitrate', '192k') + + if not url: + return jsonify({'error': 'URL manquante'}), 400 + + # Construction de la chaîne de format + if video_format and audio_format: + format_string = f"{video_format}+{audio_format}" + elif video_format: + format_string = f"{video_format}+ba" + elif audio_format: + format_string = f"bv+{audio_format}" + else: + format_string = "bv+ba/best" + + # Nom de fichier sécurisé avec timestamp pour éviter les conflits + import time + timestamp = int(time.time()) + output_template = str(DOWNLOAD_DIR / f'%(title)s_{timestamp}.%(ext)s') + + # Construction de la commande yt-dlp + cmd = [ + 'yt-dlp', + '-f', format_string, + '--merge-output-format', output_container, + '-o', output_template + ] + + # Ajout des arguments de post-processing pour l'audio + postproc_added = False + if audio_codec and audio_codec != 'copy': + postproc_args = f"-c:a {audio_codec}" + if audio_bitrate: + postproc_args += f" -b:a {audio_bitrate}" + cmd.extend(['--postprocessor-args', f'ffmpeg:{postproc_args}']) + postproc_added = True + # Si copy est sélectionné, on ne réencode pas du tout + # Le bitrate est ignoré car on ne peut pas changer le bitrate sans réencoder + + # Ajout de l'URL à la fin + cmd.append(url) + + # Conversion de la commande en string pour affichage + cmd_string = ' '.join(f'"{arg}"' if ' ' in arg else arg for arg in cmd) + + # Exécution du téléchargement + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + + if result.returncode != 0: + return jsonify({ + 'error': f'Erreur téléchargement: {result.stderr}', + 'command': cmd_string + }), 400 + + # Recherche du fichier téléchargé + downloaded_files = list(DOWNLOAD_DIR.glob(f'*.{output_container}')) + if not downloaded_files: + downloaded_files = list(DOWNLOAD_DIR.glob('*')) + + if not downloaded_files: + return jsonify({ + 'error': 'Fichier téléchargé introuvable', + 'command': cmd_string + }), 404 + + # Dernier fichier modifié + latest_file = max(downloaded_files, key=lambda p: p.stat().st_mtime) + + return jsonify({ + 'success': True, + 'filename': latest_file.name, + 'size': latest_file.stat().st_size, + 'command': cmd_string, + 'stdout': result.stdout, + 'format_used': format_string, + 'postproc_applied': postproc_added, + 'audio_codec': audio_codec if audio_codec != 'copy' else 'original (copy)', + 'audio_bitrate': audio_bitrate if audio_codec != 'copy' else 'original' + }) + + except subprocess.TimeoutExpired: + return jsonify({'error': 'Timeout lors du téléchargement'}), 408 + except Exception as e: + return jsonify({'error': f'Erreur serveur: {str(e)}'}), 500 + +@app.route('/api/download-file/') +def download_file(filename): + """Télécharge le fichier généré""" + try: + file_path = DOWNLOAD_DIR / filename + if not file_path.exists(): + return jsonify({'error': 'Fichier introuvable'}), 404 + + return send_file(file_path, as_attachment=True) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/cleanup', methods=['POST']) +def cleanup(): + """Nettoie les fichiers téléchargés""" + try: + for file in DOWNLOAD_DIR.glob('*'): + if file.is_file(): + file.unlink() + return jsonify({'success': True}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..836a442 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3.8' + +services: + ytdlp-web: + build: . + container_name: ytdlp-web + ports: + - "5001:5000" + volumes: + - ./downloads:/app/downloads + restart: unless-stopped + environment: + - FLASK_ENV=production + - PYTHONUNBUFFERED=1 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000..9ef8187 --- /dev/null +++ b/docs/INSTALLATION.md @@ -0,0 +1,81 @@ +# 📦 Guide d'installation + +## Prérequis + +- Docker ou Podman installé +- 2 Go d'espace disque disponible +- Connexion internet + +## Installation avec Docker + +### 1. Via Docker Hub +```bash +docker pull votreusername/ytdlp-web:latest +docker run -d -p 5000:5000 --name ytdlp-web votreusername/ytdlp-web:latest +``` + +### 2. Build depuis les sources +```bash +git clone https://github.com/votreusername/yt-dlp-web.git +cd yt-dlp-web +docker build -t ytdlp-web . +docker run -d -p 5000:5000 --name ytdlp-web ytdlp-web +``` + +## Installation avec Podman + +### 1. Via registre +```bash +podman pull votreusername/ytdlp-web:latest +podman run -d -p 5000:5000 --name ytdlp-web votreusername/ytdlp-web:latest +``` + +### 2. Build depuis les sources +```bash +git clone https://github.com/votreusername/yt-dlp-web.git +cd yt-dlp-web +podman build -t ytdlp-web . +podman run -d -p 5000:5000 --name ytdlp-web ytdlp-web +``` + +## Configuration avancée + +### Avec volumes persistants +```bash +mkdir -p ~/ytdlp-downloads +docker run -d \ + -p 5000:5000 \ + -v ~/ytdlp-downloads:/app/downloads:Z \ + --name ytdlp-web \ + votreusername/ytdlp-web:latest +``` + +### Avec Docker Compose +```bash +docker-compose up -d +``` + +## Vérification + +Accédez à http://localhost:5000 dans votre navigateur. + +## Dépannage + +### Le conteneur ne démarre pas +```bash +docker logs ytdlp-web +``` + +### Port déjà utilisé + +Changez le port : +```bash +docker run -d -p 8080:5000 --name ytdlp-web votreusername/ytdlp-web:latest +``` + +### Permissions SELinux + +Ajoutez `:Z` au volume : +```bash +-v ~/downloads:/app/downloads:Z +``` diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..df1e0c9 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,124 @@ +# 📖 Guide d'utilisation + +## Interface principale + +### 1. Analyser une vidéo + +1. Collez l'URL YouTube dans le champ de saisie +2. Cliquez sur "🔍 Analyser la vidéo" +3. Attendez quelques secondes + +L'application va récupérer : +- Le titre de la vidéo +- La miniature +- Tous les formats disponibles (vidéo et audio) + +### 2. Sélectionner les formats + +#### Format vidéo +- Choisissez la résolution souhaitée (1080p, 720p, etc.) +- Ou laissez "Meilleure qualité auto" + +#### Format audio +- Sélectionnez la qualité audio +- Ou laissez "Meilleure qualité auto" + +### 3. Options de sortie + +#### Conteneur +- **MP4** : Compatible universellement (recommandé) +- **MKV** : Meilleure qualité, fichiers plus gros +- **WebM** : Format web, plus léger + +#### Codec Audio +- **AAC** : Standard, excellente qualité (recommandé) +- **MP3** : Compatible partout +- **Opus** : Meilleure qualité/taille +- **Copy** : Conserve l'audio original (pas de réencodage) + +#### Bitrate Audio +- **128 kbps** : Qualité correcte, fichier léger +- **192 kbps** : Bon compromis (recommandé) +- **256 kbps** : Très bonne qualité +- **320 kbps** : Qualité maximale + +> ⚠️ Le bitrate est désactivé si vous sélectionnez "Copy" + +### 4. Télécharger + +1. Cliquez sur "⬇️ Télécharger" +2. Attendez la fin du traitement +3. Consultez la commande exécutée +4. Cliquez sur "💾 Télécharger le fichier" + +## Exemples d'utilisation + +### Qualité maximale MP4 + +1. Sélectionnez le format vidéo le plus élevé (ex: 1080p60) +2. Sélectionnez le format audio le plus élevé +3. Conteneur : MP4 +4. Codec : AAC +5. Bitrate : 320k + +### Audio seulement + +1. Ne sélectionnez pas de format vidéo +2. Sélectionnez le meilleur format audio +3. Codec : MP3 ou AAC +4. Bitrate : 320k + +### Copie directe (rapide) + +1. Sélectionnez vos formats +2. Codec : Copy +3. Le téléchargement sera plus rapide (pas de réencodage) + +## Commandes équivalentes + +L'interface affiche la commande yt-dlp exécutée. Exemples : + +### Qualité max avec AAC +```bash +yt-dlp -f "137+140" --merge-output-format mp4 --postprocessor-args "ffmpeg:-c:a aac -b:a 192k" "URL" +``` + +### Copy direct +```bash +yt-dlp -f "137+140" --merge-output-format mp4 "URL" +``` + +## Astuces + +### Téléchargement rapide +- Utilisez "Copy" si les formats natifs vous conviennent +- Évitez le réencodage audio + +### Meilleure qualité +- Sélectionnez manuellement les meilleurs formats +- Utilisez AAC 320k +- Préférez MKV pour la qualité maximale + +### Compatibilité maximale +- Utilisez MP4 + AAC 192k +- Fonctionne sur tous les appareils + +## FAQ + +**Q : Pourquoi le téléchargement est long ?** +R : Le réencodage audio prend du temps. Utilisez "Copy" pour accélérer. + +**Q : Le bitrate est grisé, pourquoi ?** +R : Vous avez sélectionné "Copy", le bitrate ne peut pas être modifié sans réencodage. + +**Q : Puis-je télécharger plusieurs vidéos ?** +R : Oui, mais une à la fois. Les anciens fichiers sont automatiquement supprimés. + +**Q : Où sont stockés les fichiers ?** +R : Dans le conteneur à `/app/downloads`. Configurez un volume pour les conserver. + +## Limites + +- Vidéos publiques YouTube uniquement +- Timeout à 10 minutes par téléchargement +- Un téléchargement à la fois diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..5dd9d29 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,522 @@ + + + + + + yt-dlp Web Interface + + + +
+
+

🎬 yt-dlp Web Interface

+

Téléchargez des vidéos YouTube en haute qualité

+
+ +
+
+
+
+

Chargement en cours...

+
+ +
+
+ + +
+ +
+ +
+ Thumbnail +
+
+ +
+
+
📹 Sélection des formats
+
+
+ + +
+
+ + +
+
+
+ +
+
⚙️ Options de sortie
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ +
+
+
+ + + +