ytget API

Асинхронные задания: создать → опрашивать → скачать файл. Версия 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 часов, её можно отдать пользователю напрямую.

Пример: подпись на PHP

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идентификатор задания
statusqueued → 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, finishedunix-время создания и завершения

Режимы 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..."

Пример: Python

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.