Асинхронные задания: создать → опрашивать → скачать файл. Версия 0.1.0. интерфейс · openapi.json
Все эндпоинты, кроме /, /docs, /openapi.json и /api/health, требуют подписи запроса.
Секрет (YTGET_API_SECRET) знают только сервер и ваш бэкенд, по сети он не передаётся. Каждый запрос несёт три заголовка:
X-Ytget-Timestamp: <unix-секунды>
X-Ytget-Nonce: <случайная строка, 8–128 символов [A-Za-z0-9_-]>
X-Ytget-Signature: hex( HMAC-SHA256( secret, canonical ) )
canonical = "YTGET-HMAC-SHA256" + "\n" + METHOD + "\n" + PATH_С_QUERY + "\n"
+ TIMESTAMP + "\n" + NONCE + "\n" + hex( SHA256(тело запроса) )
Подпись действует 5 минут от серверного времени и принимается один раз: перехваченный запрос нельзя повторить, а изменить в нём
метод, путь или тело нельзя без секрета. Без подписи ответ 401 {"error": "unauthorized", "reason": "..."}.
Ссылка file_url в готовом задании уже подписана (?exp=…&sig=…) и действует 6 часов, её можно отдать
пользователю напрямую.
function ytget_call(string $method, string $path, ?array $json = null): array
{
$secret = getenv('YTGET_API_SECRET');
$body = $json === null ? '' : json_encode($json);
$ts = (string) time();
$nonce = bin2hex(random_bytes(16));
$canon = implode("\n", ['YTGET-HMAC-SHA256', $method, $path, $ts, $nonce, hash('sha256', $body)]);
$headers = [
'X-Ytget-Timestamp: ' . $ts,
'X-Ytget-Nonce: ' . $nonce,
'X-Ytget-Signature: ' . hash_hmac('sha256', $canon, $secret),
];
if ($json !== null) { $headers[] = 'Content-Type: application/json'; }
$ch = curl_init('https://api.soundzilla.io' . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $json === null ? null : $body, CURLOPT_RETURNTRANSFER => true]);
$res = json_decode((string) curl_exec($ch), true);
curl_close($ch);
return $res ?? [];
}
| POST | /api/jobs | Создать задание. Тело JSON: url (обязательно), mode (mp3 по умолчанию; mp3 | m4a | opus | audio | mp4 | webm | video), quality (макс. высота видео для mp4/webm, по умолчанию 1080). Ответ 202 с объектом задания. |
| GET | /api/jobs/{id} | Статус и прогресс задания. Опрашивайте раз в 1–2 с, пока status не станет done или error. Для done заполнены filename, size, file_url. |
| GET | /api/jobs | Последние 50 заданий (старые первыми). |
| GET / HEAD | /files/{name} | Готовый файл. Берите путь из file_url: в нём уже есть подпись exp/sig, заголовки не нужны, ссылка живёт 6 часов. Поддерживает Range, отдаёт Content-Disposition: attachment. |
| GET | /api/health | Проверка живости, без авторизации: {"ok": true, "auth": true, "queued": N, "jobs": M}. |
| GET | /openapi.json | Спецификация OpenAPI 3.0 (можно открыть в Swagger UI / Postman). |
id | идентификатор задания |
status | queued → running → done | error |
stage | текущий шаг при running: extracted, audio, video, converting, muxing |
done / total | байты скачано / ожидается на текущем шаге (total может быть null) |
title, client | название видео и InnerTube-клиент, давший ссылки (появляются после извлечения) |
filename, size, file_url | имя, размер и относительная ссылка на файл (только для done) |
error | текст ошибки (только для error) |
created, finished | unix-время создания и завершения |
Режимы mode: mp3, m4a, opus, audio, mp4, webm, video. mp3 перекодирует в MP3 192k с тегами и обложкой; m4a/opus
сохраняют оригинальную дорожку без перекодирования; mp4/webm склеивают лучшее видео до quality и аудио.
# signed curl command, valid for 5 minutes and one use (needs YTGET_API_SECRET in the env)
python ytget.py sign POST /api/jobs --body '{"url": "dQw4w9WgXcQ", "mode": "mp3"}'
# -> curl -sS -X POST -H 'X-Ytget-Timestamp: ...' -H 'X-Ytget-Nonce: ...' -H 'X-Ytget-Signature: ...' \
# -H 'Content-Type: application/json' --data-binary '{"url": ...}' https://api.soundzilla.io/api/jobs
# {"id": "0b72cf193680", "status": "queued", "mode": "mp3", ...}
python ytget.py sign GET /api/jobs/0b72cf193680 # then run the printed curl
# {"id": "0b72cf193680", "status": "running", "stage": "audio", "done": 4194304, "total": 3449447, ...}
# ...
# {"status": "done", "filename": "Rick Astley - ... [dQw4w9WgXcQ].mp3", "size": 5153422,
# "file_url": "/files/Rick%20Astley%20-%20...%20%5BdQw4w9WgXcQ%5D.mp3?exp=1789150000&sig=4f1c...",
# "client": "web_embedded"}
# file_url is pre-signed: no headers needed curl -OJ "https://api.soundzilla.io/files/Rick%20Astley%20-%20...%20%5BdQw4w9WgXcQ%5D.mp3?exp=1789150000&sig=4f1c..."
import hashlib, hmac, json, os, secrets, time, requests
API, SECRET = "https://api.soundzilla.io", os.environ["YTGET_API_SECRET"]
def signed(method, path, payload=None):
body = b"" if payload is None else json.dumps(payload).encode()
ts, nonce = str(int(time.time())), secrets.token_hex(16)
canon = "\n".join(["YTGET-HMAC-SHA256", method, path, ts, nonce, hashlib.sha256(body).hexdigest()])
headers = {"X-Ytget-Timestamp": ts, "X-Ytget-Nonce": nonce,
"X-Ytget-Signature": hmac.new(SECRET.encode(), canon.encode(), hashlib.sha256).hexdigest()}
if payload is not None:
headers["Content-Type"] = "application/json"
return requests.request(method, API + path, data=body or None, headers=headers).json()
job = signed("POST", "/api/jobs", {"url": "https://youtu.be/dQw4w9WgXcQ", "mode": "mp4", "quality": 720})
while job["status"] not in ("done", "error"):
time.sleep(2)
job = signed("GET", f"/api/jobs/{job['id']}")
print(job["status"], job.get("stage"), job.get("done"), job.get("total"))
if job["status"] == "done":
with requests.get(API + job["file_url"], stream=True) as r, open(job["filename"], "wb") as f:
for chunk in r.iter_content(1 << 20):
f.write(chunk)
Ошибки валидации приходят кодом 400 с телом {"error": "..."}; неизвестное задание или файл — 404.
Ошибки самой загрузки не меняют HTTP-код: задание переходит в status = "error", текст в поле error
(например, all clients failed: ... когда YouTube отверг все клиенты, или LOGIN_REQUIRED для приватного видео).
Ошибка подписи: 401 с полем reason (например, timestamp outside the allowed window,
signature mismatch, nonce already used). Тело больше 64 КБ: 413.