Spaces:
Sleeping
Sleeping
Nueva version
Browse files- Dockerfile.txt +13 -0
- app/actions.ts +49 -0
- app/api/download/route.ts +46 -0
- app/page.tsx +10 -0
- components/VideoDownloader.tsx +89 -0
- package.json +14 -0
- public/index.html +66 -0
- server.js +89 -0
- tailwind.config.js +72 -0
Dockerfile.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-alpine
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY package.json package-lock.json ./
|
| 6 |
+
RUN npm install
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 7860
|
| 11 |
+
|
| 12 |
+
CMD ["npm", "start"]
|
| 13 |
+
|
app/actions.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use server"
|
| 2 |
+
|
| 3 |
+
import ytdl from "ytdl-core"
|
| 4 |
+
|
| 5 |
+
async function fetchYouTubeInfo(url: string) {
|
| 6 |
+
const info = await ytdl.getInfo(url)
|
| 7 |
+
return {
|
| 8 |
+
platform: "YouTube",
|
| 9 |
+
title: info.videoDetails.title,
|
| 10 |
+
formats: [
|
| 11 |
+
{
|
| 12 |
+
quality: "Alta calidad (720p)",
|
| 13 |
+
extension: "mp4",
|
| 14 |
+
url: `/api/download?url=${encodeURIComponent(url)}&format=mp4`,
|
| 15 |
+
},
|
| 16 |
+
{ quality: "Audio (MP3)", extension: "mp3", url: `/api/download?url=${encodeURIComponent(url)}&format=mp3` },
|
| 17 |
+
],
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
async function fetchGenericInfo(url: string, platform: string) {
|
| 22 |
+
return {
|
| 23 |
+
platform,
|
| 24 |
+
title: "Video",
|
| 25 |
+
formats: [{ quality: "Original", extension: "mp4", url: `/api/download?url=${encodeURIComponent(url)}` }],
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export async function processUrl(url: string) {
|
| 30 |
+
try {
|
| 31 |
+
const cleanUrl = url.trim()
|
| 32 |
+
if (!cleanUrl) {
|
| 33 |
+
return { error: "Por favor, ingrese una URL válida." }
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
if (cleanUrl.includes("youtube.com") || cleanUrl.includes("youtu.be")) {
|
| 37 |
+
return await fetchYouTubeInfo(cleanUrl)
|
| 38 |
+
} else if (cleanUrl.includes("facebook.com")) {
|
| 39 |
+
return await fetchGenericInfo(cleanUrl, "Facebook")
|
| 40 |
+
} else if (cleanUrl.includes("twitter.com")) {
|
| 41 |
+
return await fetchGenericInfo(cleanUrl, "Twitter")
|
| 42 |
+
} else {
|
| 43 |
+
throw new Error("Plataforma no soportada")
|
| 44 |
+
}
|
| 45 |
+
} catch (error) {
|
| 46 |
+
return { error: "No se pudo procesar la URL. Asegúrese de que sea una URL de video válida." }
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
app/api/download/route.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
| 2 |
+
import ytdl from "ytdl-core"
|
| 3 |
+
import axios from "axios"
|
| 4 |
+
|
| 5 |
+
export async function GET(req: NextRequest) {
|
| 6 |
+
const url = req.nextUrl.searchParams.get("url")
|
| 7 |
+
const format = req.nextUrl.searchParams.get("format")
|
| 8 |
+
|
| 9 |
+
if (!url) {
|
| 10 |
+
return NextResponse.json({ error: "URL no proporcionada" }, { status: 400 })
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
try {
|
| 14 |
+
if (url.includes("youtube.com") || url.includes("youtu.be")) {
|
| 15 |
+
const info = await ytdl.getInfo(url)
|
| 16 |
+
const format_id = format === "mp3" ? "audioonly" : "highest"
|
| 17 |
+
const format_info = ytdl.chooseFormat(info.formats, { quality: format_id })
|
| 18 |
+
|
| 19 |
+
const stream = ytdl(url, { format: format_info })
|
| 20 |
+
const filename = `${info.videoDetails.title}.${format === "mp3" ? "mp3" : "mp4"}`
|
| 21 |
+
|
| 22 |
+
return new NextResponse(stream as any, {
|
| 23 |
+
headers: {
|
| 24 |
+
"Content-Disposition": `attachment; filename="${filename}"`,
|
| 25 |
+
"Content-Type": format === "mp3" ? "audio/mpeg" : "video/mp4",
|
| 26 |
+
},
|
| 27 |
+
})
|
| 28 |
+
} else {
|
| 29 |
+
// For other platforms, we'll use a more generic approach
|
| 30 |
+
const response = await axios.get(url, { responseType: "stream" })
|
| 31 |
+
const contentType = response.headers["content-type"]
|
| 32 |
+
const contentDisposition = response.headers["content-disposition"]
|
| 33 |
+
|
| 34 |
+
return new NextResponse(response.data, {
|
| 35 |
+
headers: {
|
| 36 |
+
"Content-Type": contentType,
|
| 37 |
+
"Content-Disposition": contentDisposition || "attachment",
|
| 38 |
+
},
|
| 39 |
+
})
|
| 40 |
+
}
|
| 41 |
+
} catch (error) {
|
| 42 |
+
console.error("Error downloading video:", error)
|
| 43 |
+
return NextResponse.json({ error: "Error al descargar el video" }, { status: 500 })
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
app/page.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import VideoDownloader from "../components/VideoDownloader"
|
| 2 |
+
|
| 3 |
+
export default function Home() {
|
| 4 |
+
return (
|
| 5 |
+
<main className="min-h-screen bg-gradient-to-br from-purple-400 to-indigo-600 flex items-center justify-center p-4">
|
| 6 |
+
<VideoDownloader />
|
| 7 |
+
</main>
|
| 8 |
+
)
|
| 9 |
+
}
|
| 10 |
+
|
components/VideoDownloader.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { useState } from "react"
|
| 4 |
+
import { Input } from "@/components/ui/input"
|
| 5 |
+
import { Button } from "@/components/ui/button"
|
| 6 |
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
| 7 |
+
import { AlertCircle, Download } from "lucide-react"
|
| 8 |
+
import { processUrl } from "../app/actions"
|
| 9 |
+
|
| 10 |
+
interface VideoInfo {
|
| 11 |
+
platform: string
|
| 12 |
+
title: string
|
| 13 |
+
formats: Array<{
|
| 14 |
+
quality: string
|
| 15 |
+
extension: string
|
| 16 |
+
url: string
|
| 17 |
+
}>
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export default function VideoDownloader() {
|
| 21 |
+
const [url, setUrl] = useState("")
|
| 22 |
+
const [result, setResult] = useState<VideoInfo | null>(null)
|
| 23 |
+
const [error, setError] = useState<string | null>(null)
|
| 24 |
+
|
| 25 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 26 |
+
e.preventDefault()
|
| 27 |
+
setError(null)
|
| 28 |
+
setResult(null)
|
| 29 |
+
try {
|
| 30 |
+
const data = await processUrl(url)
|
| 31 |
+
if ("error" in data) {
|
| 32 |
+
setError(data.error)
|
| 33 |
+
} else {
|
| 34 |
+
setResult(data as VideoInfo)
|
| 35 |
+
}
|
| 36 |
+
} catch (err) {
|
| 37 |
+
setError("Ocurrió un error al procesar la URL.")
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
return (
|
| 42 |
+
<Card className="w-full max-w-md">
|
| 43 |
+
<CardHeader>
|
| 44 |
+
<CardTitle className="text-2xl font-bold text-center">Descargador de Videos</CardTitle>
|
| 45 |
+
</CardHeader>
|
| 46 |
+
<CardContent>
|
| 47 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
| 48 |
+
<Input
|
| 49 |
+
type="url"
|
| 50 |
+
placeholder="Ingrese la URL del video"
|
| 51 |
+
value={url}
|
| 52 |
+
onChange={(e) => setUrl(e.target.value)}
|
| 53 |
+
required
|
| 54 |
+
/>
|
| 55 |
+
<Button type="submit" className="w-full">
|
| 56 |
+
Analizar URL
|
| 57 |
+
</Button>
|
| 58 |
+
</form>
|
| 59 |
+
|
| 60 |
+
{error && (
|
| 61 |
+
<div className="mt-4 p-4 bg-red-100 text-red-700 rounded-md flex items-center">
|
| 62 |
+
<AlertCircle className="mr-2" />
|
| 63 |
+
<p>{error}</p>
|
| 64 |
+
</div>
|
| 65 |
+
)}
|
| 66 |
+
|
| 67 |
+
{result && (
|
| 68 |
+
<div className="mt-4 space-y-4">
|
| 69 |
+
<h3 className="font-semibold">Opciones de descarga para {result.title}:</h3>
|
| 70 |
+
{result.formats.map((format, index) => (
|
| 71 |
+
<Button
|
| 72 |
+
key={index}
|
| 73 |
+
variant="outline"
|
| 74 |
+
className="w-full justify-between"
|
| 75 |
+
onClick={() => window.open(format.url, "_blank")}
|
| 76 |
+
>
|
| 77 |
+
<span>
|
| 78 |
+
{format.quality} - {format.extension}
|
| 79 |
+
</span>
|
| 80 |
+
<Download size={20} />
|
| 81 |
+
</Button>
|
| 82 |
+
))}
|
| 83 |
+
</div>
|
| 84 |
+
)}
|
| 85 |
+
</CardContent>
|
| 86 |
+
</Card>
|
| 87 |
+
)
|
| 88 |
+
}
|
| 89 |
+
|
package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "video-downloader",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"start": "node server.js"
|
| 7 |
+
},
|
| 8 |
+
"dependencies": {
|
| 9 |
+
"axios": "^1.4.0",
|
| 10 |
+
"express": "^4.18.2",
|
| 11 |
+
"ytdl-core": "^4.11.5"
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
|
public/index.html
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Descargador de Videos</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
</head>
|
| 9 |
+
<body class="bg-gradient-to-br from-purple-400 to-indigo-600 min-h-screen flex items-center justify-center p-4">
|
| 10 |
+
<div class="bg-white rounded-lg shadow-md w-full max-w-md p-6">
|
| 11 |
+
<h1 class="text-2xl font-bold text-center mb-6">Descargador de Videos</h1>
|
| 12 |
+
<form id="downloadForm" class="space-y-4">
|
| 13 |
+
<input type="url" id="videoUrl" placeholder="Ingrese la URL del video" required
|
| 14 |
+
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
|
| 15 |
+
<button type="submit" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
| 16 |
+
Analizar URL
|
| 17 |
+
</button>
|
| 18 |
+
</form>
|
| 19 |
+
<div id="result" class="mt-4 space-y-4 hidden">
|
| 20 |
+
<h3 class="font-semibold">Opciones de descarga:</h3>
|
| 21 |
+
<div id="downloadOptions"></div>
|
| 22 |
+
</div>
|
| 23 |
+
<div id="error" class="mt-4 p-4 bg-red-100 text-red-700 rounded-md hidden"></div>
|
| 24 |
+
</div>
|
| 25 |
+
<script>
|
| 26 |
+
document.getElementById('downloadForm').addEventListener('submit', async (e) => {
|
| 27 |
+
e.preventDefault();
|
| 28 |
+
const url = document.getElementById('videoUrl').value;
|
| 29 |
+
const resultDiv = document.getElementById('result');
|
| 30 |
+
const errorDiv = document.getElementById('error');
|
| 31 |
+
const downloadOptions = document.getElementById('downloadOptions');
|
| 32 |
+
|
| 33 |
+
resultDiv.classList.add('hidden');
|
| 34 |
+
errorDiv.classList.add('hidden');
|
| 35 |
+
downloadOptions.innerHTML = '';
|
| 36 |
+
|
| 37 |
+
try {
|
| 38 |
+
const response = await fetch(`/api/info?url=${encodeURIComponent(url)}`);
|
| 39 |
+
const data = await response.json();
|
| 40 |
+
|
| 41 |
+
if (data.error) {
|
| 42 |
+
throw new Error(data.error);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
resultDiv.classList.remove('hidden');
|
| 46 |
+
data.formats.forEach(format => {
|
| 47 |
+
const button = document.createElement('button');
|
| 48 |
+
button.className = 'w-full bg-gray-200 text-gray-800 py-2 px-4 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 mb-2 flex justify-between items-center';
|
| 49 |
+
button.innerHTML = `
|
| 50 |
+
<span>${format.quality} - ${format.extension}</span>
|
| 51 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
| 52 |
+
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
| 53 |
+
</svg>
|
| 54 |
+
`;
|
| 55 |
+
button.onclick = () => window.open(format.url, '_blank');
|
| 56 |
+
downloadOptions.appendChild(button);
|
| 57 |
+
});
|
| 58 |
+
} catch (error) {
|
| 59 |
+
errorDiv.classList.remove('hidden');
|
| 60 |
+
errorDiv.textContent = error.message;
|
| 61 |
+
}
|
| 62 |
+
});
|
| 63 |
+
</script>
|
| 64 |
+
</body>
|
| 65 |
+
</html>
|
| 66 |
+
|
server.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require("express")
|
| 2 |
+
const path = require("path")
|
| 3 |
+
const ytdl = require("ytdl-core")
|
| 4 |
+
const axios = require("axios")
|
| 5 |
+
|
| 6 |
+
const app = express()
|
| 7 |
+
const port = process.env.PORT || 7860 // Hugging Face uses port 7860 by default
|
| 8 |
+
|
| 9 |
+
app.use(express.static(path.join(__dirname, "public")))
|
| 10 |
+
|
| 11 |
+
app.get("/api/info", async (req, res) => {
|
| 12 |
+
const { url } = req.query
|
| 13 |
+
|
| 14 |
+
if (!url) {
|
| 15 |
+
return res.status(400).json({ error: "URL no proporcionada" })
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
try {
|
| 19 |
+
if (url.includes("youtube.com") || url.includes("youtu.be")) {
|
| 20 |
+
const info = await ytdl.getInfo(url)
|
| 21 |
+
res.json({
|
| 22 |
+
platform: "YouTube",
|
| 23 |
+
title: info.videoDetails.title,
|
| 24 |
+
formats: [
|
| 25 |
+
{
|
| 26 |
+
quality: "Alta calidad (720p)",
|
| 27 |
+
extension: "mp4",
|
| 28 |
+
url: `/api/download?url=${encodeURIComponent(url)}&format=mp4`,
|
| 29 |
+
},
|
| 30 |
+
{ quality: "Audio (MP3)", extension: "mp3", url: `/api/download?url=${encodeURIComponent(url)}&format=mp3` },
|
| 31 |
+
],
|
| 32 |
+
})
|
| 33 |
+
} else if (url.includes("facebook.com")) {
|
| 34 |
+
res.json({
|
| 35 |
+
platform: "Facebook",
|
| 36 |
+
title: "Video de Facebook",
|
| 37 |
+
formats: [{ quality: "Original", extension: "mp4", url: `/api/download?url=${encodeURIComponent(url)}` }],
|
| 38 |
+
})
|
| 39 |
+
} else if (url.includes("twitter.com")) {
|
| 40 |
+
res.json({
|
| 41 |
+
platform: "Twitter",
|
| 42 |
+
title: "Video de Twitter",
|
| 43 |
+
formats: [{ quality: "Original", extension: "mp4", url: `/api/download?url=${encodeURIComponent(url)}` }],
|
| 44 |
+
})
|
| 45 |
+
} else {
|
| 46 |
+
res.status(400).json({ error: "Plataforma no soportada" })
|
| 47 |
+
}
|
| 48 |
+
} catch (error) {
|
| 49 |
+
console.error("Error processing URL:", error)
|
| 50 |
+
res.status(500).json({ error: "Error al procesar la URL" })
|
| 51 |
+
}
|
| 52 |
+
})
|
| 53 |
+
|
| 54 |
+
app.get("/api/download", async (req, res) => {
|
| 55 |
+
const { url, format } = req.query
|
| 56 |
+
|
| 57 |
+
if (!url) {
|
| 58 |
+
return res.status(400).json({ error: "URL no proporcionada" })
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
try {
|
| 62 |
+
if (url.includes("youtube.com") || url.includes("youtu.be")) {
|
| 63 |
+
const info = await ytdl.getInfo(url)
|
| 64 |
+
const format_id = format === "mp3" ? "audioonly" : "highest"
|
| 65 |
+
const format_info = ytdl.chooseFormat(info.formats, { quality: format_id })
|
| 66 |
+
|
| 67 |
+
res.header(
|
| 68 |
+
"Content-Disposition",
|
| 69 |
+
`attachment; filename="${info.videoDetails.title}.${format === "mp3" ? "mp3" : "mp4"}"`,
|
| 70 |
+
)
|
| 71 |
+
res.header("Content-Type", format === "mp3" ? "audio/mpeg" : "video/mp4")
|
| 72 |
+
|
| 73 |
+
ytdl(url, { format: format_info }).pipe(res)
|
| 74 |
+
} else {
|
| 75 |
+
const response = await axios.get(url, { responseType: "stream" })
|
| 76 |
+
res.header("Content-Type", response.headers["content-type"])
|
| 77 |
+
res.header("Content-Disposition", response.headers["content-disposition"] || "attachment")
|
| 78 |
+
response.data.pipe(res)
|
| 79 |
+
}
|
| 80 |
+
} catch (error) {
|
| 81 |
+
console.error("Error downloading video:", error)
|
| 82 |
+
res.status(500).json({ error: "Error al descargar el video" })
|
| 83 |
+
}
|
| 84 |
+
})
|
| 85 |
+
|
| 86 |
+
app.listen(port, () => {
|
| 87 |
+
console.log(`Server is running on port ${port}`)
|
| 88 |
+
})
|
| 89 |
+
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
module.exports = {
|
| 3 |
+
darkMode: ["class"],
|
| 4 |
+
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
|
| 5 |
+
theme: {
|
| 6 |
+
container: {
|
| 7 |
+
center: true,
|
| 8 |
+
padding: "2rem",
|
| 9 |
+
screens: {
|
| 10 |
+
"2xl": "1400px",
|
| 11 |
+
},
|
| 12 |
+
},
|
| 13 |
+
extend: {
|
| 14 |
+
colors: {
|
| 15 |
+
border: "hsl(var(--border))",
|
| 16 |
+
input: "hsl(var(--input))",
|
| 17 |
+
ring: "hsl(var(--ring))",
|
| 18 |
+
background: "hsl(var(--background))",
|
| 19 |
+
foreground: "hsl(var(--foreground))",
|
| 20 |
+
primary: {
|
| 21 |
+
DEFAULT: "hsl(var(--primary))",
|
| 22 |
+
foreground: "hsl(var(--primary-foreground))",
|
| 23 |
+
},
|
| 24 |
+
secondary: {
|
| 25 |
+
DEFAULT: "hsl(var(--secondary))",
|
| 26 |
+
foreground: "hsl(var(--secondary-foreground))",
|
| 27 |
+
},
|
| 28 |
+
destructive: {
|
| 29 |
+
DEFAULT: "hsl(var(--destructive))",
|
| 30 |
+
foreground: "hsl(var(--destructive-foreground))",
|
| 31 |
+
},
|
| 32 |
+
muted: {
|
| 33 |
+
DEFAULT: "hsl(var(--muted))",
|
| 34 |
+
foreground: "hsl(var(--muted-foreground))",
|
| 35 |
+
},
|
| 36 |
+
accent: {
|
| 37 |
+
DEFAULT: "hsl(var(--accent))",
|
| 38 |
+
foreground: "hsl(var(--accent-foreground))",
|
| 39 |
+
},
|
| 40 |
+
popover: {
|
| 41 |
+
DEFAULT: "hsl(var(--popover))",
|
| 42 |
+
foreground: "hsl(var(--popover-foreground))",
|
| 43 |
+
},
|
| 44 |
+
card: {
|
| 45 |
+
DEFAULT: "hsl(var(--card))",
|
| 46 |
+
foreground: "hsl(var(--card-foreground))",
|
| 47 |
+
},
|
| 48 |
+
},
|
| 49 |
+
borderRadius: {
|
| 50 |
+
lg: "var(--radius)",
|
| 51 |
+
md: "calc(var(--radius) - 2px)",
|
| 52 |
+
sm: "calc(var(--radius) - 4px)",
|
| 53 |
+
},
|
| 54 |
+
keyframes: {
|
| 55 |
+
"accordion-down": {
|
| 56 |
+
from: { height: 0 },
|
| 57 |
+
to: { height: "var(--radix-accordion-content-height)" },
|
| 58 |
+
},
|
| 59 |
+
"accordion-up": {
|
| 60 |
+
from: { height: "var(--radix-accordion-content-height)" },
|
| 61 |
+
to: { height: 0 },
|
| 62 |
+
},
|
| 63 |
+
},
|
| 64 |
+
animation: {
|
| 65 |
+
"accordion-down": "accordion-down 0.2s ease-out",
|
| 66 |
+
"accordion-up": "accordion-up 0.2s ease-out",
|
| 67 |
+
},
|
| 68 |
+
},
|
| 69 |
+
},
|
| 70 |
+
plugins: [require("tailwindcss-animate")],
|
| 71 |
+
}
|
| 72 |
+
|