Compare commits
11 Commits
d972d40f7a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6336f1689f | |||
| 1ce626a20e | |||
| 2edfc6bb1d | |||
| 53d1bdb33a | |||
| f7df28b7f1 | |||
| c06a4f7b91 | |||
| fb9a3d6fc1 | |||
| 60d2f431bd | |||
| 8df38f3ec1 | |||
| 137f4ceac0 | |||
| 73742515ed |
@@ -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.
|
||||
|
||||
1
app
1
app
Submodule app deleted from 10d2cee94f
16
app/Dockerfile
Normal file
16
app/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Instala dependencias del sistema
|
||||
RUN apt-get update && apt-get install -y ffmpeg curl
|
||||
|
||||
# Instala yt-dlp
|
||||
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux -o /usr/local/bin/yt-dlp && \
|
||||
#RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp && \
|
||||
chmod a+rx /usr/local/bin/yt-dlp
|
||||
|
||||
# Crea carpetas necesarias
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
0
app/downloads/.gitkeep
Normal file
0
app/downloads/.gitkeep
Normal file
137
app/main.py
Normal file
137
app/main.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from flask import Flask, render_template, request, send_file, after_this_request
|
||||
import subprocess
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
|
||||
app = Flask(__name__)
|
||||
BASE_DOWNLOADS_DIR = "downloads"
|
||||
|
||||
os.makedirs(BASE_DOWNLOADS_DIR, exist_ok=True)
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
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'
|
||||
|
||||
if not url:
|
||||
# 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
|
||||
if download_type == "single":
|
||||
folder_id = str(uuid.uuid4())
|
||||
folder_path = os.path.join(BASE_DOWNLOADS_DIR, folder_id)
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
|
||||
filename_template = "%(title)s.%(ext)s"
|
||||
output_path = os.path.join(folder_path, filename_template)
|
||||
|
||||
cmd = ["yt-dlp", "-o", output_path]
|
||||
|
||||
if mode == "audio":
|
||||
cmd += ["-x", "--audio-format", "mp3"]
|
||||
|
||||
cmd.append(url)
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
files = os.listdir(folder_path)
|
||||
if not files:
|
||||
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 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())
|
||||
folder_path = os.path.join(BASE_DOWNLOADS_DIR, folder_id)
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
|
||||
# Obtener título real de la playlist
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["yt-dlp", "--flat-playlist", "--print", "%(playlist_title)s", url],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=True,
|
||||
text=True
|
||||
)
|
||||
playlist_title = result.stdout.strip().splitlines()[0]
|
||||
playlist_title_clean = "".join(c for c in playlist_title if c.isalnum() or c in " _-").strip().replace(" ", "_")
|
||||
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]
|
||||
|
||||
if mode == "audio":
|
||||
cmd += ["-x", "--audio-format", "mp3"]
|
||||
|
||||
cmd.append(url)
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
zip_path = shutil.make_archive(folder_path, 'zip', folder_path)
|
||||
zip_filename = f"{playlist_title_clean}.zip"
|
||||
|
||||
@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 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."), 400
|
||||
|
||||
return render_template("index.html")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
87
app/main.py.bak
Normal file
87
app/main.py.bak
Normal file
@@ -0,0 +1,87 @@
|
||||
from flask import Flask, render_template, request, send_file
|
||||
import subprocess
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
|
||||
app = Flask(__name__)
|
||||
BASE_DOWNLOADS_DIR = "downloads"
|
||||
|
||||
os.makedirs(BASE_DOWNLOADS_DIR, exist_ok=True)
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
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'
|
||||
|
||||
if not url:
|
||||
return render_template("index.html", error="URL obligatoria")
|
||||
|
||||
# Modo single o playlist
|
||||
if download_type == "single":
|
||||
unique_id = str(uuid.uuid4())
|
||||
filename_template = f"{unique_id}.%(ext)s"
|
||||
output_path = os.path.join(BASE_DOWNLOADS_DIR, filename_template)
|
||||
|
||||
cmd = ["yt-dlp", "-o", output_path]
|
||||
|
||||
if mode == "audio":
|
||||
cmd += ["-x", "--audio-format", "mp3"]
|
||||
|
||||
cmd.append(url)
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
# Encuentra el archivo resultante
|
||||
downloaded_file = None
|
||||
for file in os.listdir(BASE_DOWNLOADS_DIR):
|
||||
if file.startswith(unique_id):
|
||||
downloaded_file = os.path.join(BASE_DOWNLOADS_DIR, file)
|
||||
break
|
||||
|
||||
if not downloaded_file:
|
||||
return render_template("index.html", error="No se encontró el archivo descargado.")
|
||||
|
||||
# Enviar al navegador y luego borrar
|
||||
return send_file(downloaded_file, as_attachment=True, download_name=os.path.basename(downloaded_file), mimetype='application/octet-stream')
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
return render_template("index.html", error="Error al descargar el archivo.")
|
||||
|
||||
elif download_type == "playlist":
|
||||
folder_id = str(uuid.uuid4())
|
||||
folder_path = os.path.join(BASE_DOWNLOADS_DIR, folder_id)
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
|
||||
output_path = os.path.join(folder_path, "%(title)s.%(ext)s")
|
||||
cmd = ["yt-dlp", "-o", output_path]
|
||||
|
||||
if mode == "audio":
|
||||
cmd += ["-x", "--audio-format", "mp3"]
|
||||
|
||||
cmd.append(url)
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
# Comprimir en ZIP
|
||||
zip_path = shutil.make_archive(folder_path, 'zip', folder_path)
|
||||
|
||||
# Enviar al navegador
|
||||
# return send_file(zip_path, as_attachment=True, download_name="descarga_playlist.zip", mimetype='application/zip')
|
||||
return send_file(zip_path, as_attachment=True, download_name="%(playlist_title).zip", mimetype='application/zip')
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
return render_template("index.html", error="Error al descargar la lista.")
|
||||
|
||||
else:
|
||||
return render_template("index.html", error="Tipo de descarga no válido.")
|
||||
|
||||
return render_template("index.html")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
|
||||
2
app/requirements.txt
Normal file
2
app/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Flask
|
||||
yt-dlp
|
||||
269
app/templates/index.html
Normal file
269
app/templates/index.html
Normal file
@@ -0,0 +1,269 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Descargador de YouTube</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<style>
|
||||
.fade-out {
|
||||
opacity: 1;
|
||||
transition: opacity 1s ease-out;
|
||||
}
|
||||
.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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="playlist-tab" data-bs-toggle="tab" data-bs-target="#playlist" type="button" role="tab">Lista de reproducción</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content pt-4" id="downloadTabContent">
|
||||
<div class="tab-pane fade show active" id="single" role="tabpanel">
|
||||
<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>
|
||||
<input type="url" class="form-control" name="url" id="url1" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Formato</label><br>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="mode" id="video1" value="video" checked>
|
||||
<label class="form-check-label" for="video1">Vídeo</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="mode" id="audio1" value="audio">
|
||||
<label class="form-check-label" for="audio1">MP3</label>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="tab-pane fade" id="playlist" role="tabpanel">
|
||||
<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>
|
||||
<input type="url" class="form-control" name="url" id="url2" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Formato</label><br>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="mode" id="video2" value="video" checked>
|
||||
<label class="form-check-label" for="video2">Vídeo</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="mode" id="audio2" value="audio">
|
||||
<label class="form-check-label" for="audio2">MP3</label>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="modal fade" id="feedbackModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-light">
|
||||
<div class="modal-body">
|
||||
{% if success %}
|
||||
<div class="alert alert-success fade-out" role="alert">{{ success }}</div>
|
||||
{% elif error %}
|
||||
<div class="alert alert-danger fade-out" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const modal = new bootstrap.Modal(document.getElementById('feedbackModal'), {});
|
||||
|
||||
{% if success or error %}
|
||||
window.addEventListener("load", () => {
|
||||
modal.show();
|
||||
setTimeout(() => {
|
||||
modal.hide();
|
||||
}, 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>
|
||||
42
app/templates/index.html.bak
Normal file
42
app/templates/index.html.bak
Normal file
@@ -0,0 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Descargador de YouTube</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4 text-center">Descargador de YouTube</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
{% if success %}
|
||||
<div class="alert alert-success">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="url" class="form-label">URL de YouTube</label>
|
||||
<input type="url" class="form-control" name="url" id="url" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tipo de descarga</label><br>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="mode" id="video" value="video" checked>
|
||||
<label class="form-check-label" for="video">Vídeo</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="mode" id="audio" value="audio">
|
||||
<label class="form-check-label" for="audio">Audio (MP3)</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Descargar</button>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
<p class="text-muted">Los archivos descargados se guardan en la carpeta <code>downloads/</code>.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
web:
|
||||
build: ./app
|
||||
ports:
|
||||
- "5080:5000"
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- ./app/downloads:/app/downloads
|
||||
- ./app:/app
|
||||
container_name: yt-downloader
|
||||
|
||||
Reference in New Issue
Block a user