aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-27 18:18:07 +0100
committersalmonik <salmonik@tuta.io>2026-10-27 18:18:07 +0100
commitbab3f614f8b42545fdb0101c4e353c87e0966d47 (patch)
treec257bc4712bba088bea2eab42d93b07e9cfed89c
parentmusic: add per-guild queue with now/next/last, skip and queue (diff)
downloadsenator-sperma-bab3f614f8b42545fdb0101c4e353c87e0966d47.tar.gz
senator-sperma-bab3f614f8b42545fdb0101c4e353c87e0966d47.zip
music: group playback commands under /music
Graduate grajcuj/skip/queue/stop/join into the /music parent and drop the top-level names. On startup, reconcile global slash commands and delete any that are no longer registered, since Discord otherwise keeps stale commands after a rename or move. Docs updated for the new command layout and queue semantics.
-rw-r--r--AGENTS.md8
-rw-r--r--CONTEXT.md12
-rw-r--r--README.md10
-rw-r--r--src/commands/music.rs28
-rw-r--r--src/main.rs39
5 files changed, 66 insertions, 31 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 8352e0e..0c1c2d7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -7,12 +7,13 @@ src/
7├── main.rs — Entry: init tracing, dotenvy, build Config/Data, start poise+serenity 7├── main.rs — Entry: init tracing, dotenvy, build Config/Data, start poise+serenity
8├── config.rs — Config struct (env-driven), pub const TEMP_PATH 8├── config.rs — Config struct (env-driven), pub const TEMP_PATH
9├── error.rs — Unified Error enum + From impls, pub type Result<T> 9├── error.rs — Unified Error enum + From impls, pub type Result<T>
10├── music.rs — MusicState: per-guild queue, now/next/last, skip, auto-advance
10├── commands/ 11├── commands/
11│ ├── mod.rs — all_commands() aggregates every submodule 12│ ├── mod.rs — all_commands() aggregates every submodule
12│ ├── ai.rs — /senator (OpenRouter chat + optional TTS) 13│ ├── ai.rs — /senator (OpenRouter chat + optional TTS)
13│ ├── cron.rs — /cron group (6 subcommands) 14│ ├── cron.rs — /cron group (6 subcommands)
14│ ├── math.rs — /math (qalc subprocess) 15│ ├── math.rs — /math (qalc subprocess)
15│ ├── music.rs — /play (🟡 stub), /stop, /join, /loop_toggle 16│ ├── music.rs — /music group (grajcuj, skip, queue, stop, join) + context menu
16│ ├── qr.rs — /qr encode + decode 17│ ├── qr.rs — /qr encode + decode
17│ ├── trigger.rs — /trigger group (6 subcommands) 18│ ├── trigger.rs — /trigger group (6 subcommands)
18│ ├── tts.rs — /tts (local Piper TTS → file/vc), /log_volume 19│ ├── tts.rs — /tts (local Piper TTS → file/vc), /log_volume
@@ -41,12 +42,11 @@ src/
41- `build.rs` — compiles C stubs for espeak-rs-sys missing sonic/audio_object symbols 42- `build.rs` — compiles C stubs for espeak-rs-sys missing sonic/audio_object symbols
42 43
43## Stubs to Fill (see COMPARISON.md) 44## Stubs to Fill (see COMPARISON.md)
44- `commands/music.rs` — `/play` is placeholder
45- `commands/utilities.rs` — `/compress`, `/convert`, `/download` upload part 45- `commands/utilities.rs` — `/compress`, `/convert`, `/download` upload part
46- `tasks/mod.rs` — cron execution bodies are `info!` only, no actual action 46- `tasks/mod.rs` — cron execution bodies are `info!` only, no actual action
47 47
48## Database Tables 48## Database Tables
49`settings` (key/value), `voice_activity`, `cron_jobs`, `triggers`, `trigger_keywords`, `trigger_responses` 49`settings` (key/value), `voice_activity`, `cron_jobs`, `triggers`, `trigger_keywords`, `trigger_responses`
50 50
51## All Commands (26 total) 51## Commands
52`/senator`, `/cron` (6), `/math`, `/play`, `/stop`, `/join`, `/loop_toggle`, `/qr` (2), `/trigger` (6), `/tts`, `/log_volume`, `/compress`, `/convert`, `/upload`, `/search`, `/download`, `/say`, `/web` 52`/senator`, `/cron` (6), `/math`, `/music` (5: grajcuj, skip, queue, stop, join), `/qr` (2), `/trigger` (6), `/tts`, `/log_volume`, `/compress`, `/convert`, `/upload`, `/search`, `/download`, `/say`, `/web`, `/record`, `/leaderboard`
diff --git a/CONTEXT.md b/CONTEXT.md
index 990dc8e..278420f 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -50,7 +50,17 @@ holds no implementation decisions or specs (those live in `docs/adr/`).
50 50
51## Music Playback 51## Music Playback
52 52
53- **Compressed playback** — `/grajcuj \w compressed`: downloads audio, runs 53- **Queue** — per-guild, in-memory list of tracks waiting to play. Lost on
54 restart. `/music queue` shows the current track first, then the pending
55 items in play order.
56- **Position** — where `/music grajcuj` inserts a track: `now` interrupts the
57 current track, `next` inserts after it, `last` appends (default). When
58 nothing is playing, every position starts playback immediately.
59- **Skip** — stop the current track and play the next queued one. With an
60 empty queue, playback stops but the bot stays in the voice channel.
61- **Auto-advance** — when a track ends (or errors), the next queued track is
62 started automatically and announced in the channel.
63- **Compressed playback** — `/music grajcuj ... compressed`: downloads audio, runs
54 through the /compress pipeline (Opus 500bps, 12 kHz, mono → OGG), then 64 through the /compress pipeline (Opus 500bps, 12 kHz, mono → OGG), then
55 decodes the compressed file via symphonia for playback. Produces properly 65 decodes the compressed file via symphonia for playback. Produces properly
56 destroyed audio via Opus codec, not just raw low-sample-rate PCM. 66 destroyed audio via Opus codec, not just raw low-sample-rate PCM.
diff --git a/README.md b/README.md
index 55ca02d..41ef0c3 100644
--- a/README.md
+++ b/README.md
@@ -66,9 +66,11 @@ servers.
66- `/senator` — chat with the OpenRouter model 66- `/senator` — chat with the OpenRouter model
67 67
68### Music (songbird) 68### Music (songbird)
69- `/grajcuj` — play from Copyparty search, a direct URL, or an uploaded file 69- `/music grajcuj` — play from Copyparty search, a direct URL, or an uploaded file; optional `position` (`now`, `next`, `last`, default `last`)
70- `/stop` — stop playback and leave voice 70- `/music queue` — show the current track followed by the pending queue
71- `/join` — join the caller's voice channel 71- `/music skip` — stop the current track and play the next queued one
72- `/music stop` — stop playback, clear the queue, and leave voice
73- `/music join` — join the caller's voice channel
72 74
73### TTS 75### TTS
74- `/tts` — local Piper TTS (Polish, Google Translate fallback), sent as a file or played in voice 76- `/tts` — local Piper TTS (Polish, Google Translate fallback), sent as a file or played in voice
@@ -112,7 +114,7 @@ servers.
112 114
113The runtime is organised around a shared `Data` struct (poise `user_data`) 115The runtime is organised around a shared `Data` struct (poise `user_data`)
114that holds the DB pool, HTTP client, Copyparty client, voice manager, the 116that holds the DB pool, HTTP client, Copyparty client, voice manager, the
115in-memory trigger cache, autocomplete cache, and per-guild music track handles. 117in-memory trigger cache, autocomplete cache, and per-guild music queue.
116 118
117- `src/main.rs` — boot: tracing, dotenvy, Config, DB pool, framework, cron task 119- `src/main.rs` — boot: tracing, dotenvy, Config, DB pool, framework, cron task
118- `src/config.rs` — env-driven `Config` + variable validation 120- `src/config.rs` — env-driven `Config` + variable validation
diff --git a/src/commands/music.rs b/src/commands/music.rs
index 8841329..26e655e 100644
--- a/src/commands/music.rs
+++ b/src/commands/music.rs
@@ -15,7 +15,10 @@ type Context<'a> = poise::Context<'a, Data, Error>;
15// ── Source classification ────────────────────────────────────────── 15// ── Source classification ──────────────────────────────────────────
16 16
17/// Music playback and queue management. 17/// Music playback and queue management.
18#[poise::command(slash_command, subcommands("grajcuj", "skip", "queue"))] 18#[poise::command(
19 slash_command,
20 subcommands("grajcuj", "skip", "queue", "stop", "join")
21)]
19pub async fn music(_ctx: Context<'_>) -> Result<()> { 22pub async fn music(_ctx: Context<'_>) -> Result<()> {
20 Ok(()) 23 Ok(())
21} 24}
@@ -29,25 +32,8 @@ pub async fn grajcuj(
29 melodia: Option<String>, 32 melodia: Option<String>,
30 #[description = "Bezpośredni plik z twojego systemu"] file: Option<serenity::Attachment>, 33 #[description = "Bezpośredni plik z twojego systemu"] file: Option<serenity::Attachment>,
31 #[description = "Chujowa kompresja!?"] compressed: Option<bool>, 34 #[description = "Chujowa kompresja!?"] compressed: Option<bool>,
32 #[description = "Gdzie dodać utwór w kolejce (domyślnie: last)"] position: Option< 35 #[description = "Gdzie dodać utwór w kolejce (domyślnie: last)"]
33 QueuePosition, 36 position: Option<QueuePosition>,
34 >,
35) -> Result<()> {
36 play_command(ctx, melodia, file, compressed, position).await
37}
38
39/// Play music from Copyparty, URL, or file upload (top-level alias)
40#[poise::command(slash_command, rename = "grajcuj")]
41pub async fn grajcuj_top(
42 ctx: Context<'_>,
43 #[description = "Plik do odtworzenia"]
44 #[autocomplete = "audio_file"]
45 melodia: Option<String>,
46 #[description = "Bezpośredni plik z twojego systemu"] file: Option<serenity::Attachment>,
47 #[description = "Chujowa kompresja!?"] compressed: Option<bool>,
48 #[description = "Gdzie dodać utwór w kolejce (domyślnie: last)"] position: Option<
49 QueuePosition,
50 >,
51) -> Result<()> { 37) -> Result<()> {
52 play_command(ctx, melodia, file, compressed, position).await 38 play_command(ctx, melodia, file, compressed, position).await
53} 39}
@@ -404,5 +390,5 @@ pub async fn message_grajcuj(ctx: Context<'_>, message: serenity::Message) -> Re
404} 390}
405 391
406pub fn commands() -> Vec<poise::Command<Data, Error>> { 392pub fn commands() -> Vec<poise::Command<Data, Error>> {
407 vec![music(), grajcuj_top(), stop(), join(), message_grajcuj()] 393 vec![music(), message_grajcuj()]
408} 394}
diff --git a/src/main.rs b/src/main.rs
index 4e308b8..8d11d76 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,4 @@
1use std::collections::HashMap; 1use std::collections::{HashMap, HashSet};
2use std::sync::Arc; 2use std::sync::Arc;
3use std::time::{Duration, Instant}; 3use std::time::{Duration, Instant};
4 4
@@ -236,6 +236,43 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
236 236
237 poise::builtins::register_globally(ctx, &framework.options().commands).await?; 237 poise::builtins::register_globally(ctx, &framework.options().commands).await?;
238 238
239 // Discord keeps global commands that disappear from the
240 // registration payload. Delete leftovers from renamed or
241 // removed commands so the move under `/music` is visible.
242 let expected: HashSet<String> = framework
243 .options()
244 .commands
245 .iter()
246 .map(|command| {
247 command
248 .context_menu_name
249 .clone()
250 .unwrap_or_else(|| command.name.clone())
251 })
252 .collect();
253 match ctx.http.get_global_commands().await {
254 Ok(existing) => {
255 for command in existing {
256 if !expected.contains(&command.name) {
257 match ctx.http.delete_global_command(command.id).await {
258 Ok(()) => tracing::info!(
259 command = %command.name,
260 "removed stale global command"
261 ),
262 Err(e) => tracing::warn!(
263 command = %command.name,
264 error = %e,
265 "failed to remove stale global command"
266 ),
267 }
268 }
269 }
270 }
271 Err(e) => {
272 tracing::warn!(error = %e, "could not list global commands for cleanup")
273 }
274 }
275
239 events::handle_ready(ctx).await.map_err(|e| { 276 events::handle_ready(ctx).await.map_err(|e| {
240 tracing::error!("on_ready handler: {e}"); 277 tracing::error!("on_ready handler: {e}");
241 e 278 e