From fc5e0f48477366641b97efb0541053cc1e061be5 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 27 Oct 2026 18:14:07 +0100 Subject: 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. --- src/tasks/mod.rs | 67 +++++++++++++++++++------------------------------------- 1 file changed, 23 insertions(+), 44 deletions(-) (limited to 'src/tasks') 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 @@ -use std::collections::HashMap; use std::sync::Arc; use chrono::{DateTime, Duration, Local, TimeZone, Timelike, Utc}; @@ -10,16 +9,15 @@ use tracing::{error, info}; use crate::copyparty::CopypartyClient; use crate::database::{self, DbPool}; -use crate::voice::VoiceManager; +use crate::music::{MusicState, SourceKind, TrackRequest}; /// Shared resources the cron scheduler needs. #[derive(Clone)] pub struct CronContext { pub pool: DbPool, pub http: Arc, - pub voice_manager: VoiceManager, pub copyparty: CopypartyClient, - pub music_tracks: Arc>>, + pub music: Arc, } /// 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) { // Legacy relative path — build full URL with password ctx.copyparty.build_url(audio_url) }; - info!("Cron audio job {} streaming via ffmpeg: {url}", job.id); - let guild_channel_fut = channel_id.to_channel(&ctx.http); - let input_fut = crate::voice::ffmpeg_stream_input(&url); - - let (channel_result, input_result) = tokio::join!(guild_channel_fut, input_fut); - - let guild_id = match channel_result { - Ok(serenity::Channel::Guild(ch)) => ch.guild_id, + info!("Cron audio job {} playing: {url}", job.id); + let channel = match channel_id.to_channel(&ctx.http).await { + Ok(serenity::Channel::Guild(ch)) => ch, other => { error!( "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) { } }; - let input = match input_result { - Ok(i) => i, - Err(e) => { - error!( - "Cron audio job {} ffmpeg stream failed ({url}): {e}", - job.id - ); - return; - } + let request = TrackRequest { + url: url.clone(), + display_name: audio_url + .rsplit('/') + .next() + .unwrap_or(audio_url) + .to_string(), + kind: SourceKind::Stream, + compressed: false, + reupload: None, + requester: None, }; - // Stop any existing track in this guild. + if let Err(e) = ctx + .music + .play_now(channel.guild_id.get(), channel_id, None, request) + .await { - let mut map = ctx.music_tracks.lock().await; - if let Some(handle) = map.remove(&guild_id.get()) { - let _ = handle.stop(); - } + error!("Cron audio job {} playback failed: {e}", job.id); + return; } - // Connect to voice and play. - let call = match ctx.voice_manager.connect(guild_id, channel_id).await { - Ok(c) => c, - Err(e) => { - error!("Cron audio job {} voice connect failed: {e}", job.id); - return; - } - }; - - let handle = { - let mut call_lock = call.lock().await; - call_lock.play_input(input) - }; - - // Store the handle so the track stays alive. - ctx.music_tracks.lock().await.insert(guild_id.get(), handle); - info!( "Cron audio job {} playing '{}' (url: {url}) in guild {}", - job.id, audio_url, guild_id + job.id, audio_url, channel.guild_id ); } -- cgit v1.2.3