Spaces:
Runtime error
Runtime error
| <html lang="en" data-theme="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>API Guide - YTDL</title> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --red: #ff2d2d; | |
| --red-dark: #cc0000; | |
| --radius: 16px; | |
| --radius-sm: 10px; | |
| --radius-xs: 6px; | |
| --transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1); | |
| --font: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| --mono: 'SF Mono', 'Cascadia Code', monospace; | |
| } | |
| [data-theme="dark"] { | |
| --bg: #0a0a0f; | |
| --bg-card: #16161f; | |
| --bg-input: #1a1a26; | |
| --border: #2a2a3a; | |
| --text: #f0f0f5; | |
| --text-secondary: #8888a0; | |
| --text-muted: #55556a; | |
| --green: #22c55e; | |
| --yellow: #f59e0b; | |
| --blue: #3b82f6; | |
| --shadow: 0 8px 32px rgba(0,0,0,0.4); | |
| } | |
| [data-theme="light"] { | |
| --bg: #f5f5fa; | |
| --bg-card: #ffffff; | |
| --bg-input: #f0f0f5; | |
| --border: #e0e0ea; | |
| --text: #111118; | |
| --text-secondary: #666680; | |
| --text-muted: #9999aa; | |
| --green: #16a34a; | |
| --yellow: #d97706; | |
| --blue: #2563eb; | |
| --shadow: 0 8px 32px rgba(0,0,0,0.08); | |
| } | |
| *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; line-height: 1.7; } | |
| a { color: var(--red); text-decoration: none; } | |
| a:hover { text-decoration: underline; } | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } | |
| .navbar { | |
| position: sticky; top: 0; z-index: 100; | |
| background: color-mix(in srgb, var(--bg-card) 85%, transparent); | |
| backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--border); | |
| padding: 0 1.5rem; height: 60px; | |
| display: flex; align-items: center; | |
| } | |
| .nav-inner { max-width: 900px; width: 100%; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; } | |
| .nav-brand { display: flex; align-items: center; gap: 0.6rem; font-weight: 700; font-size: 1.15rem; color: var(--text); text-decoration: none; } | |
| .nav-brand i { color: var(--red); } | |
| .nav-links { display: flex; gap: 0.25rem; align-items: center; } | |
| .nav-links a { padding: 0.4rem 0.8rem; border-radius: var(--radius-xs); color: var(--text-secondary); font-size: 0.875rem; font-weight: 500; transition: all var(--transition); text-decoration: none; } | |
| .nav-links a:hover { color: var(--text); background: var(--bg-card); } | |
| .nav-links a.active { color: var(--red); } | |
| .container { max-width: 900px; margin: 0 auto; padding: 2.5rem 1.5rem 4rem; } | |
| h1 { font-size: 2rem; font-weight: 800; margin-bottom: 0.5rem; } | |
| h1 span { color: var(--red); } | |
| .subtitle { color: var(--text-secondary); margin-bottom: 2.5rem; font-size: 1rem; } | |
| .endpoint { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 1.5rem; overflow: hidden; } | |
| .endpoint-head { padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; } | |
| .method { padding: 0.25rem 0.6rem; border-radius: 4px; font-size: 0.75rem; font-weight: 700; font-family: var(--mono); text-transform: uppercase; } | |
| .method.post { background: rgba(59, 130, 246, 0.15); color: var(--blue); } | |
| .method.get { background: rgba(34, 197, 94, 0.15); color: var(--green); } | |
| .endpoint-path { font-family: var(--mono); font-size: 0.9rem; font-weight: 600; } | |
| .endpoint-desc { font-size: 0.85rem; color: var(--text-secondary); margin-left: auto; } | |
| .endpoint-body { padding: 1.25rem 1.5rem; } | |
| .endpoint-body h4 { font-size: 0.85rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0.5rem; } | |
| pre { background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius-xs); padding: 1rem; overflow-x: auto; font-size: 0.82rem; font-family: var(--mono); line-height: 1.6; color: var(--text-secondary); margin-bottom: 1rem; } | |
| code { font-family: var(--mono); font-size: 0.85em; } | |
| table { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-bottom: 1rem; } | |
| th { text-align: left; padding: 0.5rem 0.75rem; background: var(--bg-input); color: var(--text-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; } | |
| td { padding: 0.5rem 0.75rem; border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent); } | |
| td code { background: var(--bg-input); padding: 0.15rem 0.4rem; border-radius: 3px; font-size: 0.8rem; } | |
| .required { color: var(--red); font-weight: 600; } | |
| .tester { background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius-xs); padding: 1rem; margin-top: 0.75rem; } | |
| .tester label { font-size: 0.82rem; font-weight: 600; color: var(--text-secondary); display: block; margin-bottom: 0.3rem; } | |
| .tester input, .tester select { width: 100%; padding: 0.6rem 0.8rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-xs); color: var(--text); font-family: var(--mono); font-size: 0.85rem; margin-bottom: 0.6rem; outline: none; } | |
| .tester input:focus, .tester select:focus { border-color: var(--red); } | |
| .tester button { padding: 0.55rem 1.2rem; background: var(--red); color: #fff; border: none; border-radius: var(--radius-xs); font-weight: 600; font-size: 0.85rem; cursor: pointer; transition: all var(--transition); } | |
| .tester button:hover { background: var(--red-dark); } | |
| .tester button:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .tester-result { margin-top: 0.75rem; padding: 0.75rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-xs); font-family: var(--mono); font-size: 0.8rem; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow-y: auto; display: none; color: var(--text-secondary); } | |
| .tester-result.visible { display: block; } | |
| @media (max-width: 640px) { | |
| .endpoint-head { flex-direction: column; align-items: flex-start; } | |
| .endpoint-desc { margin-left: 0; } | |
| .container { padding: 1.5rem 1rem; } | |
| h1 { font-size: 1.5rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav class="navbar"> | |
| <div class="nav-inner"> | |
| <a href="/" class="nav-brand"><i class="fab fa-youtube"></i> YTDL</a> | |
| <div class="nav-links"> | |
| <a href="/" id="navHome">Home</a> | |
| <a href="/api-guide" class="active" id="navApi">API Guide</a> | |
| </div> | |
| </div> | |
| </nav> | |
| <div class="container"> | |
| <h1>API <span>Guide</span></h1> | |
| <p class="subtitle">All endpoints are called via the Gradio Client. Use the interactive testers below.</p> | |
| <!-- Get Video Info --> | |
| <div class="endpoint"> | |
| <div class="endpoint-head"> | |
| <span class="method post">POST</span> | |
| <span class="endpoint-path">/api/get_video_info</span> | |
| <span class="endpoint-desc">Fetch video metadata</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <h4>Parameters</h4> | |
| <table> | |
| <tr><th>Field</th><th>Type</th><th>Required</th></tr> | |
| <tr><td><code>url</code></td><td>string</td><td><span class="required">Yes</span></td></tr> | |
| </table> | |
| <h4>Response</h4> | |
| <pre>{ | |
| "success": true, | |
| "info": { | |
| "id": "dQw4w9WgXcQ", | |
| "title": "Rick Astley - Never Gonna Give You Up", | |
| "author": "Rick Astley", | |
| "duration": 212, | |
| "duration_string": "3:32", | |
| "views": 1500000000, | |
| "description": "...", | |
| "upload_date": "2009-10-25", | |
| "thumbnail": "https://i.ytimg.com/vi/...", | |
| "keywords": ["rick astley", "pop"], | |
| "captions": ["en", "es"], | |
| "is_live": false | |
| } | |
| }</pre> | |
| <h4>Try It</h4> | |
| <div class="tester"> | |
| <label>YouTube URL</label> | |
| <input type="text" id="testInfoUrl" placeholder="https://www.youtube.com/watch?v=dQw4w9WgXcQ"> | |
| <button onclick="testEndpoint('info')">Send Request</button> | |
| <div class="tester-result" id="testInfoResult"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Get Streams --> | |
| <div class="endpoint"> | |
| <div class="endpoint-head"> | |
| <span class="method post">POST</span> | |
| <span class="endpoint-path">/api/get_streams</span> | |
| <span class="endpoint-desc">List all available formats</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <h4>Parameters</h4> | |
| <table> | |
| <tr><th>Field</th><th>Type</th><th>Required</th></tr> | |
| <tr><td><code>url</code></td><td>string</td><td><span class="required">Yes</span></td></tr> | |
| </table> | |
| <h4>Response</h4> | |
| <pre>{ | |
| "success": true, | |
| "progressive": [{ "format_id": "18", "resolution": "360p", "ext": "mp4", "filesize_mb": 15.2, "label": "360p MP4" }], | |
| "video_only": [{ "format_id": "137", "resolution": "1080p", "ext": "mp4", "filesize_mb": 85.0, "vcodec": "avc1", "label": "1080p MP4 (avc1)" }], | |
| "audio_only": [{ "format_id": "140", "resolution": "audio", "ext": "m4a", "abr": "128kbps", "label": "128kbps M4A" }], | |
| "available_resolutions": ["2160p", "1080p", "720p", "360p"], | |
| "best_resolution": "2160p" | |
| }</pre> | |
| <div class="tester"> | |
| <label>YouTube URL</label> | |
| <input type="text" id="testStreamsUrl" placeholder="https://www.youtube.com/watch?v=dQw4w9WgXcQ"> | |
| <button onclick="testEndpoint('streams')">Send Request</button> | |
| <div class="tester-result" id="testStreamsResult"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Download --> | |
| <div class="endpoint"> | |
| <div class="endpoint-head"> | |
| <span class="method post">POST</span> | |
| <span class="endpoint-path">/api/download</span> | |
| <span class="endpoint-desc">Start a download task</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <h4>Parameters</h4> | |
| <table> | |
| <tr><th>Field</th><th>Type</th><th>Default</th><th>Required</th></tr> | |
| <tr><td><code>url</code></td><td>string</td><td>—</td><td><span class="required">Yes</span></td></tr> | |
| <tr><td><code>format_id</code></td><td>string</td><td>—</td><td><span class="required">Yes*</span></td></tr> | |
| <tr><td><code>is_audio</code></td><td>boolean</td><td>false</td><td>No</td></tr> | |
| <tr><td><code>audio_quality</code></td><td>string</td><td>"128"</td><td>No</td></tr> | |
| </table> | |
| <p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem;">* format_id not needed when is_audio=true (uses bestaudio).</p> | |
| <h4>Response</h4> | |
| <pre>{ "success": true, "task_id": "a1b2c3d4e5f6", "message": "Download started" }</pre> | |
| <p style="font-size: 0.82rem; color: var(--text-secondary); margin-bottom: 1rem;">After getting a task_id, poll <code>/api/get_progress</code> to track status. When complete, download the file via <code>/serve/{video_id}/{filename}</code>.</p> | |
| <div class="tester"> | |
| <label>YouTube URL</label> | |
| <input type="text" id="testDlUrl" placeholder="https://www.youtube.com/watch?v=dQw4w9WgXcQ"> | |
| <label>Format ID</label> | |
| <input type="text" id="testDlFid" placeholder="18 or 137"> | |
| <label>Audio?</label> | |
| <select id="testDlAudio"> | |
| <option value="false">No (Video)</option> | |
| <option value="true">Yes (Audio MP3)</option> | |
| </select> | |
| <button onclick="testEndpoint('download')">Start Download</button> | |
| <div class="tester-result" id="testDlResult"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Get Progress --> | |
| <div class="endpoint"> | |
| <div class="endpoint-head"> | |
| <span class="method post">POST</span> | |
| <span class="endpoint-path">/api/get_progress</span> | |
| <span class="endpoint-desc">Poll download progress</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <h4>Parameters</h4> | |
| <table> | |
| <tr><th>Field</th><th>Type</th><th>Required</th></tr> | |
| <tr><td><code>task_id</code></td><td>string</td><td><span class="required">Yes</span></td></tr> | |
| </table> | |
| <h4>Response</h4> | |
| <pre>{ | |
| "success": true, | |
| "status": "downloading", // "starting" | "downloading" | "completed" | "error" | |
| "percent": 45.2, | |
| "speed": "5.67MiB/s", | |
| "eta": "00:15", | |
| "filename": null, | |
| "filepath": null | |
| }</pre> | |
| <p style="font-size: 0.82rem; color: var(--text-secondary);">When <code>status</code> is <code>"completed"</code>, <code>filename</code> and <code>filepath</code> will be set.</p> | |
| <div class="tester"> | |
| <label>Task ID</label> | |
| <input type="text" id="testProgId" placeholder="a1b2c3d4e5f6"> | |
| <button onclick="testEndpoint('progress')">Check Progress</button> | |
| <div class="tester-result" id="testProgResult"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Download Subtitles --> | |
| <div class="endpoint"> | |
| <div class="endpoint-head"> | |
| <span class="method post">POST</span> | |
| <span class="endpoint-path">/api/download_subtitles</span> | |
| <span class="endpoint-desc">Download subtitles as SRT</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <h4>Parameters</h4> | |
| <table> | |
| <tr><th>Field</th><th>Type</th><th>Default</th><th>Required</th></tr> | |
| <tr><td><code>url</code></td><td>string</td><td>—</td><td><span class="required">Yes</span></td></tr> | |
| <tr><td><code>lang</code></td><td>string</td><td>"en"</td><td>No</td></tr> | |
| </table> | |
| <div class="tester"> | |
| <label>YouTube URL</label> | |
| <input type="text" id="testSubsUrl" placeholder="https://www.youtube.com/watch?v=dQw4w9WgXcQ"> | |
| <label>Language Code</label> | |
| <input type="text" id="testSubsLang" value="en" placeholder="en"> | |
| <button onclick="testEndpoint('subtitles')">Download Subtitles</button> | |
| <div class="tester-result" id="testSubsResult"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Serve File --> | |
| <div class="endpoint"> | |
| <div class="endpoint-head"> | |
| <span class="method get">GET</span> | |
| <span class="endpoint-path">/serve/{video_id}/{filename}</span> | |
| <span class="endpoint-desc">Download a completed file</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <p style="font-size: 0.85rem; color: var(--text-secondary);">After a download completes, use this endpoint to retrieve the file. The response includes a <code>Content-Disposition: attachment</code> header for direct browser download.</p> | |
| <pre>GET /serve/dQw4w9WgXcQ/Rick_Astley_18.mp4</pre> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="importmap"> | |
| { "imports": { "@gradio/client": "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js" } } | |
| </script> | |
| <script type="module"> | |
| import { Client } from "@gradio/client"; | |
| const client = await Client.connect(window.location.origin); | |
| async function testEndpoint(type) { | |
| let result; | |
| try { | |
| switch (type) { | |
| case 'info': { | |
| const url = document.getElementById('testInfoUrl').value.trim(); | |
| if (!url) return; | |
| const res = await client.predict('/get_video_info', { url }); | |
| result = res.data; | |
| showResult('testInfoResult', result); | |
| break; | |
| } | |
| case 'streams': { | |
| const url = document.getElementById('testStreamsUrl').value.trim(); | |
| if (!url) return; | |
| const res = await client.predict('/get_streams', { url }); | |
| result = res.data; | |
| showResult('testStreamsResult', result); | |
| break; | |
| } | |
| case 'download': { | |
| const url = document.getElementById('testDlUrl').value.trim(); | |
| const fid = document.getElementById('testDlFid').value.trim(); | |
| const isAudio = document.getElementById('testDlAudio').value === 'true'; | |
| if (!url) return; | |
| const res = await client.predict('/download', { url, format_id: fid || 'bestaudio', is_audio: isAudio, audio_quality: '128' }); | |
| result = res.data; | |
| showResult('testDlResult', result); | |
| break; | |
| } | |
| case 'progress': { | |
| const tid = document.getElementById('testProgId').value.trim(); | |
| if (!tid) return; | |
| const res = await client.predict('/get_progress', { task_id: tid }); | |
| result = res.data; | |
| showResult('testProgResult', result); | |
| break; | |
| } | |
| case 'subtitles': { | |
| const url = document.getElementById('testSubsUrl').value.trim(); | |
| const lang = document.getElementById('testSubsLang').value.trim() || 'en'; | |
| if (!url) return; | |
| const res = await client.predict('/download_subtitles', { url, lang }); | |
| result = res.data; | |
| showResult('testSubsResult', result); | |
| break; | |
| } | |
| } | |
| } catch (err) { | |
| showResult('test' + type.charAt(0).toUpperCase() + type.slice(1) + 'Result', { error: err.message }); | |
| } | |
| } | |
| function showResult(id, data) { | |
| const el = document.getElementById(id); | |
| el.textContent = JSON.stringify(data, null, 2); | |
| el.classList.add('visible'); | |
| } | |
| // Make it global for onclick handlers | |
| window.testEndpoint = testEndpoint; | |
| </script> | |
| </body> | |
| </html> |