aboutsummaryrefslogtreecommitdiffstats
path: root/src/tasks/mod.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-27 18:14:07 +0100
committersalmonik <salmonik@tuta.io>2026-10-27 18:14:07 +0100
commitfc5e0f48477366641b97efb0541053cc1e061be5 (patch)
tree99ad0352552a0eebc527f3184a11ad2e7676ffea /src/tasks/mod.rs
parentsilence songbird udp_rx decode noise (diff)
downloadsenator-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.rs67
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 @@
1use std::collections::HashMap;
2use std::sync::Arc; 1use std::sync::Arc;
3 2
4use chrono::{DateTime, Duration, Local, TimeZone, Timelike, Utc}; 3use chrono::{DateTime, Duration, Local, TimeZone, Timelike, Utc};
@@ -10,16 +9,15 @@ use tracing::{error, info};
10 9
11use crate::copyparty::CopypartyClient; 10use crate::copyparty::CopypartyClient;
12use crate::database::{self, DbPool}; 11use crate::database::{self, DbPool};
13use crate::voice::VoiceManager; 12use 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)]
17pub struct CronContext { 16pub 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