Plays audio into Stoat voice channels over LiveKit and exposes the same player through both chat commands and a browser panel, so the two never drift apart: everything routes through a single MusicManager. - core: per-server GuildPlayer (queue, loop, shuffle, seek, volume, idle auto-leave) driving revoice.js/@livekit/rtc-node and ffmpeg - sources: yt-dlp for YouTube/SoundCloud, direct media URLs and internet radio, optional local library with path-traversal guards - bot: 18 chat commands with aliases, plus !panel one-time login links - api: Fastify REST + WebSocket, sessions authenticated against the instance's own /auth/session/login (TOTP supported), permissions re-checked against Stoat membership and roles on every request - web: React panel with search, queue editing, seek and volume - deploy: Dockerfile, compose.override.yml and Caddyfile snippets for dropping the service into an existing /opt/stoat stack Verified with npm run typecheck, both builds, and scripts/smoke-api.mjs (9 API checks). Voice playback itself needs a live instance to test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stoat-mbot
Музыкальный бот для self-hosted Stoat с веб-панелью: поиск и воспроизведение в голосовых каналах, очередь, перемотка, громкость — из чата и из браузера, состояние синхронизировано в обе стороны через WebSocket.
Источники: YouTube, SoundCloud, прямые ссылки и интернет-радио, локальная медиатека (опционально).
Голос: LiveKit — тот же, что и в вашем инстансе (revoice.js + @livekit/rtc-node), звук готовит ffmpeg.
Вход в панель: учётными записями вашего же Stoat (пароль уходит прямо в /auth/session/login
вашего инстанса, бот его не хранит) либо одноразовой ссылкой по команде !panel.
Как это устроено
Stoat (чат) ──messageCreate──► bot/commands ─┐
├──► MusicManager ──► GuildPlayer ──► LiveKit
Браузер ──REST + WebSocket──► api/server ───┘ (очередь, (ffmpeg,
громкость, yt-dlp)
повтор)
Чат-команды и панель дергают один и тот же MusicManager, поэтому «нажал в браузере — увидел в чате»
работает без рассинхрона. Каждое действие панели повторно проверяет членство и права в Stoat,
так что доступ живёт в ролях Stoat, а не в отдельной базе бота.
| Слой | Файлы |
|---|---|
| Источники и yt-dlp | src/sources |
| Плеер и очередь | src/core/player.ts, src/core/manager.ts |
| Чат-бот | src/bot |
| REST + WebSocket | src/api/server.ts |
| Веб-панель (React) | web/src |
Установка рядом со Stoat
Предполагается раскладка /opt/stoat (инстанс) и /opt/stoat-mbot (этот репозиторий).
1. Клонировать репозиторий
cd /opt && git clone https://gitea.hsrv.site/mrleo1nid/stoat-mbot.git stoat-mbot
2. Создать бота в Stoat
Settings → My Bots → создать бота → скопировать токен → пригласить бота на сервер. Боту нужны права: читать сообщения, писать сообщения и подключаться к голосовым каналам.
3. Заполнить .env
cd /opt/stoat-mbot
cp .env.example .env
openssl rand -hex 32 # → JWT_SECRET
Минимум, что нужно указать:
STOAT_API_URL=https://stoat.example.com/api
STOAT_BOT_TOKEN=<токен бота>
PUBLIC_URL=https://music.example.com
JWT_SECRET=<случайные 32 байта>
STOAT_API_URL лучше указывать публичный (https://домен/api): бот тогда ведёт себя как обычный
клиент и получает от join_call тот же LiveKit-URL, что и все. Внутренний http://api:14702 тоже
работает, но убедитесь, что LiveKit-URL из ответа резолвится изнутри контейнера.
4. Добавить сервис в compose.override.yml
В /opt/stoat/compose.override.yml (готовый пример — deploy/compose.override.yml.example):
services:
caddy:
ports: !override
- "127.0.0.1:8880:80"
mbot:
build: ../stoat-mbot
restart: always
env_file: ../stoat-mbot/.env
depends_on:
api:
condition: service_started
volumes:
- ../stoat-mbot/data:/data
Сервис попадает в тот же compose-проект и ту же сеть, поэтому Caddy видит его как http://mbot:3005,
а бот ходит в Stoat API по имени api.
5. Пробросить панель через Caddy
В /opt/stoat/Caddyfile добавьте блок (deploy/Caddyfile.snippet):
http://music.example.com {
reverse_proxy http://mbot:3005
}
Схема http:// — потому что у вас TLS терминирует внешний прокси (Caddy слушает 127.0.0.1:8880).
WebSocket проксируется автоматически.
6. Запустить
cd /opt/stoat && docker compose up -d --build mbot && docker compose logs -f mbot
В логах должно появиться yt-dlp detected, bot is ready и panel is listening.
Использование
В чате (префикс по умолчанию !):
| Команда | Что делает |
|---|---|
!play <ссылка или название> |
добавить трек/плейлист в очередь |
!playnext, !playnow |
следующим / немедленно |
!search <запрос> → !pick <n> |
поиск с выбором из списка |
!skip [n], !stop, !pause, !resume |
управление воспроизведением |
!queue [страница], !nowplaying |
очередь и текущий трек |
!volume [0-200], !loop [off|track|queue], !shuffle |
звук и порядок |
!remove <n>, !clear, !seek 1:23 |
правка очереди и перемотка |
!join, !leave |
зайти в ваш голосовой канал / выйти |
!panel |
личная ссылка на веб-панель (действует 10 минут) |
!help |
список команд |
Префиксы поиска: sc: — SoundCloud, yt: — YouTube, local: — локальная медиатека.
В панели: поиск с добавлением в очередь/следующим/сейчас, drag-free перестановка треков стрелками, клик по полосе прогресса — перемотка, слайдер громкости, выбор голосового канала, история.
Настройки
Все параметры — в .env.example. Что стоит знать:
REQUIRE_DJ_ROLE=true— управлять смогут только владелец сервера, обладателиManageServerи роли изDJ_ROLE_NAME. По умолчаниюfalse: играть может любой участник сервера.IDLE_TIMEOUT_SECONDS— через сколько секунд простоя (или пустого канала) бот выходит из войса.LOCAL_MEDIA_DIR— примонтируйте том с музыкой и укажите путь внутри контейнера, тогда заработаютlocal:и поиск по медиатеке.YTDLP_COOKIES— путь кcookies.txt, если YouTube просит подтверждения возраста или логина.
Разработка
npm install && npm --prefix web install
cp .env.example .env # STOAT_API_URL можно указать публичный адрес инстанса
npm run dev # бот + API на :3005
npm run web:dev # панель на :5180 с проксированием на :3005
Проверки: npm run typecheck, npm run build, npm --prefix web run build.
Локально нужен yt-dlp в PATH (или укажите YTDLP_PATH); ffmpeg приезжает с ffmpeg-static.
Ограничения
revoice.js— сторонняя библиотека без ретраев на разрыв LiveKit-соединения; при падении войса бот выходит из канала, повторный!joinвосстанавливает работу.- Перемотка для YouTube/SoundCloud перезапускает поток с нужной позиции (одна лишняя обращение к yt-dlp), для локальных файлов и прямых ссылок — мгновенная.
- Скачивание с YouTube формально противоречит его ToS; используйте на своё усмотрение, для «чистого» сценария есть SoundCloud, прямые ссылки и локальная медиатека.
Лицензия
MIT — см. LICENSE.