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