tebicap commited on
Commit
dd3d0dc
·
verified ·
1 Parent(s): 9c50e42

Nueva version

Browse files
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
+