initial commit

main
Raynoxis 8 months ago
commit 7050c6fe56

@ -0,0 +1,15 @@
.git
.gitignore
*.md
downloads/
__pycache__/
*.pyc
.vscode/
.idea/
.env
docker-compose.yml
LICENSE
docs/
*.mp4
*.mkv
*.webm

38
.gitignore vendored

@ -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.
![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.

234
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/<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…
Cancel
Save