use std::sync::Arc; use log::{ debug, info, warn, }; use serenity::prelude::*; use songbird::{ input::YoutubeDl, Call, Songbird, }; use crate::{ bot::HttpKey, util, PoiseContext, PoiseData, CONFIG, }; pub fn commands() -> impl IntoIterator> { vec![play(), pause(), resume(), die(), list(), skip()] } pub async fn songbird(ctx: PoiseContext<'_>) -> anyhow::Result<(Arc, Arc>)> { let Some(gid) = ctx.guild_id() else { return Err(anyhow::anyhow!("no guild id").into()); }; 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; debug!("playing '{}'", url); if !url.scheme().starts_with("http") { warn!("got bad url argument to play: {}", url); 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")) { 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(()); } util::react(ctx, '🔃').await?; let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; if call.current_channel().is_none() { call.join(CONFIG.discord.voice_channel()).await?; } let client = { let data = ctx.serenity_context().data.read().await; data.get::().unwrap().clone() }; let input = YoutubeDl::new_ytdl_like(&crate::config::YTDL_COMMAND, client.clone(), url.to_string()); call.enqueue_input(input.into()).await; util::react(ctx, '📣').await?; util::unreact(ctx, '🔃').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"))] pub async fn list(ctx: PoiseContext<'_>) -> anyhow::Result<()> { let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; let queue = call.queue(); util::reply(ctx, "(command fix work-in-progress)").await?; for track in queue.current_queue().into_iter() { let info = track.get_info().await?; let fmt = format!("track playing for {:?}", info.play_time); util::reply(ctx, fmt).await?; } Ok(()) }