Commit Graph
7 Commits
Author SHA1 Message Date
Leonid PershinandClaude Opus 5 a63db8c957 Clear the search list after a pick, and stop double-announcing tracks
Two bits of clutter visible in one screenshot: the results list stayed in
the channel after a track was chosen, still wearing its reactions, and
every track produced two lines — the command's own reply and the player's
"now playing" notice.

Picking now spends the session and deletes the list, so a second reaction
does nothing and the message goes away. The player announces a track only
when it started one by itself; when a command started it, that command
has already said so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 06:04:17 +03:00
Leonid PershinandClaude Opus 5 bf2d04d579 Answer the search command immediately, and don't wait on a slow source
A search looked like it did nothing: the log shows the two sources
finishing 21 seconds apart, and since the command message is deleted on
sight, the channel stayed empty that whole time with no sign the bot had
heard anything.

The reply now goes out at once as "Ищу …" and is edited into the results,
so there is always something on screen, and each source gets 12 seconds
before the answer goes out without it — searching several at once
otherwise means always waiting for the slowest, which behind a proxy is
tens of seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 05:49:50 +03:00
Leonid PershinandClaude Opus 5 e7e80e704b Tidy chat output and pick search results by reaction
A search reply carried ten markdown links, and Stoat expands every link
into a full-size player — the result was a wall of embeds burying the
list. Stoat's embed generator skips links inside code spans or angle
brackets, so links now go out quietly, and list lines carry no links at
all: title, artist, length, source. Only `!nowplaying` keeps a preview,
where it was asked for.

Choosing a track no longer needs a second command: the results message
gets 1️⃣5️⃣ reactions and picking one queues the track, with `!pick`
still there when reactions fail or five results are not enough. Only the
person who searched can pick, so a list cannot be hijacked.

Also `!playvideo` to turn video on and queue a track in one go, and
setVideo no longer requires a running player — the switch is a setting,
and demanding a player made it fail on a server nothing had played on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 05:41:20 +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 6d65134f8e Add only the track when a link carries both a video and a list
A link copied from an open mix (watch?v=…&list=RD…) expanded into the
whole radio station — 500 entries from one paste. Any URL with a `v=`
parameter now resolves to that single track, and only a /playlist?list=…
URL expands. The rule is deliberately blunt rather than keyed on
start_radio, so pasting a link behaves the same way every time.

Playlist expansion is also capped separately from the queue limit
(MAX_PLAYLIST_TRACKS, 100 by default) and the chat says when a playlist
hit that ceiling.

Verified against the real yt-dlp: both mix links resolve to one track, a
plain video link to one, and a playlist URL to its 13 entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:20:35 +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 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