Spaces:
Paused
Paused
| import asyncio | |
| import os | |
| import re | |
| import json | |
| from dataclasses import dataclass | |
| from typing import List, Optional, Dict, Any | |
| from urllib.parse import urlparse, parse_qs | |
| import aiohttp | |
| from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | |
| from cryptography.hazmat.backends import default_backend | |
| import base64 | |
| import gradio as gr | |
| class VideoResult: | |
| title: str | |
| type: str | |
| format: str | |
| thumbnail: str | |
| download_url: str | |
| video_id: str | |
| duration: int | |
| quality: str | |
| class SaveTubeDownloader: | |
| API_BASE = "https://media.savetube.me/api" | |
| HEADERS = { | |
| 'accept': '*/*', | |
| 'content-type': 'application/json', | |
| 'origin': 'https://yt.savetube.me', | |
| 'referer': 'https://yt.savetube.me/', | |
| 'user-agent': 'Postify/1.0.0' | |
| } | |
| FORMATS = ['144', '240', '360', '480', '720', '1080', 'mp3'] | |
| SECRET_KEY = 'C5D58EF67A7584E4A29F6C35BBC4EB12' | |
| def extract_youtube_id(url: str) -> Optional[str]: | |
| """Extract YouTube video ID from URL""" | |
| patterns = [ | |
| r'youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})', | |
| r'youtube\.com\/embed\/([a-zA-Z0-9_-]{11})', | |
| r'youtube\.com\/v\/([a-zA-Z0-9_-]{11})', | |
| r'youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})', | |
| r'youtu\.be\/([a-zA-Z0-9_-]{11})' | |
| ] | |
| for pattern in patterns: | |
| match = re.search(pattern, url) | |
| if match: | |
| return match.group(1) | |
| return None | |
| def decrypt_data(encrypted: str) -> Dict[str, Any]: | |
| """Decrypt API response""" | |
| try: | |
| data = base64.b64decode(encrypted) | |
| iv = data[:16] | |
| content = data[16:] | |
| key = bytes.fromhex(SaveTubeDownloader.SECRET_KEY) | |
| cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) | |
| decryptor = cipher.decryptor() | |
| decrypted = decryptor.update(content) + decryptor.finalize() | |
| # Remove padding | |
| padding_length = decrypted[-1] | |
| decrypted = decrypted[:-padding_length] | |
| return json.loads(decrypted.decode()) | |
| except Exception as e: | |
| raise RuntimeError(f"Decryption failed: {e}") | |
| async def get_cdn(self) -> str: | |
| """Get CDN URL""" | |
| async with aiohttp.ClientSession() as session: | |
| async with session.get( | |
| f"{self.API_BASE}/random-cdn", | |
| headers=self.HEADERS | |
| ) as response: | |
| if response.status == 200: | |
| data = await response.json() | |
| return data.get('cdn') | |
| raise RuntimeError("Failed to get CDN") | |
| async def download(self, url: str, format: str = '720') -> VideoResult: | |
| """Download video from YouTube""" | |
| # Validate URL | |
| video_id = self.extract_youtube_id(url) | |
| if not video_id: | |
| raise ValueError("URL YouTube tidak valid. Pastikan URL benar!") | |
| if format not in self.FORMATS: | |
| raise ValueError(f"Format tidak valid. Pilih: {', '.join(self.FORMATS)}") | |
| try: | |
| # Get CDN | |
| cdn = await self.get_cdn() | |
| async with aiohttp.ClientSession() as session: | |
| # Get video info | |
| async with session.post( | |
| f"https://{cdn}/api/v2/info", | |
| json={"url": f"https://www.youtube.com/watch?v={video_id}"}, | |
| headers=self.HEADERS | |
| ) as response: | |
| if response.status != 200: | |
| raise RuntimeError("Gagal mendapatkan info video") | |
| result = await response.json() | |
| decrypted = self.decrypt_data(result['data']) | |
| # Get download link | |
| async with session.post( | |
| f"https://{cdn}/api/download", | |
| json={ | |
| "id": video_id, | |
| "downloadType": "audio" if format == 'mp3' else "video", | |
| "quality": "128" if format == 'mp3' else format, | |
| "key": decrypted['key'] | |
| }, | |
| headers=self.HEADERS | |
| ) as response: | |
| if response.status != 200: | |
| raise RuntimeError("Gagal mendapatkan link download") | |
| dl_result = await response.json() | |
| return VideoResult( | |
| title=decrypted.get('title', 'Unknown'), | |
| type='audio' if format == 'mp3' else 'video', | |
| format=format, | |
| thumbnail=decrypted.get('thumbnail', f'https://i.ytimg.com/vi/{video_id}/maxresdefault.jpg'), | |
| download_url=dl_result['data']['downloadUrl'], | |
| video_id=video_id, | |
| duration=decrypted.get('duration', 0), | |
| quality="128kbps" if format == 'mp3' else f"{format}p" | |
| ) | |
| except Exception as e: | |
| raise RuntimeError(f"Error: {str(e)}") | |
| def format_duration(seconds: int) -> str: | |
| """Format duration to MM:SS or HH:MM:SS""" | |
| hours = seconds // 3600 | |
| minutes = (seconds % 3600) // 60 | |
| secs = seconds % 60 | |
| if hours > 0: | |
| return f"{hours:02d}:{minutes:02d}:{secs:02d}" | |
| return f"{minutes:02d}:{secs:02d}" | |
| async def process_download(url: str, quality: str): | |
| """Process download request""" | |
| if not url.strip(): | |
| return None, None, "β οΈ Masukkan URL video terlebih dahulu!", gr.update(visible=False), gr.update(visible=False) | |
| try: | |
| downloader = SaveTubeDownloader() | |
| result = await downloader.download(url, quality) | |
| # Create result card HTML | |
| duration_str = format_duration(result.duration) | |
| type_icon = "π΅" if result.type == "audio" else "π¬" | |
| type_badge = "AUDIO" if result.type == "audio" else "VIDEO" | |
| result_html = f""" | |
| <div style='background: white; border-radius: 16px; padding: 24px; box-shadow: 0 4px 20px rgba(0,0,0,0.08);'> | |
| <div style='display: flex; gap: 20px; align-items: start;'> | |
| <img src='{result.thumbnail}' | |
| style='width: 200px; height: 112px; object-fit: cover; border-radius: 12px; flex-shrink: 0;' | |
| onerror="this.src='https://via.placeholder.com/200x112?text=No+Image'"> | |
| <div style='flex: 1;'> | |
| <div style='display: flex; align-items: center; gap: 8px; margin-bottom: 12px;'> | |
| <span style='font-size: 24px;'>{type_icon}</span> | |
| <span style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; padding: 4px 12px; border-radius: 6px; | |
| font-size: 12px; font-weight: 600;'>{type_badge}</span> | |
| <span style='background: #f3f4f6; color: #374151; padding: 4px 12px; | |
| border-radius: 6px; font-size: 12px; font-weight: 600;'>{result.quality}</span> | |
| </div> | |
| <h3 style='margin: 0 0 12px 0; font-size: 18px; color: #1f2937; line-height: 1.4;'>{result.title}</h3> | |
| <div style='display: flex; gap: 16px; color: #6b7280; font-size: 14px; margin-bottom: 16px;'> | |
| <span>β±οΈ {duration_str}</span> | |
| <span>π¦ {result.format.upper()}</span> | |
| <span>π {result.video_id}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| success_msg = f""" | |
| <div style='padding: 20px; background: linear-gradient(135deg, #10b981 0%, #059669 100%); | |
| border-radius: 12px; color: white; text-align: center; box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);'> | |
| <h3 style='margin: 0; font-size: 24px;'>β Siap Download!</h3> | |
| <p style='margin: 10px 0 0 0; font-size: 16px;'>Klik tombol download di bawah untuk menyimpan file.</p> | |
| </div> | |
| """ | |
| return result_html, result.download_url, success_msg, gr.update(visible=True), gr.update(visible=True, link=result.download_url) | |
| except Exception as e: | |
| error_msg = f""" | |
| <div style='padding: 20px; background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); | |
| border-radius: 12px; color: white; text-align: center; box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);'> | |
| <h3 style='margin: 0; font-size: 24px;'>β Terjadi Kesalahan</h3> | |
| <p style='margin: 10px 0 0 0; font-size: 14px;'>{str(e)}</p> | |
| </div> | |
| """ | |
| return None, None, error_msg, gr.update(visible=False), gr.update(visible=False) | |
| def run_download(url, quality): | |
| return asyncio.run(process_download(url, quality)) | |
| # ----------- Custom CSS ----------- | |
| custom_css = """ | |
| :root { | |
| --primary: #667eea; | |
| --primary-dark: #764ba2; | |
| --success: #10b981; | |
| --danger: #ef4444; | |
| } | |
| #app-container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| #header { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); | |
| padding: 48px 32px; | |
| border-radius: 20px; | |
| margin-bottom: 32px; | |
| text-align: center; | |
| color: white; | |
| box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| #header::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); | |
| animation: pulse 4s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); opacity: 0.5; } | |
| 50% { transform: scale(1.1); opacity: 0.8; } | |
| } | |
| #header-content { | |
| position: relative; | |
| z-index: 1; | |
| } | |
| #header h1 { | |
| font-size: 3em; | |
| margin: 0 0 12px 0; | |
| font-weight: 800; | |
| text-shadow: 2px 4px 8px rgba(0,0,0,0.2); | |
| } | |
| #header p { | |
| font-size: 1.2em; | |
| margin: 0; | |
| opacity: 0.95; | |
| font-weight: 500; | |
| } | |
| .card { | |
| background: white; | |
| border-radius: 16px; | |
| padding: 28px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08); | |
| margin-bottom: 24px; | |
| } | |
| .card-title { | |
| font-size: 1.3em; | |
| font-weight: 700; | |
| color: #1f2937; | |
| margin-bottom: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| #url_input input { | |
| border-radius: 12px !important; | |
| border: 2px solid #e5e7eb !important; | |
| padding: 14px 16px !important; | |
| font-size: 15px !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| #url_input input:focus { | |
| border-color: var(--primary) !important; | |
| box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1) !important; | |
| } | |
| .quality-radio { | |
| display: flex; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .gr-button-primary { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%) !important; | |
| border: none !important; | |
| border-radius: 12px !important; | |
| padding: 14px 32px !important; | |
| font-weight: 700 !important; | |
| font-size: 16px !important; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3) !important; | |
| color: white !important; | |
| } | |
| .gr-button-primary:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4) !important; | |
| } | |
| .gr-button-secondary { | |
| background: linear-gradient(135deg, var(--success) 0%, #059669 100%) !important; | |
| border: none !important; | |
| border-radius: 12px !important; | |
| padding: 14px 32px !important; | |
| font-weight: 700 !important; | |
| font-size: 16px !important; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3) !important; | |
| color: white !important; | |
| } | |
| .gr-button-secondary:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4) !important; | |
| } | |
| #info-box { | |
| background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); | |
| padding: 20px; | |
| border-radius: 12px; | |
| border-left: 5px solid #f59e0b; | |
| margin-bottom: 24px; | |
| color: #78350f; | |
| box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2); | |
| } | |
| #feature-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 20px; | |
| margin: 32px 0; | |
| } | |
| .feature-card { | |
| background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); | |
| padding: 24px; | |
| border-radius: 12px; | |
| border: 2px solid #e2e8f0; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| } | |
| .feature-card:hover { | |
| transform: translateY(-4px); | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.1); | |
| border-color: var(--primary); | |
| } | |
| .feature-icon { | |
| font-size: 3em; | |
| margin-bottom: 12px; | |
| } | |
| .feature-title { | |
| font-weight: 700; | |
| color: #1f2937; | |
| margin-bottom: 8px; | |
| } | |
| .feature-desc { | |
| color: #6b7280; | |
| font-size: 0.9em; | |
| line-height: 1.5; | |
| } | |
| #footer { | |
| text-align: center; | |
| padding: 32px 20px; | |
| margin-top: 48px; | |
| color: #9ca3af; | |
| border-top: 2px solid #f3f4f6; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .animate-in { | |
| animation: fadeIn 0.6s ease-out; | |
| } | |
| """ | |
| # ----------- Gradio UI ----------- | |
| with gr.Blocks( | |
| title="π¬ YouTube Downloader β SaveTube API", | |
| theme=gr.themes.Soft( | |
| primary_hue="violet", | |
| secondary_hue="purple", | |
| neutral_hue="slate", | |
| font=gr.themes.GoogleFont("Inter"), | |
| radius_size="lg" | |
| ), | |
| css=custom_css | |
| ) as demo: | |
| # Header | |
| gr.HTML(""" | |
| <div id="header" class="animate-in"> | |
| <div id="header-content"> | |
| <h1>π¬ YouTube Downloader</h1> | |
| <p>Download Video & Audio YouTube dengan Kualitas Terbaik</p> | |
| </div> | |
| </div> | |
| """) | |
| # Info Box | |
| gr.HTML(""" | |
| <div id="info-box" class="animate-in"> | |
| <strong>β οΈ Penting:</strong> Tool ini hanya untuk penggunaan pribadi dan konten yang Anda miliki haknya. | |
| Menghargai hak cipta adalah tanggung jawab kita bersama. | |
| </div> | |
| """) | |
| # Main Content | |
| with gr.Column(elem_id="app-container"): | |
| # Input Section | |
| with gr.Group(elem_classes="card"): | |
| gr.HTML("<div class='card-title'>π Input URL Video</div>") | |
| url_input = gr.Textbox( | |
| label="", | |
| placeholder="Paste URL YouTube di sini (contoh: https://www.youtube.com/watch?v=dQw4w9WgXcQ)", | |
| elem_id="url_input", | |
| scale=1, | |
| show_label=False | |
| ) | |
| # Quality Selection | |
| with gr.Group(elem_classes="card"): | |
| gr.HTML("<div class='card-title'>βοΈ Pilih Kualitas</div>") | |
| with gr.Row(): | |
| quality_selector = gr.Radio( | |
| choices=[ | |
| ("π΅ Audio MP3 (128kbps)", "mp3"), | |
| ("π± 144p", "144"), | |
| ("π± 240p", "240"), | |
| ("π» 360p", "360"), | |
| ("π» 480p", "480"), | |
| ("π₯οΈ HD 720p", "720"), | |
| ("π₯οΈ Full HD 1080p", "1080") | |
| ], | |
| value="720", | |
| label="", | |
| show_label=False, | |
| elem_classes="quality-radio" | |
| ) | |
| # Action Button | |
| with gr.Row(): | |
| process_btn = gr.Button( | |
| "π Proses & Ambil Link Download", | |
| variant="primary", | |
| size="lg", | |
| scale=1 | |
| ) | |
| # Status Message | |
| status_msg = gr.HTML() | |
| # Result Section - Simplify by removing the Column wrapper | |
| result_card = gr.HTML(visible=False) | |
| download_btn = gr.Button( | |
| "β¬οΈ Download File", | |
| variant="secondary", | |
| size="lg", | |
| link=None, | |
| visible=False | |
| ) | |
| # Features Grid | |
| gr.HTML(""" | |
| <div id="feature-grid" class="animate-in"> | |
| <div class="feature-card"> | |
| <div class="feature-icon">β‘</div> | |
| <div class="feature-title">Super Cepat</div> | |
| <div class="feature-desc">Proses download yang cepat dan efisien tanpa batasan</div> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon">π―</div> | |
| <div class="feature-title">Multi Kualitas</div> | |
| <div class="feature-desc">Pilih dari 144p hingga Full HD 1080p sesuai kebutuhan</div> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon">π΅</div> | |
| <div class="feature-title">Audio MP3</div> | |
| <div class="feature-desc">Extract audio berkualitas tinggi dalam format MP3</div> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon">π</div> | |
| <div class="feature-title">Aman & Private</div> | |
| <div class="feature-desc">Tidak menyimpan data atau riwayat download Anda</div> | |
| </div> | |
| </div> | |
| """) | |
| # Footer | |
| gr.HTML(""" | |
| <div id="footer"> | |
| <p style="font-size: 1.1em; margin-bottom: 8px;"> | |
| π‘ <strong>Tips:</strong> Untuk hasil terbaik, gunakan URL langsung dari YouTube | |
| </p> | |
| <p style="opacity: 0.7;"> | |
| Powered by SaveTube API β’ Made with β€οΈ using Gradio | |
| </p> | |
| </div> | |
| """) | |
| # Event Handler - SINGLE function call | |
| process_btn.click( | |
| fn=run_download, | |
| inputs=[url_input, quality_selector], | |
| outputs=[result_card, download_btn, status_msg, result_card, download_btn] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=int(os.getenv("PORT", 7860)), | |
| show_api=False | |
| ) |