savereelsbot

Telegram-бот: по ссылке скачивает видео/фото из Instagram/YouTube/TikTok. Два режима — личка (юзер шлёт ссылку, бот отвечает видео) и приватный канал (бот-админ, автопостинг + удаление исходного сообщения).

Стек: Node.js + TypeScript, yt-dlp как child process, локальный telegram-bot-api (лимит аплоада 2GB вместо 50MB). Репо: Kobalt695/savereelsbot. Полный план — /home/claude/.claude/plans/cuddly-strolling-starlight.md.

Instagram-карусели (2026-07-07)

Добавлена поддержка carousel-постов (2-20 фото/видео в одном посте) — раньше падали с ошибкой No video formats found!.

  • Причина: --no-playlist не помогает — Instagram-экстрактор yt-dlp всё равно разворачивает карусель в entries, и yt-dlp пытается выбрать видео-формат для каждого элемента. У фото-элементов formats пуст → hard error на весь --dump-json.
  • Фикс: флаг --ignore-no-formats-error — yt-dlp продолжает и отдаёт JSON по всем элементам. Различаем фото/видео по entry.formats.length. У фото берём entry.thumbnails.at(-1).url (полноразмерный CDN-URL, без даунскейла — подтверждено на реальном посте). У видео — доскачиваем через --playlist-items <playlist_index> на том же URL с обычным format-selector’ом (playlist_index 1-based, строго по порядку entries — подтверждено).
  • DownloadResult теперь discriminated union (kind: 'video' | 'carousel') в types.ts. Новые методы sendPhoto/sendMediaGroup в telegram.ts (multipart, attach://fileN, caption только на первом элементе — так группируется в Telegram). Лимит Telegram media group — 10 элементов, если фотка/видео одно — шлём как обычный sendPhoto/sendVideo.
  • Проверено живьём: реальный пост instagram.com/p/DadKfF3gSuL/ (10 фото) — альбом ушёл, все 10 файлов. Regression: обычный reel по-прежнему качается как раньше (kind: 'video').
  • TikTok/YouTube путь не тронут — carousel-ветка только для platform === 'instagram'.

Статус (2026-07-07)

  • ✅ Прототип yt-dlp-обёртки, локальный telegram-bot-api, MVP личка, Instagram-карусели — работают (YouTube/Instagram качаются напрямую с текущего сервера без проблем)
  • 🔴 TikTok заблокирован с сервера (82.39.215.94, Hostkey DE, AS57043) — 302 на /hk/notfound даже для @tiktok. Причина — датацентр-ASN в бан-листе TikTok, не гео
  • ⏳ Нужен residential-прокси только для TikTok-трафика (см. ниже). Отложено пользователем на потом — сейчас не покупаем, просто зафиксировали ресёрч

Проверено и отклонено

  • nl-aeza (138.124.119.57) — другой VPS/ASN, тоже не помогло: curl проходил (200, без редиректа), но yt-dlp получал "Your IP address is blocked from accessing this post" — это уже проверка репутации IP на уровне API TikTok, а не edge-редирект по ASN. Т.е. TikTok банит датацентр-диапазоны глубже, чем просто CDN-edge — любой VPS-провайдер (Hetzner/OVH/Aeza и т.п.) будет давать тот же результат. Доступ к nl-aeza (ключ) отозван и удалён после теста — не пригодился
  • 193.42.124.166 (прод-vpnbot) — сознательно не трогали, это прод-сервер реального бизнеса, не тестовая нода

Residential proxy — ресёрч провайдеров

Нужен именно residential (IP реального провайдера связи, выданный обычному человеку), не datacenter — TikTok категорически банит датацентр-ASN, но не может банить residential-диапазоны (иначе забанит обычных пользователей).

Проблема с оплатой из РФ

IPRoyal и Webshare ориентированы на западные карты — с российской картой, скорее всего, затык. Найдены RU-дружелюбные альтернативы:

ПровайдерОплата из РФЦенаОсобенности
FroxyРоссийские карты, СБП, крипта (детали оплаты)от 1.99/3 дня (100MB)Эстонская компания, 10M+ устройств, 200+ стран, 70k+ провайдеров связи
Proxy-SellerКрипта (USDT/BTC), + рекомендуют платить именно RU-картой если есть выбор — возврат без комиссии сетирезидентные от $3.5/GB, полный прайс20M+ IP, 220+ стран, rotating (по времени/запросу/sticky), таргетинг по стране/городу/провайдеру
IPRoyalЗападные карты, вероятны проблемы с RUpay-as-you-go, $1.75-7/GB, прайсТрафик не сгорает — но оплата под вопросом
WebshareЗападные карты, вероятны проблемы с RUresidential от $3.50/GB (промо), прайсFree tier есть, но это datacenter-IP — для TikTok не годится

Вывод: смотреть в сторону Froxy или Proxy-Seller из-за оплаты. Крипта (USDT) как universal fallback работает почти везде, включая IPRoyal/Webshare, если всё же захочется остаться на них.

Как это встраивается технически

Residential-прокси не даёт статичный IP на руки (кроме отдельного дорогого продукта “ISP/static residential” — не нужен нам). Работает через один постоянный gateway-адрес + логин:пароль, IP меняется прозрачно на стороне провайдера (rotating по времени/запросу, либо sticky-сессия). Для нас это просто:

TIKTOK_PROXY=http://user:pass@<gateway-host>:<port>

Один флаг --proxy в yt-dlp, применяется только к tiktok.com (YouTube/Instagram продолжают качаться напрямую). Rotating-режим (не sticky) — то что нужно, дешевле и даже лучше для нас (нет смысла держать сессию/один IP, каждый запрос независим).

Готча с окружением (уже решена, но важно помнить)

curl_cffi (нужен для TLS-impersonation, обход анти-бота TikTok независимо от прокси) — установлен в user site-packages /usr/bin/python3 (pip.pyz install --user --break-system-packages curl_cffi, т.к. apt/ensurepip/venv недоступны без sudo). Важно: ./bin/yt-dlp через shebang резолвит python3 из PATH, а там первым стоит venv [[projects/agent-second-brain]] (без curl_cffi) — нужно явно вызывать /usr/bin/python3 bin/yt-dlp ..., иначе impersonation молча не работает. При доработке downloader.ts (task #4) — учесть в spawn.

Следующий шаг

Пользователь отложил покупку прокси на потом. Когда вернёмся: выбрать Froxy или Proxy-seller, оплатить, получить gateway host:port:user:pass, воткнуть в downloader.ts (только для detectPlatform === tiktok).

Нейтральная подпись для карусели (commit 48a06b8)

Для carousel-постов заголовок вида Video by <username> заменяется на Пост от <username> (buildCaption() в [[index]].ts, regex /^Video by /i) — чтобы подпись при автопостинге в канал не выглядела мусорной/машинной.

systemd вместо ручного запуска (2026-07-07)

Бот переведён с ручного запуска на systemd user-unit:

  • ~/.config/systemd/user/savereelsbot.service: ExecStart=node dist/[[index]].js (не ts-node — собранный JS), Restart=always, RestartSec=3, лог в /home/claude/savereelsbot/bot.log
  • Обновление: npm run build && systemctl --user restart savereelsbot
  • Попутно починен git credential helper (gh auth setup-git) — до этого push из-под claude спотыкался на аутентификации

Почему это делалось реактивно, а не сразу: пользователь ожидает, что crash recovery и переживание ребута — часть первой реализации любого долгоживущего сервиса, а не то, что предлагается только после того, как процесс уже падал.

Форвард постов канала “Идеи” в vault-inbox (2026-07-07)

Цель: посты из приватного канала “Идеи” (savereelsbot туда же и постит) не теряются, а попадают в vault заготовками для разбора.

Отброшенный вариант: форвард через Telegram в топик ccbot (message_thread_id). Не работает архитектурно — getChat на приватный чат ccbot с пользователем (30777924) вернул "type": "private", а не supergroup. Значит “топики” ccbot — его собственная эмуляция поверх одного flat-чата (вероятно по reply-цепочкам), а не нативные Telegram forum-topics. Нативный message_thread_id работает только в форум-супергруппах, и один бот физически не может писать в приватный чат другого бота (у savereelsbot и у ccbot — разные, изолированные приватные чаты с одним и тем же пользователем). Заводить отдельную группу с forum-режимом ради этого не стали — пользователь выбрал вариант проще.

Выбранный вариант — filesystem inbox: savereelsbot (юзер claude, тот же, что у Claude Code) сам пишет пост прямо в vault/thoughts/ideas/inbox/<timestamp>-<platform>/:

  • [[thoughts/ideas/inbox/1784195450310-instagram/post]].md — frontmatter (source, platform, created, status: new) + секции ## Ссылка / ## Подпись
  • thumbnail.jpg (для video-постов, если скачался) или photo1.jpg, photo2.jpg, … (для carousel — все фото-элементы)

Разбор — автоматический, без напоминаний от пользователя: пункт 4 добавлен в обязательный чек-лист старта сессии в /home/claude/CLAUDE.md. Любая Claude Code сессия под /home/claude при первом сообщении проверяет inbox/, разбирает найденные папки в vault/thoughts/ideas/YYYY-MM-DD-slug.md (frontmatter: type/description/tags/status/created; секции Источник/Разбор/План) + строку в vault/MOC/[[MOC/MOC-ideas]].md, затем удаляет обработанную папку из inbox.

Код: saveToVaultInbox() в [[index]].ts, вызывается для isChannelPost после отправки в канал, до удаления скачанных файлов. VAULT_INBOX_DIR — env var с дефолтом на путь выше. Попутно добавлен захват thumbnail для одиночных видео (downloadThumbnail() в downloader.ts, VideoResult.thumbnailPath) — видео целиком в vault-inbox не положить (Claude Code не “смотрит” видео), только превью+подпись+ссылка. Коммиты: df5508b (первая версия через Telegram-топик, заменена), 532c609 (текущая filesystem-версия).

Канал сейчас пустой/debug — бэкафилл старых постов не делался.

Разбор inbox — АВТОМАТИЗИРОВАН 05.08 (был ненадёжен: держался на инструкции «разбери при старте сессии» в /home/claude/CLAUDE.md, по факту пробуксовывал — 05.08 лежали 4 папки, две с 02.08 без разбора 3 дня).

  • Как сделано: разбор встроен в ежедневный dbrain-process (21:00) тем же механизмом, что daily-обработка (persistent Claude-сессия d-brain, НЕ claude -p — биллинг на подписку).
    • processor.py → метод process_inbox(): находит папки в vault/thoughts/ideas/inbox/, пустой inbox = быстрый no-op без обращения к сессии; иначе шлёт промпт разбора (оформить YYYY-MM-DD-slug.md + строка в MOC-ideas + rm папки).
    • pipeline.py → команда inbox роутит на process_inbox().
    • scripts/process.sh → шаг python -m d_brain.pipeline inbox после daily, до graph rebuild (новые заметки попадают в тот же граф + git-коммит).
  • Проверено live 05.08: тестовая папка → d-brain создал заметку, добавил в MOC (со связью на реальные проекты Сергея), удалил папку. End-to-end работает. Инструкция в CLAUDE.md остаётся как подстраховка для интерактивных сессий.