Скачивание видео из соцсетей на Node.js
Весь клиент — это один вызов fetch. Дольше делается всё вокруг: превратить полученный адрес в скачивание, на которое нажмёт ваш собственный пользователь, не дать медленному источнику подвесить обработчик и понимать, какие ошибки окончательны. Всё это ниже.
Обновлено
Первый вызов
В Node fetch встроен начиная с 18-й версии, так что клиентскую библиотеку ставить не нужно. Скопировать стоит AbortSignal.timeout: без него медленный источник держит обработчик живым, пока его не убьёт платформа.
JavaScriptconst 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].
JavaScriptconst 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 браузер сохранит файл под выбранным вами именем.
JavaScriptimport { 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, чтобы правило жило в одном месте.
JavaScriptconst 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");
}Читайте заголовки, которые уже пришли
В каждом ответе есть минутный лимит, его остаток и остаток кредитов. Этого хватает, чтобы притормозить до того, как вас ограничат, и поднять тревогу, когда пакет заканчивается, — без единого лишнего запроса.
JavaScriptconst 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 скажет, какой платформе принадлежит ссылка, не трогая кредиты. Он считается в минутном лимите, так что это шаг проверки, а не бесплатный зонд.
JavaScriptconst 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 бесплатных кредитов, без карты и без срока.