From bab3f614f8b42545fdb0101c4e353c87e0966d47 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 27 Oct 2026 18:18:07 +0100 Subject: 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. --- AGENTS.md | 8 ++++---- CONTEXT.md | 12 +++++++++++- README.md | 10 ++++++---- src/commands/music.rs | 28 +++++++--------------------- src/main.rs | 39 ++++++++++++++++++++++++++++++++++++++- 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/ ├── main.rs — Entry: init tracing, dotenvy, build Config/Data, start poise+serenity ├── config.rs — Config struct (env-driven), pub const TEMP_PATH ├── error.rs — Unified Error enum + From impls, pub type Result +├── music.rs — MusicState: per-guild queue, now/next/last, skip, auto-advance ├── commands/ │ ├── mod.rs — all_commands() aggregates every submodule │ ├── ai.rs — /senator (OpenRouter chat + optional TTS) │ ├── cron.rs — /cron group (6 subcommands) │ ├── math.rs — /math (qalc subprocess) -│ ├── music.rs — /play (🟡 stub), /stop, /join, /loop_toggle +│ ├── music.rs — /music group (grajcuj, skip, queue, stop, join) + context menu │ ├── qr.rs — /qr encode + decode │ ├── trigger.rs — /trigger group (6 subcommands) │ ├── tts.rs — /tts (local Piper TTS → file/vc), /log_volume @@ -41,12 +42,11 @@ src/ - `build.rs` — compiles C stubs for espeak-rs-sys missing sonic/audio_object symbols ## Stubs to Fill (see COMPARISON.md) -- `commands/music.rs` — `/play` is placeholder - `commands/utilities.rs` — `/compress`, `/convert`, `/download` upload part - `tasks/mod.rs` — cron execution bodies are `info!` only, no actual action ## Database Tables `settings` (key/value), `voice_activity`, `cron_jobs`, `triggers`, `trigger_keywords`, `trigger_responses` -## All Commands (26 total) -`/senator`, `/cron` (6), `/math`, `/play`, `/stop`, `/join`, `/loop_toggle`, `/qr` (2), `/trigger` (6), `/tts`, `/log_volume`, `/compress`, `/convert`, `/upload`, `/search`, `/download`, `/say`, `/web` +## Commands +`/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/`). ## Music Playback -- **Compressed playback** — `/grajcuj \w compressed`: downloads audio, runs +- **Queue** — per-guild, in-memory list of tracks waiting to play. Lost on + restart. `/music queue` shows the current track first, then the pending + items in play order. +- **Position** — where `/music grajcuj` inserts a track: `now` interrupts the + current track, `next` inserts after it, `last` appends (default). When + nothing is playing, every position starts playback immediately. +- **Skip** — stop the current track and play the next queued one. With an + empty queue, playback stops but the bot stays in the voice channel. +- **Auto-advance** — when a track ends (or errors), the next queued track is + started automatically and announced in the channel. +- **Compressed playback** — `/music grajcuj ... compressed`: downloads audio, runs through the /compress pipeline (Opus 500bps, 12 kHz, mono → OGG), then decodes the compressed file via symphonia for playback. Produces properly 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. - `/senator` — chat with the OpenRouter model ### Music (songbird) -- `/grajcuj` — play from Copyparty search, a direct URL, or an uploaded file -- `/stop` — stop playback and leave voice -- `/join` — join the caller's voice channel +- `/music grajcuj` — play from Copyparty search, a direct URL, or an uploaded file; optional `position` (`now`, `next`, `last`, default `last`) +- `/music queue` — show the current track followed by the pending queue +- `/music skip` — stop the current track and play the next queued one +- `/music stop` — stop playback, clear the queue, and leave voice +- `/music join` — join the caller's voice channel ### TTS - `/tts` — local Piper TTS (Polish, Google Translate fallback), sent as a file or played in voice @@ -112,7 +114,7 @@ servers. The runtime is organised around a shared `Data` struct (poise `user_data`) that holds the DB pool, HTTP client, Copyparty client, voice manager, the -in-memory trigger cache, autocomplete cache, and per-guild music track handles. +in-memory trigger cache, autocomplete cache, and per-guild music queue. - `src/main.rs` — boot: tracing, dotenvy, Config, DB pool, framework, cron task - `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>; // ── Source classification ────────────────────────────────────────── /// Music playback and queue management. -#[poise::command(slash_command, subcommands("grajcuj", "skip", "queue"))] +#[poise::command( + slash_command, + subcommands("grajcuj", "skip", "queue", "stop", "join") +)] pub async fn music(_ctx: Context<'_>) -> Result<()> { Ok(()) } @@ -29,25 +32,8 @@ pub async fn grajcuj( melodia: Option, #[description = "Bezpośredni plik z twojego systemu"] file: Option, #[description = "Chujowa kompresja!?"] compressed: Option, - #[description = "Gdzie dodać utwór w kolejce (domyślnie: last)"] position: Option< - QueuePosition, - >, -) -> Result<()> { - play_command(ctx, melodia, file, compressed, position).await -} - -/// Play music from Copyparty, URL, or file upload (top-level alias) -#[poise::command(slash_command, rename = "grajcuj")] -pub async fn grajcuj_top( - ctx: Context<'_>, - #[description = "Plik do odtworzenia"] - #[autocomplete = "audio_file"] - melodia: Option, - #[description = "Bezpośredni plik z twojego systemu"] file: Option, - #[description = "Chujowa kompresja!?"] compressed: Option, - #[description = "Gdzie dodać utwór w kolejce (domyślnie: last)"] position: Option< - QueuePosition, - >, + #[description = "Gdzie dodać utwór w kolejce (domyślnie: last)"] + position: Option, ) -> Result<()> { play_command(ctx, melodia, file, compressed, position).await } @@ -404,5 +390,5 @@ pub async fn message_grajcuj(ctx: Context<'_>, message: serenity::Message) -> Re } pub fn commands() -> Vec> { - vec![music(), grajcuj_top(), stop(), join(), message_grajcuj()] + vec![music(), message_grajcuj()] } 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 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -236,6 +236,43 @@ async fn main() -> Result<(), Box> { poise::builtins::register_globally(ctx, &framework.options().commands).await?; + // Discord keeps global commands that disappear from the + // registration payload. Delete leftovers from renamed or + // removed commands so the move under `/music` is visible. + let expected: HashSet = framework + .options() + .commands + .iter() + .map(|command| { + command + .context_menu_name + .clone() + .unwrap_or_else(|| command.name.clone()) + }) + .collect(); + match ctx.http.get_global_commands().await { + Ok(existing) => { + for command in existing { + if !expected.contains(&command.name) { + match ctx.http.delete_global_command(command.id).await { + Ok(()) => tracing::info!( + command = %command.name, + "removed stale global command" + ), + Err(e) => tracing::warn!( + command = %command.name, + error = %e, + "failed to remove stale global command" + ), + } + } + } + } + Err(e) => { + tracing::warn!(error = %e, "could not list global commands for cleanup") + } + } + events::handle_ready(ctx).await.map_err(|e| { tracing::error!("on_ready handler: {e}"); e -- cgit v1.2.3