From 850fad62f12b4e764f5e64036db35259237994cb Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 19 Aug 2026 02:45:49 +0200 Subject: fix: Various cron fixes Not saving full URL Not using autocompletion content, just label --- src/commands/cron.rs | 12 +++++++++++- src/copyparty/mod.rs | 15 ++++++++++----- src/database/mod.rs | 19 +++++++++++++++++-- src/main.rs | 7 +++++-- src/tasks/mod.rs | 37 ++++++++++++++++++++++++++++++------- 5 files changed, 73 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/commands/cron.rs b/src/commands/cron.rs index 48f6ff1..a645581 100644 --- a/src/commands/cron.rs +++ b/src/commands/cron.rs @@ -32,12 +32,22 @@ pub async fn dodaj_audio( _ => return Err(lang::err_bad_channel_type()), }; + // Resolve full path from autocomplete cache (value is truncated to 99 chars by Discord) + let full_url = if let Some(hit) = ctx.data().ac_lookup(ctx.author().id, &audio_filename).await { + let path = hit.url_path().unwrap_or(&audio_filename); + ctx.data().copyparty.build_url_no_pw(path) + } else if audio_filename.starts_with("http://") || audio_filename.starts_with("https://") { + audio_filename.clone() + } else { + ctx.data().copyparty.build_url_no_pw(&audio_filename) + }; + database::add_cron_job( &ctx.data().db_pool, &cron_expression, "audio", channel_id, - Some(&audio_filename), + Some(&full_url), None, ) .await?; diff --git a/src/copyparty/mod.rs b/src/copyparty/mod.rs index 164c9f8..a9f7d01 100644 --- a/src/copyparty/mod.rs +++ b/src/copyparty/mod.rs @@ -158,11 +158,7 @@ impl CopypartyClient { /// if a password is configured. Path can be absolute (e.g. `/music/foo.mp3`) /// or relative (`music/foo.mp3`). pub fn build_url(&self, path: &str) -> String { - let prefixed = if path.starts_with('/') { - format!("{}{}", self.base_url, path) - } else { - format!("{}/{}", self.base_url, path) - }; + let prefixed = self.build_url_no_pw(path); if let Some(pw) = &self.password { format!("{prefixed}?pw={pw}") } else { @@ -170,6 +166,15 @@ impl CopypartyClient { } } + /// Build full URL without the password parameter. + pub fn build_url_no_pw(&self, path: &str) -> String { + if path.starts_with('/') { + format!("{}{}", self.base_url, path) + } else { + format!("{}/{}", self.base_url, path) + } + } + /// Search copyparty via the jPOST `/?srch` endpoint. /// /// Uses `serde_json::Value` for flexible response parsing (mirroring the Python diff --git a/src/database/mod.rs b/src/database/mod.rs index e803da9..0674477 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -43,7 +43,7 @@ async fn run_migrations(pool: &DbPool) -> Result<()> { sqlx::query( "CREATE TABLE IF NOT EXISTS cron_jobs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id INTEGER PRIMARY KEY, cron_expression TEXT NOT NULL, type TEXT NOT NULL CHECK(type IN ('audio', 'wiadomość')), channel_id INTEGER NOT NULL, @@ -189,9 +189,24 @@ pub async fn add_cron_job( audio_filename: Option<&str>, message: Option<&str>, ) -> Result { + // Find the lowest unused ID to reuse freed slots + let existing: Vec = sqlx::query_scalar("SELECT id FROM cron_jobs ORDER BY id") + .fetch_all(pool) + .await?; + + let next_id = existing.iter().enumerate().find_map(|(i, &id)| { + let expected = (i as i64) + 1; + if id != expected { + Some(expected) + } else { + None + } + }).unwrap_or(existing.len() as i64 + 1); + let result = sqlx::query( - "INSERT INTO cron_jobs (cron_expression, type, channel_id, audio_filename, message) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO cron_jobs (id, cron_expression, type, channel_id, audio_filename, message) VALUES (?, ?, ?, ?, ?, ?)", ) + .bind(next_id) .bind(cron_expression) .bind(r#type) .bind(channel_id) diff --git a/src/main.rs b/src/main.rs index 31c9e94..7f57421 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,7 +49,7 @@ pub struct Data { /// next keystroke's autocomplete can serve real results immediately. pub ac_search_cache: AcSearchCache, /// Active music track per guild (only one music source at a time). - pub music_tracks: tokio::sync::Mutex>, + pub music_tracks: Arc>>, } impl Data { @@ -185,6 +185,8 @@ async fn main() -> Result<(), Box> { Box::pin(async move { let vm = voice::VoiceManager::new(ctx).await?; + let music_tracks: Arc>> = Arc::new(tokio::sync::Mutex::new(HashMap::new())); + let data = Data { config, db_pool, @@ -195,7 +197,7 @@ async fn main() -> Result<(), Box> { triggers: tokio::sync::Mutex::new(triggers), ac_cache: tokio::sync::Mutex::new(HashMap::new()), ac_search_cache: Arc::new(tokio::sync::Mutex::new(HashMap::new())), - music_tracks: tokio::sync::Mutex::new(HashMap::new()), + music_tracks: music_tracks.clone(), }; // Spawn cron scheduler loop. @@ -205,6 +207,7 @@ async fn main() -> Result<(), Box> { http: ctx.http.clone(), voice_manager: data.voice_manager.clone(), copyparty: data.copyparty.clone(), + music_tracks: music_tracks.clone(), }; tokio::spawn(async move { crate::tasks::cron_scheduler_loop(cron_ctx).await; diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index 4408857..d4cf9b2 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::Arc; use chrono::{DateTime, Duration, Local, TimeZone, Timelike, Utc}; @@ -18,6 +19,7 @@ pub struct CronContext { pub http: Arc, pub voice_manager: VoiceManager, pub copyparty: CopypartyClient, + pub music_tracks: Arc>>, } /// Normalize user-friendly cron expressions to the 7-field format the `cron` crate expects. @@ -118,15 +120,25 @@ async fn execute_message_job(ctx: &CronContext, job: &crate::database::CronJob) } async fn execute_audio_job(ctx: &CronContext, job: &crate::database::CronJob) { - let Some(ref audio_path) = job.audio_filename else { + let Some(ref audio_url) = job.audio_filename else { error!("Cron audio job {} has no audio_filename", job.id); return; }; let channel_id = serenity::ChannelId::new(job.channel_id as u64); - // Resolve guild + build URL concurrently. - let url = ctx.copyparty.build_url(audio_path); + // Build the full URL with password + let url = if audio_url.starts_with("http://") || audio_url.starts_with("https://") { + // Already a full URL (stored without password), append it now + if let Some(pw) = &ctx.copyparty.password { + format!("{audio_url}?pw={pw}") + } else { + audio_url.clone() + } + } else { + // 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); @@ -157,6 +169,14 @@ async fn execute_audio_job(ctx: &CronContext, job: &crate::database::CronJob) { } }; + // Stop any existing track in this guild. + { + let mut map = ctx.music_tracks.lock().await; + if let Some(handle) = map.remove(&guild_id.get()) { + let _ = handle.stop(); + } + } + // Connect to voice and play. let call = match ctx.voice_manager.connect(guild_id, channel_id).await { Ok(c) => c, @@ -166,14 +186,17 @@ async fn execute_audio_job(ctx: &CronContext, job: &crate::database::CronJob) { } }; - { + let handle = { let mut call_lock = call.lock().await; - call_lock.play_input(input); - } + 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_path, guild_id + job.id, audio_url, guild_id ); } -- cgit v1.2.3