ytdl / api_guide.html
Z User
fix: gradio.Server with demo variable + proper Space config
5603df0
Raw
History Blame Contribute Delete
19.9 kB
<!DOCTYPE html>
<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>