You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

947 lines
36 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Youtube Downloader</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="checkbox"] {
margin-right: 8px;
cursor: pointer;
}
input[type="text"]:focus,
select:focus {
outline: none;
border-color: #667eea;
}
input[type="text"]:disabled,
select:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: #f5f5f5;
}
.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;
}
.progress-section {
padding: 20px;
background: #eff6ff;
border-radius: 12px;
border: 2px solid #3b82f6;
margin-top: 20px;
}
.progress-section h3 {
margin: 0 0 15px 0;
color: #1e40af;
}
.progress-bar-container {
width: 100%;
height: 30px;
background: #dbeafe;
border-radius: 15px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.5);
}
.progress-info {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 0.9em;
color: #1e40af;
}
.progress-info span {
padding: 5px 10px;
background: white;
border-radius: 5px;
margin: 0 5px;
}
.footer {
max-width: 900px;
margin: 24px auto 0;
display: flex;
justify-content: center;
gap: 10px;
}
.footer a {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.18);
color: #fff;
text-decoration: none;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(4px);
}
.footer a:hover {
background: rgba(0, 0, 0, 0.28);
transform: translateY(-1px);
transition: all 0.2s ease;
}
.footer img {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.25);
object-fit: cover;
}
.footer span {
font-weight: 600;
letter-spacing: 0.2px;
}
.lang-toggle {
position: absolute;
top: 20px;
right: 20px;
display: flex;
gap: 5px;
background: rgba(255, 255, 255, 0.2);
padding: 5px;
border-radius: 20px;
}
.lang-btn {
padding: 6px 12px;
border: none;
border-radius: 15px;
cursor: pointer;
font-weight: 600;
font-size: 0.85em;
transition: all 0.2s;
background: transparent;
color: rgba(255, 255, 255, 0.7);
}
.lang-btn.active {
background: white;
color: #667eea;
}
.lang-btn:hover:not(.active) {
background: rgba(255, 255, 255, 0.3);
color: white;
}
@media (max-width: 768px) {
.format-row,
.output-options {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="lang-toggle">
<button class="lang-btn active" id="langRu" onclick="setLanguage('ru')">RU</button>
<button class="lang-btn" id="langEn" onclick="setLanguage('en')">EN</button>
</div>
<h1>🎬 YouTube Downloader</h1>
<p data-i18n="subtitle">Скачать видео с YouTube в высоком качестве</p>
</div>
<div class="content">
<div class="status" id="status"></div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p data-i18n="loading">Загружаю...</p>
</div>
<div class="section">
<div class="input-group">
<label for="url" data-i18n="urlLabel">URL на YouTube видео</label>
<input type="text" id="url" placeholder="Вставьте сюда ссылку на YouTube видео">
</div>
<button class="btn btn-primary" id="analyzeBtn" onclick="analyzeVideo()" data-i18n="analyzeBtn">
🔍 Проверить ссылку
</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" data-i18n="formatSelection">📹 Выбор формата</div>
<div class="input-group">
<label>
<input type="checkbox" id="audioOnlyMode" onchange="toggleAudioOnlyMode()">
<span data-i18n="audioOnlyMode">🎵 Выбор аудио</span>
</label>
</div>
<div class="format-row">
<div class="input-group" id="videoFormatGroup">
<label for="videoFormat" data-i18n="videoFormat">Видео формат</label>
<select id="videoFormat">
<option value="" data-i18n="bestQualityAuto">Meilleure qualité auto</option>
</select>
</div>
<div class="input-group">
<label for="audioFormat" data-i18n="audioFormat">Аудио формат</label>
<select id="audioFormat">
<option value="" data-i18n="bestQualityAuto">Наилучшее качество</option>
</select>
</div>
</div>
</div>
<div class="section">
<div class="section-title" data-i18n="outputOptions">⚙️ Options de sortie</div>
<div class="output-options">
<div class="input-group" id="outputContainerGroup">
<label for="outputContainer" data-i18n="container">Conteneur</label>
<select id="outputContainer">
<option value="mp4" selected>MP4</option>
<option value="mkv">MKV</option>
<option value="webm">WebM</option>
<option value="m4a">M4A (Audio)</option>
<option value="mp3">MP3 (Audio)</option>
</select>
</div>
<div class="input-group">
<label for="audioCodec" data-i18n="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" data-i18n="copyOriginal">Copy (original)</option>
</select>
</div>
<div class="input-group">
<label for="audioBitrate" data-i18n="audioBitrate">Аудио Битрейт</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()" data-i18n="downloadBtn">
⬇️ Скачать
</button>
</div>
<!-- Progress Bar -->
<div class="progress-section" id="progressSection" style="display: none;">
<h3 data-i18n="downloadInProgress">📥 Загрузка...</h3>
<div class="progress-bar-container">
<div class="progress-bar" id="progressBar" style="width: 0%"></div>
</div>
<div class="progress-info">
<span id="progressPercent">0%</span>
<span id="progressSpeed"></span>
<span id="progressEta"></span>
</div>
</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()" style="margin-top: 15px;" data-i18n="downloadFileBtn">
💾 Скачать файл
</button>
</div>
</div>
</div>
<div class="footer">
<a href="https://vk.com/emmaus" target="_blank" rel="noreferrer">
<img src="https://memo.morozoff.pro/file/users/1/avatar" alt="Raynoxis YouTube avatar" loading="lazy">
<span>Aiden at VK.com</span>
</a>
</div>
<script>
let currentFilename = '';
let currentSessionId = '';
let currentLang = localStorage.getItem('lang') || 'ru';
const translations = {
ru: {
subtitle: 'Скачивайте видео с YouTube в высоком качестве',
loading: 'Загрузка...',
urlLabel: 'URL YouTube видео',
analyzeBtn: '🔍 Проверить ссылку',
formatSelection: '📹 Выбор форматов',
audioOnlyMode: '🎵 Только аудио',
videoFormat: 'Видео формат',
audioFormat: 'Аудио формат',
bestQualityAuto: 'Наилучшее качество',
bestAuto: 'Наилучшее (авто)',
outputOptions: '⚙️ Выходные параметры',
container: 'Контейнер',
audioOutputFormat: 'Выходной аудио формат',
audioCodec: 'Аудио кодек',
audioCodecManaged: 'Аудио кодек (управляется форматом)',
copyOriginal: 'Оригинал',
audioBitrate: 'Аудио битрейт',
downloadBtn: '⬇️ Скачать',
downloadInProgress: '📥 Загрузка...',
downloadFileBtn: '💾 Скачать файл',
enterUrl: 'Пожалуйста, введите URL-адрес видео на YouTube',
analysisError: "Ошибка в процессе анализа. Попробуйте еще раз",
analysisSuccess: 'Анализ успешно завершен!',
downloadError: 'Ошибка во время загрузки',
downloadSuccess: 'VВидео успешно загружено!',
serverError: 'Ошибка подключения к серверу',
downloadComplete: '✅ Загрузка завершена!',
file: 'Файл',
size: 'Размер',
audioOnlyModeLabel: '🎵 Режим: Только аудио',
formatUsed: 'Используемый формат',
audioCodecUsed: 'Аудио кодек',
audioBitrateUsed: 'Битрейт аудио',
postProcessing: 'Постобработка',
yes: 'Да',
noCopyDirect: 'Нет (прямая копия)',
commandExecuted: '📋 Выполненая команда :'
},
en: {
subtitle: 'Download YouTube videos in high quality',
loading: 'Loading...',
urlLabel: 'YouTube Video URL',
analyzeBtn: '🔍 Analyze Video',
formatSelection: '📹 Format Selection',
audioOnlyMode: '🎵 Audio Only Mode',
videoFormat: 'Video Format',
audioFormat: 'Audio Format',
bestQualityAuto: 'Best quality auto',
bestAuto: 'Best (auto)',
outputOptions: '⚙️ Output Options',
container: 'Container',
audioOutputFormat: 'Audio Output Format',
audioCodec: 'Audio Codec',
audioCodecManaged: 'Audio Codec (managed by format)',
copyOriginal: 'Copy (original)',
audioBitrate: 'Audio Bitrate',
downloadBtn: '⬇️ Download',
downloadInProgress: '📥 Downloading...',
downloadFileBtn: '💾 Download File',
enterUrl: 'Please enter a YouTube URL',
analysisError: 'Error during analysis',
analysisSuccess: 'Analysis completed successfully!',
downloadError: 'Error during download',
downloadSuccess: 'Video downloaded successfully!',
serverError: 'Server connection error',
downloadComplete: '✅ Download complete!',
file: 'File',
size: 'Size',
audioOnlyModeLabel: '🎵 Mode: Audio only',
formatUsed: 'Format used',
audioCodecUsed: 'Audio codec',
audioBitrateUsed: 'Audio bitrate',
postProcessing: 'Post-processing',
yes: 'Yes',
noCopyDirect: 'No (direct copy)',
commandExecuted: '📋 Command executed:'
}
};
function setLanguage(lang) {
currentLang = lang;
localStorage.setItem('lang', lang);
document.getElementById('langRu').classList.toggle('active', lang === 'ru');
document.getElementById('langEn').classList.toggle('active', lang === 'en');
document.documentElement.lang = lang;
applyTranslations();
// Rafraîchir les options dynamiques du mode audio-only si actif
const audioOnlyCheckbox = document.getElementById('audioOnlyMode');
if (audioOnlyCheckbox && audioOnlyCheckbox.checked) {
toggleAudioOnlyMode();
}
// Mettre à jour l'option "Meilleure qualité auto" dans les dropdowns de format
updateFormatDropdownsTranslations();
}
function updateFormatDropdownsTranslations() {
// Mettre à jour la première option des selects de format si elle contient la traduction
const videoSelect = document.getElementById('videoFormat');
const audioSelect = document.getElementById('audioFormat');
if (videoSelect && videoSelect.options.length > 0) {
const firstOption = videoSelect.options[0];
if (firstOption.value === '') {
firstOption.textContent = t('bestQualityAuto');
}
}
if (audioSelect && audioSelect.options.length > 0) {
const firstOption = audioSelect.options[0];
if (firstOption.value === '') {
firstOption.textContent = t('bestQualityAuto');
}
}
// Mettre à jour l'option "Copy (original)" dans le codec audio
const audioCodecSelect = document.getElementById('audioCodec');
if (audioCodecSelect) {
for (let option of audioCodecSelect.options) {
if (option.value === 'copy') {
option.textContent = t('copyOriginal');
}
}
}
}
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (translations[currentLang][key]) {
el.textContent = translations[currentLang][key];
}
});
}
function t(key) {
return translations[currentLang][key] || key;
}
function toggleAudioOnlyMode() {
const audioOnly = document.getElementById('audioOnlyMode').checked;
const videoFormatGroup = document.getElementById('videoFormatGroup');
const videoFormat = document.getElementById('videoFormat');
const videoLabel = videoFormatGroup.querySelector('label');
const outputContainer = document.getElementById('outputContainer');
const outputContainerGroup = document.getElementById('outputContainerGroup');
const outputLabel = outputContainerGroup.querySelector('label');
const audioCodec = document.getElementById('audioCodec');
const audioCodecLabel = audioCodec.previousElementSibling;
if (audioOnly) {
// Griser le format vidéo
videoFormat.disabled = true;
videoFormat.style.opacity = '0.5';
videoFormat.style.cursor = 'not-allowed';
videoLabel.style.opacity = '0.5';
// Adapter le conteneur pour formats audio seulement
outputContainer.innerHTML = `
<option value="mp3" selected>MP3</option>
<option value="m4a">M4A/AAC</option>
<option value="opus">Opus</option>
<option value="flac">FLAC</option>
<option value="wav">WAV</option>
<option value="best">${t('bestAuto')}</option>
`;
outputLabel.textContent = t('audioOutputFormat');
// Le codec devient moins pertinent en mode audio-only (géré par --audio-format)
audioCodec.disabled = true;
audioCodec.style.opacity = '0.5';
audioCodec.style.cursor = 'not-allowed';
audioCodecLabel.style.opacity = '0.5';
audioCodecLabel.textContent = t('audioCodecManaged');
} else {
// Réactiver le format vidéo
videoFormat.disabled = false;
videoFormat.style.opacity = '1';
videoFormat.style.cursor = 'pointer';
videoLabel.style.opacity = '1';
// Rétablir les conteneurs vidéo
outputContainer.innerHTML = `
<option value="mp4" selected>MP4</option>
<option value="mkv">MKV</option>
<option value="webm">WebM</option>
`;
outputLabel.textContent = t('container');
// Réactiver le codec audio
audioCodec.disabled = false;
audioCodec.style.opacity = '1';
audioCodec.style.cursor = 'pointer';
audioCodecLabel.style.opacity = '1';
audioCodecLabel.textContent = t('audioCodec');
}
// Toujours permettre le contrôle du bitrate
toggleBitrateInput();
}
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(t('enterUrl'), '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 || t('analysisError'));
}
// 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="">${t('bestQualityAuto')}</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="">${t('bestQualityAuto')}</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(t('analysisSuccess'), 'success');
} catch (error) {
showStatus(error.message, 'error');
} finally {
analyzeBtn.disabled = false;
showLoading(false);
}
}
async function downloadVideo() {
const url = document.getElementById('url').value.trim();
const audioOnly = document.getElementById('audioOnlyMode').checked;
const videoFormat = audioOnly ? '' : 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';
document.getElementById('progressSection').style.display = 'none';
try {
// Démarrer le téléchargement
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,
audio_only: audioOnly
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || t('downloadError'));
}
// Récupérer le session_id et se connecter au SSE
currentSessionId = data.session_id;
showLoading(false);
document.getElementById('progressSection').style.display = 'block';
// Connexion SSE pour suivre la progression
const eventSource = new EventSource(`/api/progress/${currentSessionId}`);
eventSource.onmessage = function(event) {
const progressData = JSON.parse(event.data);
if (progressData.status === 'downloading') {
// Mettre à jour la barre de progression
const percent = Math.round(progressData.progress);
document.getElementById('progressBar').style.width = percent + '%';
document.getElementById('progressPercent').textContent = percent + '%';
document.getElementById('progressSpeed').textContent = progressData.speed || '';
document.getElementById('progressEta').textContent = progressData.eta ? `ETA: ${progressData.eta}` : '';
} else if (progressData.status === 'completed') {
// Téléchargement terminé
eventSource.close();
document.getElementById('progressBar').style.width = '100%';
document.getElementById('progressPercent').textContent = '100%';
document.getElementById('progressSection').style.display = 'none';
// Afficher les infos de téléchargement
currentFilename = progressData.filename;
const sizeMB = (progressData.size / (1024 * 1024)).toFixed(2);
document.getElementById('downloadInfo').innerHTML = `
<strong>${t('downloadComplete')}</strong><br>
${t('file')}: ${progressData.filename}<br>
${t('size')}: ${sizeMB} MB<br>
${progressData.audio_only ? t('audioOnlyModeLabel') + '<br>' : ''}
${t('formatUsed')}: ${progressData.format_used}<br>
${t('audioCodecUsed')}: ${progressData.audio_codec}<br>
${t('audioBitrateUsed')}: ${progressData.audio_bitrate}<br>
${t('postProcessing')}: ${progressData.postproc_applied ? t('yes') : t('noCopyDirect')}
`;
document.getElementById('commandDisplay').innerHTML = `
<div class="command-label">${t('commandExecuted')}</div>
${progressData.command}
`;
document.getElementById('downloadSection').style.display = 'block';
showStatus(t('downloadSuccess'), 'success');
downloadBtn.disabled = false;
} else if (progressData.status === 'error') {
// Erreur de téléchargement
eventSource.close();
document.getElementById('progressSection').style.display = 'none';
showStatus(progressData.error || t('downloadError'), 'error');
downloadBtn.disabled = false;
}
};
eventSource.onerror = function() {
eventSource.close();
showStatus(t('serverError'), 'error');
downloadBtn.disabled = false;
document.getElementById('progressSection').style.display = 'none';
};
} catch (error) {
showStatus(error.message, 'error');
downloadBtn.disabled = false;
showLoading(false);
}
}
function downloadFile() {
if (currentFilename && currentSessionId) {
window.location.href = `/api/download-file/${encodeURIComponent(currentSessionId)}/${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 et la langue au chargement
window.addEventListener('load', () => {
toggleBitrateInput();
setLanguage(currentLang);
});
</script>
</body>
</html>