initial commit
commit
7050c6fe56
@ -0,0 +1,15 @@
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
downloads/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.vscode/
|
||||
.idea/
|
||||
.env
|
||||
docker-compose.yml
|
||||
LICENSE
|
||||
docs/
|
||||
*.mp4
|
||||
*.mkv
|
||||
*.webm
|
||||
@ -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
|
||||
@ -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"]
|
||||
@ -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.
|
||||
@ -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.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## ✨ 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
|
||||
|
||||

|
||||

|
||||
|
||||
## 🔧 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.
|
||||
|
||||
@ -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/<filename>')
|
||||
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)
|
||||
@ -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
|
||||
@ -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
|
||||
```
|
||||
@ -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
|
||||
@ -0,0 +1,522 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>yt-dlp Web Interface</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.3em;
|
||||
color: #667eea;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 1em;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.video-info img {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.formats-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.format-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.output-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
border-left: 4px solid #c00;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #efe;
|
||||
color: #0a0;
|
||||
border-left: 4px solid #0a0;
|
||||
}
|
||||
|
||||
.status.info {
|
||||
background: #eef;
|
||||
color: #00a;
|
||||
border-left: 4px solid #00a;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.download-section {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #10b981;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.download-info {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.command-display {
|
||||
background: #1e293b;
|
||||
color: #10b981;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin-top: 15px;
|
||||
border: 2px solid #334155;
|
||||
}
|
||||
|
||||
.command-label {
|
||||
color: #94a3b8;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.format-row,
|
||||
.output-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎬 yt-dlp Web Interface</h1>
|
||||
<p>Téléchargez des vidéos YouTube en haute qualité</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="status" id="status"></div>
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Chargement en cours...</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="input-group">
|
||||
<label for="url">URL de la vidéo YouTube</label>
|
||||
<input type="text" id="url" placeholder="https://www.youtube.com/watch?v=..." value="https://www.youtube.com/watch?v=HixGrIOBE0U">
|
||||
</div>
|
||||
<button class="btn btn-primary" id="analyzeBtn" onclick="analyzeVideo()">
|
||||
🔍 Analyser la vidéo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="video-info" id="videoInfo">
|
||||
<img id="thumbnail" src="" alt="Thumbnail">
|
||||
<div class="video-title" id="title"></div>
|
||||
</div>
|
||||
|
||||
<div class="formats-section" id="formatsSection">
|
||||
<div class="section">
|
||||
<div class="section-title">📹 Sélection des formats</div>
|
||||
<div class="format-row">
|
||||
<div class="input-group">
|
||||
<label for="videoFormat">Format Vidéo</label>
|
||||
<select id="videoFormat">
|
||||
<option value="">Meilleure qualité auto</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="audioFormat">Format Audio</label>
|
||||
<select id="audioFormat">
|
||||
<option value="">Meilleure qualité auto</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">⚙️ Options de sortie</div>
|
||||
<div class="output-options">
|
||||
<div class="input-group">
|
||||
<label for="outputContainer">Conteneur</label>
|
||||
<select id="outputContainer">
|
||||
<option value="mp4" selected>MP4</option>
|
||||
<option value="mkv">MKV</option>
|
||||
<option value="webm">WebM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="audioCodec">Codec Audio</label>
|
||||
<select id="audioCodec" onchange="toggleBitrateInput()">
|
||||
<option value="aac" selected>AAC</option>
|
||||
<option value="mp3">MP3</option>
|
||||
<option value="opus">Opus</option>
|
||||
<option value="copy">Copy (original)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="audioBitrate">Bitrate Audio</label>
|
||||
<select id="audioBitrate">
|
||||
<option value="128k">128 kbps</option>
|
||||
<option value="192k" selected>192 kbps</option>
|
||||
<option value="256k">256 kbps</option>
|
||||
<option value="320k">320 kbps</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" id="downloadBtn" onclick="downloadVideo()">
|
||||
⬇️ Télécharger
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="download-section" id="downloadSection">
|
||||
<div class="download-info" id="downloadInfo"></div>
|
||||
<div class="command-display" id="commandDisplay"></div>
|
||||
<button class="btn btn-success" id="downloadFileBtn" onclick="downloadFile()">
|
||||
💾 Télécharger le fichier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentFilename = '';
|
||||
|
||||
function toggleBitrateInput() {
|
||||
const audioCodec = document.getElementById('audioCodec').value;
|
||||
const audioBitrate = document.getElementById('audioBitrate');
|
||||
const bitrateLabel = audioBitrate.previousElementSibling;
|
||||
|
||||
if (audioCodec === 'copy') {
|
||||
audioBitrate.disabled = true;
|
||||
audioBitrate.style.opacity = '0.5';
|
||||
audioBitrate.style.cursor = 'not-allowed';
|
||||
bitrateLabel.style.opacity = '0.5';
|
||||
} else {
|
||||
audioBitrate.disabled = false;
|
||||
audioBitrate.style.opacity = '1';
|
||||
audioBitrate.style.cursor = 'pointer';
|
||||
bitrateLabel.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(message, type) {
|
||||
const status = document.getElementById('status');
|
||||
status.textContent = message;
|
||||
status.className = `status ${type}`;
|
||||
status.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
status.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
document.getElementById('loading').style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function analyzeVideo() {
|
||||
const url = document.getElementById('url').value.trim();
|
||||
|
||||
if (!url) {
|
||||
showStatus('Veuillez entrer une URL YouTube', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const analyzeBtn = document.getElementById('analyzeBtn');
|
||||
analyzeBtn.disabled = true;
|
||||
showLoading(true);
|
||||
document.getElementById('formatsSection').style.display = 'none';
|
||||
document.getElementById('downloadSection').style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/analyze', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({url})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Erreur lors de l\'analyse');
|
||||
}
|
||||
|
||||
// Affichage des informations vidéo
|
||||
document.getElementById('thumbnail').src = data.thumbnail;
|
||||
document.getElementById('title').textContent = data.title;
|
||||
document.getElementById('videoInfo').style.display = 'block';
|
||||
|
||||
// Remplissage des formats vidéo
|
||||
const videoSelect = document.getElementById('videoFormat');
|
||||
videoSelect.innerHTML = '<option value="">Meilleure qualité auto</option>';
|
||||
data.video_formats.forEach(fmt => {
|
||||
const option = document.createElement('option');
|
||||
option.value = fmt.id;
|
||||
option.textContent = fmt.label;
|
||||
videoSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Remplissage des formats audio
|
||||
const audioSelect = document.getElementById('audioFormat');
|
||||
audioSelect.innerHTML = '<option value="">Meilleure qualité auto</option>';
|
||||
data.audio_formats.forEach(fmt => {
|
||||
const option = document.createElement('option');
|
||||
option.value = fmt.id;
|
||||
option.textContent = fmt.label;
|
||||
audioSelect.appendChild(option);
|
||||
});
|
||||
|
||||
document.getElementById('formatsSection').style.display = 'block';
|
||||
showStatus('Analyse terminée avec succès !', 'success');
|
||||
} catch (error) {
|
||||
showStatus(error.message, 'error');
|
||||
} finally {
|
||||
analyzeBtn.disabled = false;
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadVideo() {
|
||||
const url = document.getElementById('url').value.trim();
|
||||
const videoFormat = document.getElementById('videoFormat').value;
|
||||
const audioFormat = document.getElementById('audioFormat').value;
|
||||
const outputContainer = document.getElementById('outputContainer').value;
|
||||
const audioCodec = document.getElementById('audioCodec').value;
|
||||
const audioBitrate = document.getElementById('audioBitrate').value;
|
||||
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
downloadBtn.disabled = true;
|
||||
showLoading(true);
|
||||
document.getElementById('downloadSection').style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/download', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
video_format: videoFormat,
|
||||
audio_format: audioFormat,
|
||||
output_container: outputContainer,
|
||||
audio_codec: audioCodec,
|
||||
audio_bitrate: audioBitrate
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Erreur lors du téléchargement');
|
||||
}
|
||||
|
||||
currentFilename = data.filename;
|
||||
const sizeMB = (data.size / (1024 * 1024)).toFixed(2);
|
||||
|
||||
document.getElementById('downloadInfo').innerHTML = `
|
||||
<strong>✅ Téléchargement terminé !</strong><br>
|
||||
Fichier: ${data.filename}<br>
|
||||
Taille: ${sizeMB} MB<br>
|
||||
Format utilisé: ${data.format_used}<br>
|
||||
Codec audio: ${data.audio_codec}<br>
|
||||
Bitrate audio: ${data.audio_bitrate}<br>
|
||||
Post-processing: ${data.postproc_applied ? 'Oui' : 'Non (copy direct)'}
|
||||
`;
|
||||
|
||||
document.getElementById('commandDisplay').innerHTML = `
|
||||
<div class="command-label">📋 Commande exécutée :</div>
|
||||
${data.command}
|
||||
`;
|
||||
|
||||
document.getElementById('downloadSection').style.display = 'block';
|
||||
showStatus('Vidéo téléchargée avec succès !', 'success');
|
||||
} catch (error) {
|
||||
showStatus(error.message, 'error');
|
||||
} finally {
|
||||
downloadBtn.disabled = false;
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile() {
|
||||
if (currentFilename) {
|
||||
window.location.href = `/api/download-file/${encodeURIComponent(currentFilename)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Permettre l'analyse avec la touche Entrée
|
||||
document.getElementById('url').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
analyzeVideo();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialiser l'état du bitrate au chargement
|
||||
window.addEventListener('load', toggleBitrateInput);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue