Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>YouTube → MP3 Downloader</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #7c3aed; | |
| --primary-dark: #6d28d9; | |
| --secondary: #06b6d4; | |
| --accent: #f472b6; | |
| --background: #0f172a; | |
| --surface: #1e293b; | |
| --text: #f8fafc; | |
| --text-secondary: #94a3b8; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif; | |
| min-height: 100vh; | |
| background: var(--background); | |
| color: var(--text); | |
| line-height: 1.6; | |
| overflow-x: hidden; | |
| } | |
| .noise { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); | |
| opacity: 0.05; | |
| pointer-events: none; | |
| z-index: 1; | |
| } | |
| .gradient-bg { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: radial-gradient(circle at 50% 50%, rgba(124, 58, 237, 0.15), transparent 50%), | |
| radial-gradient(circle at 0% 0%, rgba(6, 182, 212, 0.15), transparent 50%), | |
| radial-gradient(circle at 100% 0%, rgba(244, 114, 182, 0.15), transparent 50%); | |
| z-index: 0; | |
| } | |
| .main-container { | |
| position: relative; | |
| z-index: 2; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 2rem; | |
| } | |
| .content-wrapper { | |
| width: 100%; | |
| max-width: 1200px; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 4rem; | |
| align-items: center; | |
| } | |
| .hero-section { | |
| text-align: left; | |
| } | |
| .hero-title { | |
| font-size: 4rem; | |
| font-weight: 700; | |
| line-height: 1.1; | |
| margin-bottom: 1.5rem; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| animation: titleFloat 6s ease-in-out infinite; | |
| } | |
| @keyframes titleFloat { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-10px); } | |
| } | |
| .hero-subtitle { | |
| font-size: 1.25rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 2rem; | |
| max-width: 500px; | |
| } | |
| .features { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 1.5rem; | |
| margin-top: 3rem; | |
| } | |
| .feature-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| padding: 1rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 12px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .feature-item:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); | |
| } | |
| .feature-icon { | |
| width: 40px; | |
| height: 40px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| border-radius: 10px; | |
| color: white; | |
| } | |
| .feature-text { | |
| font-size: 0.95rem; | |
| color: var(--text-secondary); | |
| } | |
| .converter-section { | |
| background: rgba(255, 255, 255, 0.02); | |
| backdrop-filter: blur(20px); | |
| border-radius: 24px; | |
| padding: 2.5rem; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); | |
| transition: transform 0.3s ease; | |
| } | |
| .converter-section:hover { | |
| transform: translateY(-5px); | |
| } | |
| .converter-title { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| margin-bottom: 1.5rem; | |
| color: var(--text); | |
| } | |
| .input-group { | |
| position: relative; | |
| margin-bottom: 1.5rem; | |
| } | |
| .input-label { | |
| display: block; | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.5rem; | |
| } | |
| input[type="text"] { | |
| width: 100%; | |
| padding: 1rem 1.25rem; | |
| font-size: 1rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 12px; | |
| color: var(--text); | |
| transition: all 0.3s ease; | |
| } | |
| input[type="text"]:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.1); | |
| } | |
| input[type="text"]::placeholder { | |
| color: var(--text-secondary); | |
| opacity: 0.7; | |
| } | |
| .submit-btn { | |
| width: 100%; | |
| padding: 1rem; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: white; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| border: none; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.75rem; | |
| } | |
| .submit-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(124, 58, 237, 0.3); | |
| } | |
| .submit-btn:disabled { | |
| opacity: 0.7; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .preview-card { | |
| margin-top: 1.5rem; | |
| padding: 1.5rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 16px; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| display: none; | |
| } | |
| .preview-card.visible { | |
| display: block; | |
| animation: slideUp 0.5s ease forwards; | |
| } | |
| @keyframes slideUp { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .preview-content { | |
| display: flex; | |
| gap: 1.5rem; | |
| align-items: center; | |
| } | |
| .thumbnail { | |
| width: 160px; | |
| height: 90px; | |
| border-radius: 12px; | |
| object-fit: cover; | |
| } | |
| .video-info { | |
| flex: 1; | |
| } | |
| .video-title { | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| color: var(--text); | |
| } | |
| .video-meta { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| } | |
| .loading-state { | |
| display: none; | |
| margin-top: 1.5rem; | |
| padding: 1.5rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 16px; | |
| text-align: center; | |
| } | |
| .loading-state.visible { | |
| display: block; | |
| animation: fadeIn 0.3s ease forwards; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 4px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin-top: 1rem; | |
| } | |
| .progress-bar::after { | |
| content: ''; | |
| display: block; | |
| width: 30%; | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| animation: progress 1.5s ease-in-out infinite; | |
| } | |
| @keyframes progress { | |
| 0% { transform: translateX(-100%); } | |
| 100% { transform: translateX(400%); } | |
| } | |
| .download-link { | |
| display: none; | |
| margin-top: 1.5rem; | |
| padding: 1rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 12px; | |
| text-align: center; | |
| animation: slideUp 0.5s ease forwards; | |
| } | |
| .download-link.visible { | |
| display: block; | |
| } | |
| .download-link a { | |
| color: var(--primary); | |
| text-decoration: none; | |
| font-weight: 600; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| transition: color 0.3s ease; | |
| } | |
| .download-link a:hover { | |
| color: var(--secondary); | |
| } | |
| .error-message { | |
| display: none; | |
| margin-top: 1rem; | |
| padding: 1rem; | |
| background: rgba(239, 68, 68, 0.1); | |
| border: 1px solid rgba(239, 68, 68, 0.2); | |
| border-radius: 12px; | |
| color: #ef4444; | |
| font-size: 0.875rem; | |
| } | |
| .error-message.visible { | |
| display: block; | |
| animation: shake 0.5s ease-in-out; | |
| } | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 25% { transform: translateX(-5px); } | |
| 75% { transform: translateX(5px); } | |
| } | |
| @media (max-width: 1024px) { | |
| .content-wrapper { | |
| grid-template-columns: 1fr; | |
| gap: 3rem; | |
| } | |
| .hero-section { | |
| text-align: center; | |
| } | |
| .hero-subtitle { | |
| margin: 0 auto 2rem; | |
| } | |
| .features { | |
| max-width: 600px; | |
| margin: 3rem auto 0; | |
| } | |
| } | |
| @media (max-width: 640px) { | |
| .hero-title { | |
| font-size: 2.5rem; | |
| } | |
| .features { | |
| grid-template-columns: 1fr; | |
| } | |
| .converter-section { | |
| padding: 1.5rem; | |
| } | |
| .preview-content { | |
| flex-direction: column; | |
| text-align: center; | |
| } | |
| .thumbnail { | |
| width: 100%; | |
| height: auto; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="noise"></div> | |
| <div class="gradient-bg"></div> | |
| <main class="main-container"> | |
| <div class="content-wrapper"> | |
| <section class="hero-section"> | |
| <h1 class="hero-title">Convert YouTube to MP3</h1> | |
| <p class="hero-subtitle"> | |
| Transform your favorite YouTube videos into high-quality MP3 files instantly. | |
| Fast, free, and easy to use. | |
| </p> | |
| <div class="features"> | |
| <div class="feature-item"> | |
| <div class="feature-icon"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 2v20M2 12h20"/> | |
| </svg> | |
| </div> | |
| <span class="feature-text">High Quality Audio</span> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 2v20M2 12h20"/> | |
| </svg> | |
| </div> | |
| <span class="feature-text">Fast Conversion</span> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 2v20M2 12h20"/> | |
| </svg> | |
| </div> | |
| <span class="feature-text">No Registration</span> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 2v20M2 12h20"/> | |
| </svg> | |
| </div> | |
| <span class="feature-text">Unlimited Downloads</span> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="converter-section"> | |
| <h2 class="converter-title">Convert Now</h2> | |
| <form id="download-form"> | |
| <div class="input-group"> | |
| <label class="input-label">YouTube URL or Video ID</label> | |
| <input type="text" | |
| id="video-id" | |
| placeholder="https://www.youtube.com/watch?v=..." | |
| required> | |
| </div> | |
| <button type="submit" class="submit-btn" id="submit-btn"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> | |
| <polyline points="7 10 12 15 17 10"/> | |
| <line x1="12" y1="15" x2="12" y2="3"/> | |
| </svg> | |
| Convert to MP3 | |
| </button> | |
| </form> | |
| <div class="preview-card" id="preview"> | |
| <div class="preview-content"> | |
| <img id="thumbnail" class="thumbnail" src="" alt="Video thumbnail"> | |
| <div class="video-info"> | |
| <div id="video-title" class="video-title"></div> | |
| <div id="video-duration" class="video-meta"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="loading-state" id="loading"> | |
| <div class="progress-bar"></div> | |
| <p>Converting your video...</p> | |
| </div> | |
| <div class="download-link" id="result"></div> | |
| <div class="error-message" id="error"></div> | |
| </section> | |
| </div> | |
| </main> | |
| <script> | |
| const form = document.getElementById('download-form'); | |
| const btn = document.getElementById('submit-btn'); | |
| const preview = document.getElementById('preview'); | |
| const thumbnail = document.getElementById('thumbnail'); | |
| const videoTitle = document.getElementById('video-title'); | |
| const videoDuration = document.getElementById('video-duration'); | |
| const loadingEl = document.getElementById('loading'); | |
| const resultEl = document.getElementById('result'); | |
| const errorEl = document.getElementById('error'); | |
| form.addEventListener('submit', async e => { | |
| e.preventDefault(); | |
| const input = document.getElementById('video-id').value.trim(); | |
| if (!input) return; | |
| // Reset states | |
| preview.classList.remove('visible'); | |
| loadingEl.classList.remove('visible'); | |
| resultEl.classList.remove('visible'); | |
| errorEl.classList.remove('visible'); | |
| btn.disabled = true; | |
| // Extract video ID | |
| let videoId; | |
| if (input.includes('youtube.com/watch')) { | |
| const urlParams = new URLSearchParams(new URL(input).search); | |
| videoId = urlParams.get('v'); | |
| } else if (input.includes('youtu.be/')) { | |
| videoId = input.split('youtu.be/')[1].split('?')[0]; | |
| } else { | |
| // Assume it's just the video ID | |
| videoId = input.split('?')[0].split('&')[0]; | |
| } | |
| if (!videoId) { | |
| errorEl.textContent = 'Invalid YouTube URL or video ID'; | |
| errorEl.classList.add('visible'); | |
| btn.disabled = false; | |
| return; | |
| } | |
| try { | |
| // Show loading state | |
| loadingEl.classList.add('visible'); | |
| // Call backend | |
| const resp = await fetch(`/api/download?videoId=${encodeURIComponent(videoId)}`, { | |
| method: 'GET', | |
| headers: { 'Accept': 'application/json' } | |
| }); | |
| const data = await resp.json(); | |
| loadingEl.classList.remove('visible'); | |
| btn.disabled = false; | |
| if (!resp.ok) { | |
| throw new Error(`Server returned ${resp.status}`); | |
| } | |
| if (data.status === 'ok') { | |
| // 1️⃣ Sanitize + truncate video title for a safe filename | |
| const rawTitle = data.title || 'download'; | |
| const safeTitle = rawTitle | |
| .replace(/[\/\\:\*\?"<>\|]/g, '') // strip illegal chars | |
| .trim() | |
| .substring(0, 40); // cap length at 40 chars | |
| // 2️⃣ Fetch the MP3 bytes into a Blob | |
| fetch(data.link) | |
| .then(res => { | |
| if (!res.ok) throw new Error('Network response was not ok'); | |
| return res.blob(); | |
| }) | |
| .then(blob => { | |
| // 3️⃣ Create a same-origin URL and force download name | |
| const blobUrl = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = blobUrl; | |
| a.download = `${safeTitle}.mp3`; | |
| document.body.appendChild(a); // needed for Firefox | |
| a.click(); | |
| a.remove(); | |
| URL.revokeObjectURL(blobUrl); | |
| }) | |
| .catch(err => { | |
| console.error(err); | |
| alert('Download failed: ' + err.message); | |
| }); | |
| } | |
| else { | |
| errorEl.textContent = `Error: ${data.msg || 'Unknown error'}`; | |
| errorEl.classList.add('visible'); | |
| } | |
| } catch (err) { | |
| loadingEl.classList.remove('visible'); | |
| btn.disabled = false; | |
| errorEl.textContent = `Error: ${err.message}`; | |
| errorEl.classList.add('visible'); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |