Compare commits

..

9 Commits

Author SHA1 Message Date
6336f1689f Actualizar docker-compose.yml
Cambiado el volumen
2025-07-15 21:29:26 +00:00
1ce626a20e Actualizar app/requirements.txt
Actualización menor
2025-07-15 21:26:59 +00:00
2edfc6bb1d Actualizar app/main.py
Añadido mensaje de descargando mientras descargas lista de reproducción
2025-07-15 21:26:34 +00:00
53d1bdb33a Actualizar app/templates/index.html
Añadido mensaje de descargando en la lista de videos/mp3
2025-07-15 21:25:52 +00:00
f7df28b7f1 Actualizar README.md
Rectificarl colne per clone
2025-07-11 06:50:31 +00:00
c06a4f7b91 Actualizar app/main.py
Borrar archivos después de bajarlos
2025-07-08 16:13:20 +00:00
fb9a3d6fc1 Actualizar docker-compose.yml
Cambio en el puerto
2025-07-08 15:44:03 +00:00
60d2f431bd Actualizar README.md
Actualizar git clone
2025-07-08 15:41:22 +00:00
8df38f3ec1 Agrego la carpeta download 2025-07-08 15:38:21 +00:00
6 changed files with 230 additions and 35 deletions

View File

@@ -44,8 +44,8 @@ ytdwl/
### 1. Clonar el repositorio o copiar los archivos
```bash
git clone https://github.com/tuusuario/ytdwl.git
cd ytdwl
git clone https://gitea.martivich.es/marti/YTDownloader.git
cd YTDownloader
```
> O copia el contenido manualmente si no usas Git.

0
app/downloads/.gitkeep Normal file
View File

View File

@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, send_file
from flask import Flask, render_template, request, send_file, after_this_request
import subprocess
import os
import uuid
@@ -13,11 +13,12 @@ os.makedirs(BASE_DOWNLOADS_DIR, exist_ok=True)
def index():
if request.method == "POST":
url = request.form.get("url")
mode = request.form.get("mode") # 'audio' o 'video'
download_type = request.form.get("type") # 'single' o 'playlist'
mode = request.form.get("mode") # 'audio' o 'video'
download_type = request.form.get("type") # 'single' o 'playlist'
if not url:
return render_template("index.html", error="URL obligatoria")
# En caso de error, siempre renderiza la plantilla para que el JS pueda leer el error.
return render_template("index.html", error="URL obligatoria"), 400
# Modo single o playlist
@@ -39,16 +40,34 @@ def index():
try:
subprocess.run(cmd, check=True)
# Buscar el archivo descargado (único archivo en carpeta)
files = os.listdir(folder_path)
if not files:
return render_template("index.html", error="No se pudo descargar el archivo.")
return render_template("index.html", error="No se pudo descargar el archivo."), 500
downloaded_file_path = os.path.join(folder_path, files[0])
@after_this_request
def cleanup(response):
try:
if os.path.exists(downloaded_file_path):
os.remove(downloaded_file_path)
if os.path.exists(folder_path):
os.rmdir(folder_path)
except Exception as e:
print(f"Error al borrar archivos: {e}")
return response
# Si es exitoso, solo envía el archivo. No render_template aquí.
return send_file(downloaded_file_path, as_attachment=True, download_name=files[0])
except subprocess.CalledProcessError:
return render_template("index.html", error="Error al descargar el vídeo.")
except subprocess.CalledProcessError as e:
# Captura la salida de error de yt-dlp para un mensaje más específico si es posible
error_message = f"Error al descargar el vídeo: {e}"
if e.stderr:
error_message += f" - {e.stderr.decode().strip()}"
return render_template("index.html", error=error_message), 500
except Exception as e:
return render_template("index.html", error=f"Error inesperado: {e}"), 500
elif download_type == "playlist":
folder_id = str(uuid.uuid4())
@@ -64,14 +83,15 @@ def index():
check=True,
text=True
)
#playlist_title = result.stdout.strip()
playlist_title = result.stdout.strip().splitlines()[0]
# Limpiar el nombre para que sea válido como nombre de archivo
playlist_title_clean = "".join(c for c in playlist_title if c.isalnum() or c in " _-").strip().replace(" ", "_")
print(f"Título original: {playlist_title}")
print(f"Título limpio: {playlist_title_clean}")
except subprocess.CalledProcessError:
return render_template("index.html", error="Error al obtener el título de la playlist.")
except subprocess.CalledProcessError as e:
error_message = f"Error al obtener el título de la playlist: {e}"
if e.stderr:
error_message += f" - {e.stderr.decode().strip()}"
return render_template("index.html", error=error_message), 500
except Exception as e:
return render_template("index.html", error=f"Error inesperado al obtener título de playlist: {e}"), 500
output_path = os.path.join(folder_path, "%(title)s.%(ext)s")
cmd = ["yt-dlp", "-o", output_path]
@@ -84,21 +104,34 @@ def index():
try:
subprocess.run(cmd, check=True)
# Comprimir en ZIP
zip_path = shutil.make_archive(folder_path, 'zip', folder_path)
zip_filename = f"{playlist_title_clean}.zip"
# Enviar al navegador
@after_this_request
def cleanup(response):
try:
if os.path.exists(folder_path):
shutil.rmtree(folder_path)
if os.path.exists(zip_path):
os.remove(zip_path)
except Exception as e:
print(f"Error al borrar archivos de la playlist: {e}")
return response
# Si es exitoso, solo envía el archivo. No render_template aquí.
return send_file(zip_path, as_attachment=True, download_name=zip_filename, mimetype='application/zip')
except subprocess.CalledProcessError:
return render_template("index.html", error="Error al descargar la lista.")
except subprocess.CalledProcessError as e:
error_message = f"Error al descargar la lista: {e}"
if e.stderr:
error_message += f" - {e.stderr.decode().strip()}"
return render_template("index.html", error=error_message), 500
except Exception as e:
return render_template("index.html", error=f"Error inesperado al descargar playlist: {e}"), 500
else:
return render_template("index.html", error="Tipo de descarga no válido.")
return render_template("index.html", error="Tipo de descarga no válido."), 400
return render_template("index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
app.run(host="0.0.0.0", port=5000)

View File

@@ -1 +1,2 @@
Flask
yt-dlp

View File

@@ -13,13 +13,22 @@
.fade-out.hide {
opacity: 0;
}
/* Estilos para el spinner */
.spinner-container {
display: none; /* Oculto por defecto */
text-align: center;
margin-top: 20px;
}
.spinner-border {
width: 3rem;
height: 3rem;
}
</style>
</head>
<body class="bg-light">
<div class="container mt-5">
<h1 class="mb-4 text-center">Descargador de YouTube</h1>
<!-- Tabs -->
<ul class="nav nav-tabs" id="downloadTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="single-tab" data-bs-toggle="tab" data-bs-target="#single" type="button" role="tab">Vídeo / MP3 individual</button>
@@ -30,9 +39,8 @@
</ul>
<div class="tab-content pt-4" id="downloadTabContent">
<!-- Descarga individual -->
<div class="tab-pane fade show active" id="single" role="tabpanel">
<form method="POST">
<form method="POST" id="singleDownloadForm">
<input type="hidden" name="type" value="single">
<div class="mb-3">
<label for="url1" class="form-label">URL del vídeo</label>
@@ -49,13 +57,18 @@
<label class="form-check-label" for="audio1">MP3</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Descargar</button>
<button type="submit" class="btn btn-primary" id="singleDownloadButton">Descargar</button>
<div class="spinner-container" id="singleLoadingSpinner">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p class="mt-2">Descargando archivo...</p>
</div>
</form>
</div>
<!-- Descarga de lista -->
<div class="tab-pane fade" id="playlist" role="tabpanel">
<form method="POST">
<form method="POST" id="playlistDownloadForm">
<input type="hidden" name="type" value="playlist">
<div class="mb-3">
<label for="url2" class="form-label">URL de la lista</label>
@@ -72,12 +85,17 @@
<label class="form-check-label" for="audio2">MP3</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Descargar Lista</button>
<button type="submit" class="btn btn-primary" id="playlistDownloadButton">Descargar Lista</button>
<div class="spinner-container" id="playlistLoadingSpinner">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p class="mt-2">Descargando archivos...</p>
</div>
</form>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="feedbackModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content bg-light">
@@ -95,6 +113,7 @@
<script>
const modal = new bootstrap.Modal(document.getElementById('feedbackModal'), {});
{% if success or error %}
window.addEventListener("load", () => {
modal.show();
@@ -103,6 +122,148 @@
}, 3000);
});
{% endif %}
const singleDownloadForm = document.getElementById('singleDownloadForm');
const singleDownloadButton = document.getElementById('singleDownloadButton');
const singleLoadingSpinner = document.getElementById('singleLoadingSpinner');
const playlistDownloadForm = document.getElementById('playlistDownloadForm');
const playlistDownloadButton = document.getElementById('playlistDownloadButton');
const playlistLoadingSpinner = document.getElementById('playlistLoadingSpinner');
function showLoadingState(button, spinner) {
button.disabled = true;
spinner.style.display = 'block';
}
function hideLoadingState(button, spinner) {
button.disabled = false;
spinner.style.display = 'none';
}
// Single video/MP3 download
singleDownloadForm.addEventListener('submit', function(event) {
// Prevent default form submission to handle it with Fetch API
event.preventDefault();
showLoadingState(singleDownloadButton, singleLoadingSpinner);
const formData = new FormData(this); // 'this' refers to the form element
fetch('/', {
method: 'POST',
body: formData
})
.then(response => {
// Check if the response is a file (e.g., status 200 and content-disposition header)
const contentDisposition = response.headers.get('Content-Disposition');
if (response.ok && contentDisposition && contentDisposition.includes('attachment')) {
return response.blob().then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const filename = contentDisposition.split('filename=')[1] ? contentDisposition.split('filename=')[1].replace(/"/g, '') : 'download';
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
// Hide loading state after file download starts
hideLoadingState(singleDownloadButton, singleLoadingSpinner);
// Show success message if needed, or clear form
showModal('Descarga iniciada correctamente.', 'success');
singleDownloadForm.reset(); // Clear the form
});
} else {
// If not a file, it's likely an error message from the server
return response.text().then(text => {
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const errorMessage = doc.querySelector('.alert-danger')?.textContent || 'Error desconocido';
hideLoadingState(singleDownloadButton, singleLoadingSpinner);
showModal(errorMessage, 'error');
});
}
})
.catch(error => {
console.error('Error:', error);
hideLoadingState(singleDownloadButton, singleLoadingSpinner);
showModal('Error al conectar con el servidor.', 'error');
});
});
// Playlist download
playlistDownloadForm.addEventListener('submit', function(event) {
// Prevent default form submission to handle it with Fetch API
event.preventDefault();
showLoadingState(playlistDownloadButton, playlistLoadingSpinner);
const formData = new FormData(this); // 'this' refers to the form element
fetch('/', {
method: 'POST',
body: formData
})
.then(response => {
const contentDisposition = response.headers.get('Content-Disposition');
if (response.ok && contentDisposition && contentDisposition.includes('attachment')) {
return response.blob().then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const filename = contentDisposition.split('filename=')[1] ? contentDisposition.split('filename=')[1].replace(/"/g, '') : 'download.zip';
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
// Hide loading state after file download starts
hideLoadingState(playlistDownloadButton, playlistLoadingSpinner);
// Show success message or clear form
showModal('Descarga de la lista iniciada correctamente.', 'success');
playlistDownloadForm.reset(); // Clear the form
});
} else {
return response.text().then(text => {
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const errorMessage = doc.querySelector('.alert-danger')?.textContent || 'Error desconocido';
hideLoadingState(playlistDownloadButton, playlistLoadingSpinner);
showModal(errorMessage, 'error');
});
}
})
.catch(error => {
console.error('Error:', error);
hideLoadingState(playlistDownloadButton, playlistLoadingSpinner);
showModal('Error al conectar con el servidor.', 'error');
});
});
// Function to show the modal with messages
function showModal(message, type) {
const feedbackModalBody = document.querySelector('#feedbackModal .modal-body');
feedbackModalBody.innerHTML = `
<div class="alert alert-${type} fade-out" role="alert">${message}</div>
`;
modal.show();
setTimeout(() => {
modal.hide();
}, 3000);
}
// Set initial state for tabs (important for refreshing page and keeping correct tab active)
document.addEventListener('DOMContentLoaded', (event) => {
const currentTab = localStorage.getItem('activeTab');
if (currentTab) {
new bootstrap.Tab(document.getElementById(currentTab + '-tab')).show();
}
document.querySelectorAll('.nav-link').forEach(tabLink => {
tabLink.addEventListener('shown.bs.tab', function (e) {
localStorage.setItem('activeTab', e.target.id.replace('-tab', ''));
});
});
});
</script>
</body>
</html>
</html>

View File

@@ -2,7 +2,7 @@ services:
web:
build: ./app
ports:
- "5080:5000"
- "5000:5000"
volumes:
- ./app/downloads:/app/downloads
- ./app:/app
container_name: yt-downloader