saveapi.org

Скачивание видео из соцсетей на Node.js

Весь клиент — это один вызов fetch. Дольше делается всё вокруг: превратить полученный адрес в скачивание, на которое нажмёт ваш собственный пользователь, не дать медленному источнику подвесить обработчик и понимать, какие ошибки окончательны. Всё это ниже.

Обновлено

Первый вызов

В Node fetch встроен начиная с 18-й версии, так что клиентскую библиотеку ставить не нужно. Скопировать стоит AbortSignal.timeout: без него медленный источник держит обработчик живым, пока его не убьёт платформа.

JavaScript
const API = "https://api.saveapi.org/v1/download";

async function resolve(link) {
  const res = await fetch(API + "?url=" + encodeURIComponent(link), {
    headers: { Authorization: "Bearer sk_live_xxxx" },
    signal: AbortSignal.timeout(30_000),
  });
  return res.json();
}

Прочитайте ответ

medias[] хранит все файлы поста в их порядке. Слайд-шоу TikTok даст записи image и отдельную запись audio с фоновым треком, поэтому ищите find() по type, а не надейтесь на medias[0].

JavaScript
const data = await resolve("https://www.tiktok.com/@user/video/123");

if (!data.success) throw new Error(data.error.code);

const video = data.medias.find((m) => m.type === "video");
const images = data.medias.filter((m) => m.type === "image");
const audio = data.medias.find((m) => m.type === "audio");

Отдайте файл пользователю потоком

Если ваши пользователи жмут кнопку скачивания, не буферизуйте файл и не отправляйте их редиректом на CDN источника — ссылка подписана и истекает. Пропустите тело ответа источника прямо в свой ответ, и с заголовком Content-Disposition браузер сохранит файл под выбранным вами именем.

JavaScript
import { Readable } from "node:stream";

app.get("/save", async (req, res) => {
  const data = await resolve(req.query.url);
  if (!data.success) return res.status(400).json(data.error);

  const upstream = await fetch(data.medias[0].url);
  res.setHeader("Content-Type", "video/mp4");
  res.setHeader("Content-Disposition", 'attachment; filename="video.mp4"');
  Readable.fromWeb(upstream.body).pipe(res);
});

Повторяйте только то, что лечится повтором

Таймауты и ошибки источника заслуживают второй попытки. Закрыто, не найдено и не поддерживается — окончательные ответы: их повтор тратит время, а на тарифицируемых ошибках ещё и кредит. Держите повторяемые коды в Set, чтобы правило жило в одном месте.

JavaScript
const RETRY = new Set(["UPSTREAM_TIMEOUT", "UPSTREAM_ERROR"]);

async function withRetry(link) {
  for (let attempt = 0; attempt < 2; attempt++) {
    const data = await resolve(link);
    if (data.success) return data;
    if (!RETRY.has(data.error.code)) throw new Error(data.error.message);
  }
  throw new Error("source unavailable");
}

Читайте заголовки, которые уже пришли

В каждом ответе есть минутный лимит, его остаток и остаток кредитов. Этого хватает, чтобы притормозить до того, как вас ограничат, и поднять тревогу, когда пакет заканчивается, — без единого лишнего запроса.

JavaScript
const res = await fetch(API + "?url=" + encodeURIComponent(link), { headers });

const perMinute = Number(res.headers.get("X-RateLimit-Limit"));
const left = Number(res.headers.get("X-RateLimit-Remaining"));
const credits = Number(res.headers.get("X-Credits-Remaining"));

Проверьте ссылку до того, как потратите кредит

Там, где пользователи вставляют произвольный текст, /v1/detect скажет, какой платформе принадлежит ссылка, не трогая кредиты. Он считается в минутном лимите, так что это шаг проверки, а не бесплатный зонд.

JavaScript
const check = await fetch(
  "https://api.saveapi.org/v1/detect?url=" + encodeURIComponent(userInput),
  { headers },
);
const { platform } = await check.json();   // kredit sarflanmaydi

Вопросы перед началом

Можно ли вызывать это из браузера?
Технически да, но не стоит: ключ окажется в вашем бандле, и его прочитает кто угодно. Вынесите вызов в маршрут на сервере — туда же относятся логика повторов и работа с лимитами.
Есть ли TypeScript-тип для ответа?
Формат стабилен и достаточно мал, чтобы объявить его самому — success, platform, type, medias[], необязательный formats[], meta и credits. У собственного объявления есть и второй плюс: новое необязательное поле не сломает сборку.
Почему потоком, а не редиректом на адрес медиа?
Ссылка CDN подписана и живёт недолго, а редирект её раскрывает. Поток через собственный маршрут оставляет имя файла, Content-Type и срок жизни ссылки под вашим контролем.

Платформы, использованные здесь

Другие руководства

Запустите это руководство со своим ключом

Ключ за минуту. 1 000 бесплатных кредитов, без карты и без срока.

Получить ключ и попробоватьПолный справочник API