diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-27 18:14:07 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-27 18:14:07 +0100 |
| commit | fc5e0f48477366641b97efb0541053cc1e061be5 (patch) | |
| tree | 99ad0352552a0eebc527f3184a11ad2e7676ffea /src/tasks | |
| parent | silence songbird udp_rx decode noise (diff) | |
| download | senator-sperma-fc5e0f48477366641b97efb0541053cc1e061be5.tar.gz senator-sperma-fc5e0f48477366641b97efb0541053cc1e061be5.zip | |
music: add per-guild queue with now/next/last, skip and queue
Tracks are queued in memory per guild, defaulting to append (last).
Playback auto-advances on TrackEvent::End/Error with a per-track token
so manual stops and skips cannot double-advance. /music queue shows the
current track followed by pending items. /stop clears the queue.
Cron audio now goes through the same MusicState instead of poking the
track-handle map directly.
Diffstat (limited to '')
| -rw-r--r-- | src/tasks/mod.rs | 67 |
1 files changed, 23 insertions, 44 deletions
diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index d4cf9b2..ab0d062 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs | |||
| @@ -1,4 +1,3 @@ | |||
| 1 | use std::collections::HashMap; | ||
| 2 | use std::sync::Arc; | 1 | use std::sync::Arc; |
| 3 | 2 | ||
| 4 | use chrono::{DateTime, Duration, Local, TimeZone, Timelike, Utc}; | 3 | use chrono::{DateTime, Duration, Local, TimeZone, Timelike, Utc}; |
| @@ -10,16 +9,15 @@ use tracing::{error, info}; | |||
| 10 | 9 | ||
| 11 | use crate::copyparty::CopypartyClient; | 10 | use crate::copyparty::CopypartyClient; |
| 12 | use crate::database::{self, DbPool}; | 11 | use crate::database::{self, DbPool}; |
| 13 | use crate::voice::VoiceManager; | 12 | use crate::music::{MusicState, SourceKind, TrackRequest}; |
| 14 | 13 | ||
| 15 | /// Shared resources the cron scheduler needs. | 14 | /// Shared resources the cron scheduler needs. |
| 16 | #[derive(Clone)] | 15 | #[derive(Clone)] |
| 17 | pub struct CronContext { | 16 | pub struct CronContext { |
| 18 | pub pool: DbPool, | 17 | pub pool: DbPool, |
| 19 | pub http: Arc<serenity::Http>, | 18 | pub http: Arc<serenity::Http>, |
| 20 | pub voice_manager: VoiceManager, | ||
| 21 | pub copyparty: CopypartyClient, | 19 | pub copyparty: CopypartyClient, |
| 22 | pub music_tracks: Arc<tokio::sync::Mutex<HashMap<u64, songbird::tracks::TrackHandle>>>, | 20 | pub music: Arc<MusicState>, |
| 23 | } | 21 | } |
| 24 | 22 | ||
| 25 | /// Normalize user-friendly cron expressions to the 7-field format the `cron` crate expects. | 23 | /// Normalize user-friendly cron expressions to the 7-field format the `cron` crate expects. |
| @@ -139,14 +137,9 @@ async fn execute_audio_job(ctx: &CronContext, job: &crate::database::CronJob) { | |||
| 139 | // Legacy relative path — build full URL with password | 137 | // Legacy relative path — build full URL with password |
| 140 | ctx.copyparty.build_url(audio_url) | 138 | ctx.copyparty.build_url(audio_url) |
| 141 | }; | 139 | }; |
| 142 | info!("Cron audio job {} streaming via ffmpeg: {url}", job.id); | 140 | info!("Cron audio job {} playing: {url}", job.id); |
| 143 | let guild_channel_fut = channel_id.to_channel(&ctx.http); | 141 | let channel = match channel_id.to_channel(&ctx.http).await { |
| 144 | let input_fut = crate::voice::ffmpeg_stream_input(&url); | 142 | Ok(serenity::Channel::Guild(ch)) => ch, |
| 145 | |||
| 146 | let (channel_result, input_result) = tokio::join!(guild_channel_fut, input_fut); | ||
| 147 | |||
| 148 | let guild_id = match channel_result { | ||
| 149 | Ok(serenity::Channel::Guild(ch)) => ch.guild_id, | ||
| 150 | other => { | 143 | other => { |
| 151 | error!( | 144 | error!( |
| 152 | "Cron audio job {}: channel {} is not a guild voice channel ({:?})", | 145 | "Cron audio job {}: channel {} is not a guild voice channel ({:?})", |
| @@ -158,45 +151,31 @@ async fn execute_audio_job(ctx: &CronContext, job: &crate::database::CronJob) { | |||
| 158 | } | 151 | } |
| 159 | }; | 152 | }; |
| 160 | 153 | ||
| 161 | let input = match input_result { | 154 | let request = TrackRequest { |
| 162 | Ok(i) => i, | 155 | url: url.clone(), |
| 163 | Err(e) => { | 156 | display_name: audio_url |
| 164 | error!( | 157 | .rsplit('/') |
| 165 | "Cron audio job {} ffmpeg stream failed ({url}): {e}", | 158 | .next() |
| 166 | job.id | 159 | .unwrap_or(audio_url) |
| 167 | ); | 160 | .to_string(), |
| 168 | return; | 161 | kind: SourceKind::Stream, |
| 169 | } | 162 | compressed: false, |
| 163 | reupload: None, | ||
| 164 | requester: None, | ||
| 170 | }; | 165 | }; |
| 171 | 166 | ||
| 172 | // Stop any existing track in this guild. | 167 | if let Err(e) = ctx |
| 168 | .music | ||
| 169 | .play_now(channel.guild_id.get(), channel_id, None, request) | ||
| 170 | .await | ||
| 173 | { | 171 | { |
| 174 | let mut map = ctx.music_tracks.lock().await; | 172 | error!("Cron audio job {} playback failed: {e}", job.id); |
| 175 | if let Some(handle) = map.remove(&guild_id.get()) { | 173 | return; |
| 176 | let _ = handle.stop(); | ||
| 177 | } | ||
| 178 | } | 174 | } |
| 179 | 175 | ||
| 180 | // Connect to voice and play. | ||
| 181 | let call = match ctx.voice_manager.connect(guild_id, channel_id).await { | ||
| 182 | Ok(c) => c, | ||
| 183 | Err(e) => { | ||
| 184 | error!("Cron audio job {} voice connect failed: {e}", job.id); | ||
| 185 | return; | ||
| 186 | } | ||
| 187 | }; | ||
| 188 | |||
| 189 | let handle = { | ||
| 190 | let mut call_lock = call.lock().await; | ||
| 191 | call_lock.play_input(input) | ||
| 192 | }; | ||
| 193 | |||
| 194 | // Store the handle so the track stays alive. | ||
| 195 | ctx.music_tracks.lock().await.insert(guild_id.get(), handle); | ||
| 196 | |||
| 197 | info!( | 176 | info!( |
| 198 | "Cron audio job {} playing '{}' (url: {url}) in guild {}", | 177 | "Cron audio job {} playing '{}' (url: {url}) in guild {}", |
| 199 | job.id, audio_url, guild_id | 178 | job.id, audio_url, channel.guild_id |
| 200 | ); | 179 | ); |
| 201 | } | 180 | } |
| 202 | 181 | ||
