use grate::tracing; use serenity::{ all::UserId, prelude::*, }; use songbird::{ input::{ AuxMetadata, Compose, YoutubeDl, }, Call, Songbird, }; use std::{ sync::Arc, time::Duration, }; use tap::Conv; use crate::{ bot::{ HttpKey, PlaybackKey, }, db, util, PoiseContext, PoiseData, }; pub fn commands() -> impl IntoIterator> { vec![play(), pause(), resume(), die(), list(), skip(), move_()] } pub async fn songbird(ctx: PoiseContext<'_>) -> anyhow::Result<(Arc, Arc>)> { let gid = util::guild_id(ctx)?; let sb = songbird::get(ctx.serenity_context()).await.expect("acquiring songbird handle"); let call = sb.get_or_insert(gid); Ok((sb, call)) } pub async fn _play(ctx: PoiseContext<'_>, url: &url::Url) -> anyhow::Result<()> { use url::Host; tracing::debug!(%url, "playing"); if !url.scheme().starts_with("http") { tracing::warn!(%url, "got bad url argument"); util::reply(ctx, "bAD LiNk").await?; return Ok(()); } let host = url.host().and_then(|u| match u { Host::Domain(h) => Some(h.to_owned()), _ => None, }); if host.is_some_and(|h| h.to_lowercase().contains("imgur")) { tracing::info!("detected imgur link"); if ctx.author().id == 106160362109272064 { util::reply(ctx, "fuck you conway").await?; } else { util::reply(ctx, "IMGUR IS BAD, YOU TRASH CAN MAN").await?; } return Ok(()); } let Some(voice_channel) = util::best_voice_channel(ctx) else { tracing::error!(?ctx, "couldn't find a relevant voice channel"); util::react(ctx, '🔇').await?; return Ok(()); }; util::react(ctx, '🔃').await?; let client = { let data = ctx.serenity_context().data.read().await; data.get::().unwrap().clone() }; let volume = util::volume(ctx).await; tracing::debug!(volume); let playback = { let data = ctx.serenity_context().data.read().await; data.get::().unwrap().clone() }; let mut input = YoutubeDl::new_ytdl_like(&crate::config::YTDL_COMMAND, client.clone(), url.to_string()); let meta = input.aux_metadata().await?; let track = input.conv::(); { let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; if call.current_channel().is_none() { call.join(voice_channel).await?; } // TODO: store enqueueing channel so songbird handler can switch channels let handle = call.enqueue(track).await; handle.set_volume(volume as _)?; playback.insert(handle.uuid(), Metadata { invoker: ctx.author().id, invoke_info: InvokeInfo::Ytdl { metadata: meta, }, }); } util::react(ctx, '📣').await?; util::unreact(ctx, '🔃').await?; Ok(()) } /// Move audio to the caller's voice channel. #[poise::command(rename = "move", prefix_command, guild_only, category = "playback")] pub async fn move_(ctx: PoiseContext<'_>) -> anyhow::Result<()> { let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; if call.current_channel().is_none() { tracing::debug!("no current channel"); util::reply(ctx, "let's get yer head screwed on straight").await?; return Ok(()); } let Some(voice_channel) = util::best_voice_channel(ctx) else { tracing::error!(?ctx, "couldn't find a relevant voice channel"); util::react(ctx, '🔇').await?; return Ok(()); }; call.join(voice_channel).await?; Ok(()) } /// Play a link. #[poise::command(prefix_command, guild_only, category = "playback")] pub async fn play( ctx: PoiseContext<'_>, #[description = "link to play (if absent, resumes playback)"] u: Option, ) -> anyhow::Result<()> { let Some(u) = u else { return _resume(ctx).await; }; _play(ctx, &u).await } /// Pause audio playback. #[poise::command(prefix_command, guild_only, category = "playback")] pub async fn pause(ctx: PoiseContext<'_>) -> anyhow::Result<()> { let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; call.queue().pause()?; Ok(()) } /// Resume audio playback. #[poise::command(prefix_command, guild_only, aliases("continue"), category = "playback")] pub async fn resume(ctx: PoiseContext<'_>) -> anyhow::Result<()> { _resume(ctx).await } async fn _resume(ctx: PoiseContext<'_>) -> anyhow::Result<()> { let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; call.queue().resume()?; Ok(()) } /// Skip the current track in the queue. #[poise::command(prefix_command, guild_only, category = "playback", aliases("next"))] pub async fn skip(ctx: PoiseContext<'_>) -> anyhow::Result<()> { let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; call.queue().skip()?; Ok(()) } /// Stop playing audio and delete the queue. #[poise::command( prefix_command, guild_only, category = "playback", aliases("sudoku", "fuckoff", "stop", "kill") )] pub async fn die(ctx: PoiseContext<'_>) -> anyhow::Result<()> { let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; call.queue().stop(); call.leave().await?; Ok(()) } /// List queued audio. #[poise::command(prefix_command, guild_only, category = "playback", aliases("queue", "q"))] pub async fn list(ctx: PoiseContext<'_>) -> anyhow::Result<()> { let current_queue = { let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; let queue = call.queue(); queue.current_queue() }; let playback_data = { let data = ctx.serenity_context().data.read().await; data.get::().cloned().unwrap() }; if current_queue.is_empty() { util::reply(ctx, "nothing queued").await?; } fn fmt_option(fallback: impl AsRef, opt: Option) -> String where T: std::fmt::Display, { if let Some(opt) = opt { format!("{opt}") } else { format!("<{}>", fallback.as_ref()) } } for track in current_queue.into_iter() { let info = track.get_info().await?; let playback_pos = humantime::format_duration(Duration::from_secs(info.position.as_secs())).to_string(); let meta = playback_data.get(&track.uuid()).map(|x| x.clone()); let Some(meta) = meta else { tracing::warn!("no track metadata"); util::reply(ctx, format!("`[unk ]` playing for {playback_pos}")).await?; continue; }; let fmt = match meta.invoke_info { InvokeInfo::Ytdl { metadata, } => { format!( "`[web ]` **{}**: {playback_pos} / {}, queued by {}", fmt_option("unknown title", metadata.title), fmt_option( "unknown duration", metadata.duration.map(|dur| humantime::format_duration(dur).to_string()) ), meta.invoker.mention() ) }, InvokeInfo::Meme { meme, } => { if info.position > Duration::default() { format!( "`[meme]` **{}**, {playback_pos}, queued by {}", meme.title, meta.invoker.mention() ) } else { format!("`[meme]` **{}**, queued by {}", meme.title, meta.invoker.mention()) } }, }; util::reply(ctx, fmt).await?; } Ok(()) } #[derive(Debug, Clone)] pub struct Metadata { pub invoker: UserId, pub invoke_info: InvokeInfo, } #[derive(Debug, Clone)] pub enum InvokeInfo { Ytdl { metadata: AuxMetadata, }, #[cfg(feature = "db")] Meme { meme: db::Meme, }, }