Commit Graph
8 Commits
Author SHA1 Message Date
Leonid PershinandClaude Opus 5 ec50e4c4df Read voice presence from the bot's own LiveKit room
The panel still claimed it could not see anyone in voice while the bot
and the user were sitting in the same channel: Stoat's gateway does not
send voice states to bots, so the SDK's participant list stays empty no
matter what.

While the bot is connected its LiveKit room knows exactly who is there,
so that is now consulted alongside the SDK — presence is reported as
known, the panel names the channel the user is in, and the listener rule
has real data to work with instead of falling back to trust.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 02:51:24 +03:00
Leonid PershinandClaude Opus 5 224d5679a3 Stop the listener rule from locking people out on stale presence
The panel said "you are not in a voice channel" while the client showed
the user sitting in the call, which also disabled the only button that
could summon the bot. The SDK learns voice participants from gateway
events alone — no endpoint to ask — so a bot that restarted, or missed a
VoiceChannelJoin, sees every channel as empty forever, and that looks
exactly like nobody listening.

The rule now refuses only on proven absence: when the bot can see people
in voice somewhere in the server and the requester is not among them.
With no presence data at all it accepts an explicitly chosen channel and
logs that it is trusting the request, and the panel falls back to the
channel picker instead of a dead button.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 02:34:54 +03:00
Leonid PershinandClaude Opus 5 a9b680c418 Make video a per-server toggle instead of an env-wide setting
VIDEO_ENABLED is now permission rather than behaviour: it decides whether
the feature exists at all, while turning it on for a server is a toggle in
the panel or !video in chat, off by default. Video costs real CPU for
every playing channel, so that should be a deliberate choice rather than
something a config flag switches on everywhere.

With the env flag off the panel renders no toggle at all and !video says
so, and the switch applies from the next track — swapping tracks mid-play
would cut the current one.

The loop button no longer reads "выкл" either: it sat next to the video
button showing the same word, so the two states were indistinguishable.
Both now name what they do and rely on highlighting for state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 02:13:08 +03:00
Leonid PershinandClaude Opus 5 ffcf88efb7 Translate yt-dlp errors and drop the redundant prefix hint
Errors reached people as raw tool output: "Unsupported URL:
https://...%D0%B0%D1%83..." says nothing to whoever pasted a link. The
common cases now map to plain descriptions with the next step where there
is one (age gate and bot checks point at YTDLP_COOKIES, a stale extractor
at updating yt-dlp), URLs are decoded, and anything unrecognised still
falls through verbatim.

The panel's prefix hint is gone now that the source picker does the same
job visibly; prefixes keep working and stay documented for chat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:15:25 +03:00
Leonid PershinandClaude Opus 5 971fd65b1f Add a source picker to search and use the full window width
Search now queries YouTube and SoundCloud together by default and
interleaves the two result lists so neither buries the other; a picker
left of the input narrows it to one source (plus the local library when
configured), and a prefix typed into the query still outranks it.

The panel was capped at 1180px, which left most of a wide screen empty —
it now scales to 1680px and gives the search column the extra room.

A dead link also reported "could not parse yt-dlp's response", which
described our parser rather than the problem; it now shows yt-dlp's own
error line, or says the link did not open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:11:29 +03:00
Leonid PershinandClaude Opus 5 f84489e27e Fall back to SoundCloud when YouTube hides search results
YouTube applies restricted-mode filtering to programmatic search: the
same query that returns a clip in a signed-in browser comes back as an
empty list over the API, with exit code 0 and no message at all, and
cookies do not change it. Verified locally — "gudium идол" returns three
results, "gudium аудиопорно" returns none, and so does the plain
youtube.com/results page for the same words.

An empty YouTube search now retries on SoundCloud, and when both come up
empty the message says the query may have been filtered and suggests
pasting a link, instead of implying the track does not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:07:20 +03:00
Leonid PershinandClaude Opus 5 3e53f34374 Tie playback to the listener and keep the panel's view live
Four things people hit while using the panel:

- The bot could be sent into a channel the requester was not in, and
  playback could be started from nowhere. Playback now follows the
  listener (REQUIRE_LISTENER, on by default), and the voice row shows
  where you and the bot are instead of offering a free channel picker.
- Voice presence only refreshed on reload, because the SDK updates
  channel participants without emitting an event. The socket now watches
  that view and pushes changes.
- The bot left the channel whenever the queue ran dry. It now leaves only
  after the last person does, EMPTY_TIMEOUT_SECONDS later (120 by
  default), and stays put while anyone is still listening.
- A search that yielded nothing said nothing: yt-dlp can exit 0 with an
  empty result, so that case now reports the reason (or "nothing found"),
  and searches are logged with their result count.

The queue moved under the player so search owns the left column, and
elapsed time no longer renders as "LIVE" — formatDuration treated 0 as a
live stream, which also affected the chat's progress bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:59:17 +03:00
Leonid PershinandClaude Opus 5 a9b7ccdd16 Add music bot for self-hosted Stoat with web control panel
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>
2026-09-08 23:12:43 +03:00