diff options
| -rw-r--r-- | src/audio/mod.rs | 81 | ||||
| -rw-r--r-- | src/audio/play_queue.rs (renamed from src/commands/playback/types.rs) | 152 | ||||
| -rw-r--r-- | src/audio/timeutil.rs | 201 | ||||
| -rw-r--r-- | src/audio/ytdl.rs | 117 | ||||
| -rw-r--r-- | src/commands/meme.rs | 10 | ||||
| -rw-r--r-- | src/commands/mod.rs | 11 | ||||
| -rw-r--r-- | src/commands/playback.rs | 224 | ||||
| -rw-r--r-- | src/commands/playback/mod.rs | 472 | ||||
| -rw-r--r-- | src/commands/sound_levels.rs (renamed from src/commands/sound.rs) | 3 | ||||
| -rw-r--r-- | src/main.rs | 5 |
10 files changed, 646 insertions, 630 deletions
diff --git a/src/audio/mod.rs b/src/audio/mod.rs new file mode 100644 index 0000000..a7f3e83 --- /dev/null +++ b/src/audio/mod.rs @@ -0,0 +1,81 @@ +use std::sync::Arc; + +use either::Either; +use typemap::Key; +use chrono::Duration; + +use serenity::{ + model::{ + id::ChannelId, + }, + prelude::*, + client::bridge::voice::ClientVoiceManager, + voice::LockedAudio, +}; + +use crate::{ + must_env_lookup, + Result, +}; + +pub use self::timeutil::parse_times; +pub use self::ytdl::ytdl; +pub use self::play_queue::PlayQueue; + +mod timeutil; +mod ytdl; +mod play_queue; + +pub trait CtxExt { + fn currently_playing(&self) -> bool; + fn users_listening(&self) -> Result<bool>; +} + +impl CtxExt for Context { + fn currently_playing(&self) -> bool { + let queue_lock = self.data.lock().get::<PlayQueue>().cloned().unwrap(); + let play_queue = queue_lock.read().unwrap(); + play_queue.playing.is_none() + } + + fn users_listening(&self) -> Result<bool> { + let channel_id = ChannelId(must_env_lookup::<u64>("VOICE_CHANNEL")); + let channel = channel_id.to_channel()?; + let res = channel.guild() + .and_then(|ch| ch.read().guild()) + .map(|g| (&g.read().voice_states) + .into_iter() + .any(|(_, state)| state.channel_id == Some(channel_id))) + .unwrap_or(false); + + Ok(res) + } +} + +pub struct VoiceManager; + +impl Key for VoiceManager { + type Value = Arc<Mutex<ClientVoiceManager>>; +} + +impl VoiceManager { + pub fn register(c: &mut Client) { + let mut data = c.data.lock(); + data.insert::<VoiceManager>(Arc::clone(&c.voice_manager)); + } +} + +#[derive(Clone, Debug)] +pub struct PlayArgs { + pub data: Either<String, Vec<u8>>, + pub initiator: String, + pub sender_channel: ChannelId, + pub start: Option<Duration>, + pub end: Option<Duration>, +} + +#[derive(Clone)] +pub struct CurrentItem { + pub init_args: PlayArgs, + pub audio: LockedAudio, +} diff --git a/src/commands/playback/types.rs b/src/audio/play_queue.rs index e4bbd26..6f3a130 100644 --- a/src/commands/playback/types.rs +++ b/src/audio/play_queue.rs @@ -1,72 +1,28 @@ use std::{ - collections::VecDeque, sync::{Arc, RwLock}, thread, + collections::VecDeque, time::Duration, - io::{ - Read, - Result as IoResult, - }, - process::{ - Command, - Stdio, - Child, - } }; -use chrono::Duration as CDuration; -use either::{Either, Left, Right}; -use serenity::{ - client::bridge::voice::ClientVoiceManager, - model::id::ChannelId, - prelude::*, - voice::{ - LockedAudio, - AudioSource, - pcm, - }, -}; use typemap::Key; -use serde_json::Value; +use either::{Left, Right}; +use serenity::prelude::*; use crate::{ + audio::{ + CurrentItem, + PlayArgs, + ytdl, + }, commands::{ + sound_levels::DEFAULT_VOLUME, send, - sound::DEFAULT_VOLUME }, must_env_lookup, TARGET_GUILD_ID, - Result, }; -pub struct VoiceManager; - -impl Key for VoiceManager { - type Value = Arc<Mutex<ClientVoiceManager>>; -} - -impl VoiceManager { - pub fn register(c: &mut Client) { - let mut data = c.data.lock(); - data.insert::<VoiceManager>(Arc::clone(&c.voice_manager)); - } -} - -#[derive(Clone, Debug)] -pub struct PlayArgs { - pub data: Either<String, Vec<u8>>, - pub initiator: String, - pub sender_channel: ChannelId, - pub start: Option<CDuration>, - pub end: Option<CDuration>, -} - -#[derive(Clone)] -pub struct CurrentItem { - pub init_args: PlayArgs, - pub audio: LockedAudio, -} - #[derive(Clone)] pub struct PlayQueue { pub queue: VecDeque<PlayArgs>, @@ -179,93 +135,3 @@ impl PlayQueue { } } -// Copied from serenity -struct ChildContainer(Child); - -impl Read for ChildContainer { - fn read(&mut self, buffer: &mut [u8]) -> IoResult<usize> { - self.0.stdout.as_mut().unwrap().read(buffer) - } -} - -impl Drop for ChildContainer { - fn drop (&mut self) { - if let Err(e) = self.0.kill() { - debug!("[Voice] Error awaiting child process: {:?}", e); - } - } -} - -pub fn ytdl(uri: &str, start: Option<CDuration>, end: Option<CDuration>) -> Result<Box<AudioSource>> { - let args = [ - "-f", - "webm[abr>0]/bestaudio/best", - "--no-playlist", - "--print-json", - "--skip-download", - uri, - ]; - - let out = Command::new("youtube-dl") - .args(&args) - .stdin(Stdio::null()) - .output()?; - - if !out.status.success() { - return Err(VoiceError::YouTubeDLRun(out).into()); - } - - let value = serde_json::from_reader(&out.stdout[..])?; - let mut obj = match value { - Value::Object(obj) => obj, - other => return Err(VoiceError::YouTubeDLProcessing(other).into()), - }; - - let uri = match obj.remove("url") { - Some(v) => match v { - Value::String(uri) => uri, - other => return Err(VoiceError::YouTubeDLUrl(other).into()), - }, - None => return Err(VoiceError::YouTubeDLUrl(Value::Object(obj)).into()), - }; - - let start = start.unwrap_or(CDuration::zero()); - let start_str = format!("{:02}:{:02}:{:02}", start.num_hours(), start.num_minutes() % 60, start.num_seconds() % 60); - - let mut opts = vec! [ - "-f", - "s16le", - "-ac", - "2", // force stereo -- this may cause issues - "-ar", - "48000", - "-acodec", - "pcm_s16le", - "-ss", - &start_str, - ] - .into_iter() - .map(|s| s.to_owned()) - .collect::<Vec<_>>(); - - match end { - Some(e) => { - opts.push("-to".to_owned()); - opts.push(format!("{:02}:{:02}:{:02}", e.num_hours(), e.num_minutes() % 60, e.num_seconds() % 60)); - }, - _ => {}, - } - - opts.push("-".to_owned()); - - let command = Command::new("ffmpeg") - .arg("-i") - .arg(uri) - .args(opts) - .stderr(Stdio::null()) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .spawn()?; - - Ok(pcm(true, ChildContainer(command))) -} diff --git a/src/audio/timeutil.rs b/src/audio/timeutil.rs new file mode 100644 index 0000000..d0bd9d5 --- /dev/null +++ b/src/audio/timeutil.rs @@ -0,0 +1,201 @@ +use regex::{ + Regex, + Match, +}; +use chrono::Duration; + +lazy_static! { + static ref START_REGEX: Regex = + Regex::new(r"(?:start|begin(?:ning)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); + + static ref DUR_REGEX: Regex = + Regex::new(r"dur(?:ation)?\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); + + static ref END_REGEX: Regex = + Regex::new(r"(?:end|term(?:inate|ination)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); +} + +pub fn parse_times<A: AsRef<str>>(s: A) -> (Option<Duration>, Option<Duration>) { + fn parse_match(m: Option<Match>) -> u64 { + m.and_then(|s| s.as_str().parse::<u64>().ok()).unwrap_or(0) + } + + fn parse_captures<B: AsRef<str>>(r: &Regex, s: B) -> Option<Duration> { + r.captures(s.as_ref()) + .map(|capt| { + let hours = parse_match(capt.name("hours")); + let minutes = parse_match(capt.name("minutes")); + let seconds = parse_match(capt.name("seconds")); + + let result = Duration::hours(hours as i64) + + Duration::minutes(minutes as i64) + + Duration::seconds(seconds as i64); + + assert!(result >= Duration::zero()); + + result + }) + } + + let start_time = parse_captures(&START_REGEX, &s); + let dur = parse_captures(&DUR_REGEX, &s); + let end_time = parse_captures(&END_REGEX, s) + .or_else(|| start_time.and_then(|start| dur.map(|d| start + d))); + + (start_time, end_time) +} + +#[cfg(test)] +mod test { + use super::*; + use time::Duration; + + #[test] + fn test_start() { + let captures = START_REGEX.captures("start 1h2m3s").unwrap(); + + assert_eq!(captures.name("hours").unwrap().as_str(), "1"); + assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); + assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); + + assert!(START_REGEX.captures("").is_none()); + assert!(START_REGEX.captures("start s").is_none()); + + let captures = START_REGEX.captures("start 1").unwrap(); + assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); + } + + #[test] + fn test_dur() { + let captures = DUR_REGEX.captures("dur 1h2m3s").unwrap(); + + assert_eq!(captures.name("hours").unwrap().as_str(), "1"); + assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); + assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); + + assert!(DUR_REGEX.captures("").is_none()); + assert!(DUR_REGEX.captures("dur s").is_none()); + + let captures = DUR_REGEX.captures("dur 1").unwrap(); + assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); + } + + #[test] + fn test_end() { + let captures = END_REGEX.captures("end 1h2m3s").unwrap(); + + assert_eq!(captures.name("hours").unwrap().as_str(), "1"); + assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); + assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); + + assert!(END_REGEX.captures("").is_none()); + assert!(END_REGEX.captures("end s").is_none()); + + let captures = END_REGEX.captures("end 1").unwrap(); + assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); + } + + #[test] + fn test_parse_matrix() { + fn format_time(d: &Duration) -> impl Iterator<Item=String> { + let seconds = d.num_seconds() % 60; + let minutes = d.num_minutes() % 60; + let hours = d.num_hours(); + + let elems = vec![true, false]; + + #[inline] + fn format_maybe_zero<S: AsRef<str>>(v: i64, unit: S, always: bool) -> String { + if always || v != 0 { + format!("{}{}", v, unit.as_ref()) + } else { + "".to_owned() + } + } + + iproduct!(elems.clone(), elems.clone(), elems) + .filter_map(move |(secs, mins, hr)| { + if !secs && !mins && !hr { + return None; + } + + let hr_string = format_maybe_zero(hours, "h", hr); + let mn_string = format_maybe_zero(minutes, "m", mins); + let sec_string = format_maybe_zero(seconds, "s", secs); + + Some(format!("{}{}{}", hr_string, mn_string, sec_string)) + }) + } + + let start_times = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(32))]; + let durs = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(123141))]; + let end_times = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(19851598))]; + + let start_names = vec!["start", "begin", "beginning"]; + let dur_names = vec!["dur", "duration"]; + let end_names = vec!["end", "term", "terminate", "termination"]; + + let pairs = vec! [ + (start_times, start_names), + (durs, dur_names), + (end_times, end_names), + ]; + + let elems = pairs.into_iter() + .map(|(times, names)| { + let result = times.into_iter() + .flat_map(move |d| { + let names_iter = names.clone().into_iter(); + + d.as_ref().map(move |dur| { + let dur = dur.clone(); + + Box::new(iproduct!(format_time(&dur), names_iter) + .map(move |(time, name)| Some((dur, format!("{} {}", name, time))))) as Box<dyn Iterator<Item=Option<(Duration, String)>>> + }).unwrap_or_else(|| Box::new(::std::iter::once(None))) + }); + + result.collect::<Vec<Option<(Duration, String)>>>() + }) + .collect::<Vec<Vec<Option<(Duration, String)>>>>(); + + let start_iters = &elems[0]; + let dur_iters = &elems[1]; + let end_iters = &elems[2]; + + iproduct!(start_iters, dur_iters, end_iters) + .for_each(|(start, dur, end)| { + let s = vec![start, dur, end] + .into_iter() + .filter_map(|o| { + o.as_ref().map(|(_, formatted)| formatted.to_owned()) + }) + .collect::<Vec<_>>() + .join(" "); + + println!("testing {}", s); + + let (parse_start, parse_end) = parse_times(s); + + match start { + Some((dur, _)) => assert_eq!(*dur, parse_start.unwrap()), + None => assert_eq!(None, parse_start), + } + + match end { + Some((d, _)) => assert_eq!(*d, parse_end.unwrap()), + None => { + match dur { + Some((d, _)) => { + match start { + Some((s, _)) => assert_eq!(parse_end.unwrap(), *s + *d), + None => assert_eq!(None, parse_end), + } + }, + None => assert_eq!(None, parse_end), + } + } + } + }); + } +} diff --git a/src/audio/ytdl.rs b/src/audio/ytdl.rs new file mode 100644 index 0000000..d16d166 --- /dev/null +++ b/src/audio/ytdl.rs @@ -0,0 +1,117 @@ +/// This module is entirely adapted from the relevant code in Serenity. + +use std::{ + io::{ + Read, + Result as IoResult, + }, + process::{ + Command, + Stdio, + Child, + }, +}; + +use chrono::Duration; +use serde_json::Value; + +use serenity::{ + voice::{ + AudioSource, + pcm, + VoiceError, + } +}; + +use crate::Result; + + +struct ChildContainer(Child); + +impl Read for ChildContainer { + fn read(&mut self, buffer: &mut [u8]) -> IoResult<usize> { + self.0.stdout.as_mut().unwrap().read(buffer) + } +} + +impl Drop for ChildContainer { + fn drop (&mut self) { + if let Err(e) = self.0.kill() { + debug!("[Voice] Error awaiting child process: {:?}", e); + } + } +} + +pub fn ytdl(uri: &str, start: Option<Duration>, end: Option<Duration>) -> Result<Box<AudioSource>> { + let args = [ + "-f", + "webm[abr>0]/bestaudio/best", + "--no-playlist", + "--print-json", + "--skip-download", + uri, + ]; + + let out = Command::new("youtube-dl") + .args(&args) + .stdin(Stdio::null()) + .output()?; + + if !out.status.success() { + return Err(VoiceError::YouTubeDLRun(out).into()); + } + + let value = serde_json::from_reader(&out.stdout[..])?; + let mut obj = match value { + Value::Object(obj) => obj, + other => return Err(VoiceError::YouTubeDLProcessing(other).into()), + }; + + let uri = match obj.remove("url") { + Some(v) => match v { + Value::String(uri) => uri, + other => return Err(VoiceError::YouTubeDLUrl(other).into()), + }, + None => return Err(VoiceError::YouTubeDLUrl(Value::Object(obj)).into()), + }; + + let start = start.unwrap_or(Duration::zero()); + let start_str = format!("{:02}:{:02}:{:02}", start.num_hours(), start.num_minutes() % 60, start.num_seconds() % 60); + + let mut opts = vec! [ + "-f", + "s16le", + "-ac", + "2", // force stereo -- this may cause issues + "-ar", + "48000", + "-acodec", + "pcm_s16le", + "-ss", + &start_str, + ] + .into_iter() + .map(|s| s.to_owned()) + .collect::<Vec<_>>(); + + match end { + Some(e) => { + opts.push("-to".to_owned()); + opts.push(format!("{:02}:{:02}:{:02}", e.num_hours(), e.num_minutes() % 60, e.num_seconds() % 60)); + }, + _ => {}, + } + + opts.push("-".to_owned()); + + let command = Command::new("ffmpeg") + .arg("-i") + .arg(uri) + .args(opts) + .stderr(Stdio::null()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .spawn()?; + + Ok(pcm(true, ChildContainer(command))) +} diff --git a/src/commands/meme.rs b/src/commands/meme.rs index 8514eed..0305e79 100644 --- a/src/commands/meme.rs +++ b/src/commands/meme.rs @@ -13,12 +13,12 @@ use serenity::{ }; use crate::{ + audio::{ + CtxExt, + PlayArgs, + PlayQueue, + }, commands::{ - playback::{ - CtxExt, - PlayArgs, - PlayQueue, - }, send, }, db::*, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b73faef..b991c18 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -8,11 +8,11 @@ use crate::Result; #[cfg(feature = "diesel")] pub use self::meme::*; pub use self::playback::*; -pub use self::sound::*; +pub use self::sound_levels::*; -mod playback; -mod sound; -mod roll; +pub(crate) mod playback; +pub(crate) mod sound_levels; +pub(crate) mod roll; pub fn register_commands(f: StandardFramework) -> StandardFramework { let f: StandardFramework = register_db(f); @@ -114,8 +114,7 @@ fn register_db(f: StandardFramework) -> StandardFramework { f } -fn send<A: AsRef<str>>(channel: ChannelId, text: A, tts: bool) -> Result<()> { +pub(crate) fn send<A: AsRef<str>>(channel: ChannelId, text: A, tts: bool) -> Result<()> { channel.send_message(|m| m.content(text.as_ref()).tts(tts))?; Ok(()) } - diff --git a/src/commands/playback.rs b/src/commands/playback.rs new file mode 100644 index 0000000..661dd87 --- /dev/null +++ b/src/commands/playback.rs @@ -0,0 +1,224 @@ +use serenity::{ + prelude::*, + model::channel::Message, + framework::standard::Args, +}; +use either::{Left, Right}; + +use crate::{ + TARGET_GUILD_ID, + Result, + commands::send, + audio::{ + parse_times, + PlayQueue, + PlayArgs, + VoiceManager, + }, +}; + +pub fn _play(ctx: &Context, msg: &Message, url: &str) -> Result<()> { + use url::{Url, Host}; + + debug!("playing '{}'", url); + if !url.starts_with("http") { + send(msg.channel_id, "bAD LiNk", msg.tts)?; + return Ok(()); + } + + let url = match Url::parse(url) { + Err(e) => { + send(msg.channel_id, "INVALID URL", msg.tts)?; + return Err(e.into()); + }, + Ok(u) => u, + }; + + let host = url.host().and_then(|u| match u { + Host::Domain(h) => Some(h.to_owned()), + _ => None, + }); + + if host.map(|h| h.to_lowercase().contains("imgur")).unwrap_or(false) { + send(msg.channel_id, "IMGUR IS BAD, YOU TRASH CAN MAN", msg.tts)?; + return Ok(()); + } + + let (start, end) = parse_times(&msg.content); + + let queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap(); + let mut play_queue = queue_lock.write().unwrap(); + + play_queue.queue.push_back(PlayArgs{ + initiator: msg.author.name.clone(), + data: Left(url.into_string()), + sender_channel: msg.channel_id, + start, + end, + }); + + Ok(()) +} + +pub fn play(ctx: &mut Context, msg: &Message, mut args: Args) -> Result<()> { + if args.len() == 0 { + return _resume(ctx, msg); + } + + let url = match args.single::<String>() { + Ok(url) => url, + Err(_) => return send(msg.channel_id, "BAD LINK", msg.tts), + }; + + _play(ctx, msg, &url) +} + +pub fn pause(ctx: &mut Context, msg: &Message, _: Args) -> Result<()> { + let queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap(); + + let done = || send(msg.channel_id, "r u srs", msg.tts); + let playing = { + let play_queue = queue_lock.read().unwrap(); + + let current_item = match play_queue.playing { + Some(ref x) => x, + None => return done(), + }; + + let audio = current_item.audio.lock(); + audio.playing + }; + + if !playing { + return done(); + } + + { + let queue = queue_lock.write().unwrap(); + let ref audio = queue.playing.clone().unwrap().audio; + audio.lock().pause(); + } + + Ok(()) +} + +pub fn resume(ctx: &mut Context, msg: &Message, _: Args) -> Result<()> { + _resume(ctx, msg) +} + +fn _resume(ctx: &mut Context, msg: &Message) -> Result<()> { + let queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap(); + + let done = || send(msg.channel_id, "r u srs", msg.tts); + let playing = { + let play_queue = queue_lock.read().unwrap(); + + let current_item = match play_queue.playing { + Some(ref x) => x, + None => { + done()?; + return Ok(()); + }, + }; + + let audio = current_item.audio.lock(); + audio.playing + }; + + if playing { + done()?; + return Ok(()); + } + + { + let queue = queue_lock.write().unwrap(); + let ref audio = queue.playing.clone().unwrap().audio; + audio.lock().play(); + } + + Ok(()) +} + +pub fn skip(ctx: &mut Context, _msg: &Message, _args: Args) -> Result<()> { + let data = ctx.data.lock(); + + let mgr_lock = data.get::<VoiceManager>().cloned().unwrap(); + let mut manager = mgr_lock.lock(); + + let queue_lock = data.get::<PlayQueue>().cloned().unwrap(); + + if let Some(handler) = manager.get_mut(*TARGET_GUILD_ID) { + handler.stop(); + let mut play_queue = queue_lock.write().unwrap(); + play_queue.playing = None; + } else { + debug!("got skip with no handler attached"); + } + + Ok(()) +} + +pub fn die(ctx: &mut Context, msg: &Message, _: Args) -> Result<()> { + let data = ctx.data.lock(); + + let mgr_lock = data.get::<VoiceManager>().cloned().unwrap(); + let mut manager = mgr_lock.lock(); + + let queue_lock = data.get::<PlayQueue>().cloned().unwrap(); + + { + let mut play_queue = queue_lock.write().unwrap(); + + play_queue.playing = None; + play_queue.queue.clear(); + } + + if let Some(handler) = manager.get_mut(*TARGET_GUILD_ID) { + handler.stop(); + handler.leave(); + } else { + send(msg.channel_id, "YOU die", msg.tts)?; + debug!("got die with no handler attached"); + } + + Ok(()) +} + +pub fn list(ctx: &mut Context, msg: &Message, _: Args) -> Result<()> { + let queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap(); + let play_queue = queue_lock.read().unwrap(); + + let channel_tmp = msg.channel().unwrap().guild().unwrap(); + let channel = channel_tmp.read(); + + match play_queue.playing { + Some(ref info) => { + let audio = info.audio.lock(); + let status = if audio.playing { "playing" } else { "paused:" }; + + let playing_info = match info.init_args.data { + Left(ref url) => format!(" `{}`", url), + Right(_) => "memeing".to_owned(), + }; + + send(msg.channel_id, &format!("Currently {} {} ({})", status, playing_info, info.init_args.initiator), msg.tts)?; + }, + None => { + debug!("`list` called with no items in queue"); + send(msg.channel_id, "Nothing is playing you meme", msg.tts)?; + return Ok(()); + }, + } + + play_queue.queue.iter() + .for_each(|info| { + let playing_info = match info.data { + Left(ref url) => format!("`{}`", url), + Right(_) => "meme".to_owned(), + }; + + let _ = channel.say(&format!("{} ({})", playing_info, info.initiator)); + }); + + Ok(()) +} diff --git a/src/commands/playback/mod.rs b/src/commands/playback/mod.rs deleted file mode 100644 index e5e7a4c..0000000 --- a/src/commands/playback/mod.rs +++ /dev/null @@ -1,472 +0,0 @@ -use either::{Left, Right}; -use regex::Regex; -use time::Duration; - -use serenity::{ - framework::standard::Args, - model::{ - channel::Message, - id::ChannelId, - }, - prelude::*, -}; - -use crate::{ - commands::send, - must_env_lookup, - Result, - TARGET_GUILD_ID, -}; - -pub use self::types::*; - -mod types; - -pub trait CtxExt { - fn currently_playing(&self) -> bool; - fn users_listening(&self) -> Result<bool>; -} - -impl CtxExt for Context { - fn currently_playing(&self) -> bool { - let queue_lock = self.data.lock().get::<PlayQueue>().cloned().unwrap(); - let play_queue = queue_lock.read().unwrap(); - play_queue.playing.is_none() - } - - fn users_listening(&self) -> Result<bool> { - let channel_id = ChannelId(must_env_lookup::<u64>("VOICE_CHANNEL")); - let channel = channel_id.to_channel()?; - let res = channel.guild() - .and_then(|ch| ch.read().guild()) - .map(|g| (&g.read().voice_states) - .into_iter() - .any(|(_, state)| state.channel_id == Some(channel_id))) - .unwrap_or(false); - - Ok(res) - } -} - -pub fn _play(ctx: &Context, msg: &Message, url: &str) -> Result<()> { - use url::{Url, Host}; - - debug!("playing '{}'", url); - if !url.starts_with("http") { - send(msg.channel_id, "bAD LiNk", msg.tts)?; - return Ok(()); - } - - let url = match Url::parse(url) { - Err(e) => { - send(msg.channel_id, "INVALID URL", msg.tts)?; - return Err(e.into()); - }, - Ok(u) => u, - }; - - let host = url.host().and_then(|u| match u { - Host::Domain(h) => Some(h.to_owned()), - _ => None, - }); - - if host.map(|h| h.to_lowercase().contains("imgur")).unwrap_or(false) { - send(msg.channel_id, "IMGUR IS BAD, YOU TRASH CAN MAN", msg.tts)?; - return Ok(()); - } - - let (start, end) = parse_times(&msg.content); - - let queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap(); - let mut play_queue = queue_lock.write().unwrap(); - - play_queue.queue.push_back(PlayArgs{ - initiator: msg.author.name.clone(), - data: Left(url.into_string()), - sender_channel: msg.channel_id, - start, - end, - }); - - Ok(()) -} - -lazy_static! { - static ref START_REGEX: Regex = - Regex::new(r"(?:start|begin(?:ning)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); - - static ref DUR_REGEX: Regex = - Regex::new(r"dur(?:ation)?\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); - - static ref END_REGEX: Regex = - Regex::new(r"(?:end|term(?:inate|ination)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); -} - -fn parse_times<A: AsRef<str>>(s: A) -> (Option<Duration>, Option<Duration>) { - use regex::Match; - - fn parse_match(m: Option<Match>) -> u64 { - m.and_then(|s| s.as_str().parse::<u64>().ok()).unwrap_or(0) - } - - fn parse_captures<B: AsRef<str>>(r: &Regex, s: B) -> Option<Duration> { - r.captures(s.as_ref()) - .map(|capt| { - let hours = parse_match(capt.name("hours")); - let minutes = parse_match(capt.name("minutes")); - let seconds = parse_match(capt.name("seconds")); - - let result = Duration::hours(hours as i64) + - Duration::minutes(minutes as i64) + - Duration::seconds(seconds as i64); - - assert!(result >= Duration::zero()); - - result - }) - } - - let start_time = parse_captures(&START_REGEX, &s); - let dur = parse_captures(&DUR_REGEX, &s); - let end_time = parse_captures(&END_REGEX, s) - .or_else(|| start_time.and_then(|start| dur.map(|d| start + d))); - - (start_time, end_time) -} - -#[cfg(test)] -mod test { - use super::*; - use time::Duration; - - #[test] - fn test_start() { - let captures = START_REGEX.captures("start 1h2m3s").unwrap(); - - assert_eq!(captures.name("hours").unwrap().as_str(), "1"); - assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); - assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); - - assert!(START_REGEX.captures("").is_none()); - assert!(START_REGEX.captures("start s").is_none()); - - let captures = START_REGEX.captures("start 1").unwrap(); - assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); - } - - #[test] - fn test_dur() { - let captures = DUR_REGEX.captures("dur 1h2m3s").unwrap(); - - assert_eq!(captures.name("hours").unwrap().as_str(), "1"); - assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); - assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); - - assert!(DUR_REGEX.captures("").is_none()); - assert!(DUR_REGEX.captures("dur s").is_none()); - - let captures = DUR_REGEX.captures("dur 1").unwrap(); - assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); - } - - #[test] - fn test_end() { - let captures = END_REGEX.captures("end 1h2m3s").unwrap(); - - assert_eq!(captures.name("hours").unwrap().as_str(), "1"); - assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); - assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); - - assert!(END_REGEX.captures("").is_none()); - assert!(END_REGEX.captures("end s").is_none()); - - let captures = END_REGEX.captures("end 1").unwrap(); - assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); - } - - #[test] - fn test_parse_matrix() { - fn format_time(d: &Duration) -> impl Iterator<Item=String> { - let seconds = d.num_seconds() % 60; - let minutes = d.num_minutes() % 60; - let hours = d.num_hours(); - - let elems = vec![true, false]; - - #[inline] - fn format_maybe_zero<S: AsRef<str>>(v: i64, unit: S, always: bool) -> String { - if always || v != 0 { - format!("{}{}", v, unit.as_ref()) - } else { - "".to_owned() - } - } - - iproduct!(elems.clone(), elems.clone(), elems) - .filter_map(move |(secs, mins, hr)| { - if !secs && !mins && !hr { - return None; - } - - let hr_string = format_maybe_zero(hours, "h", hr); - let mn_string = format_maybe_zero(minutes, "m", mins); - let sec_string = format_maybe_zero(seconds, "s", secs); - - Some(format!("{}{}{}", hr_string, mn_string, sec_string)) - }) - } - - #[inline] - fn produce_time_strings(d: Option<Duration>, names: Vec<&'static str>) -> Box<dyn Iterator<Item=String>> { - d - .map(move |dur| { - let iter = iproduct!(format_time(&dur), names.into_iter()) - .map(|(time, name)| format!("{} {}", name, time)); - Box::new(iter) as Box<dyn Iterator<Item=String>> - }) - .unwrap_or(Box::new(vec!["".to_owned()].into_iter()) as Box<dyn Iterator<Item=String>>) - } - - #[inline] - fn dur_strs(v: Vec<Option<Duration>>, names: Vec<&'static str>) -> Vec<String> { - v - .into_iter() - .flat_map(|d| produce_time_strings(d, names.clone())) - .collect() - } - - let start_times = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(32))]; - let durs = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(123141))]; - let end_times = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(19851598))]; - - let start_names = vec!["start", "begin", "beginning"]; - let dur_names = vec!["dur", "duration"]; - let end_names = vec!["end", "term", "terminate", "termination"]; - - let pairs = vec! [ - (start_times, start_names), - (durs, dur_names), - (end_times, end_names), - ]; - - let elems = pairs.into_iter() - .map(|(times, names)| { - let result = times.into_iter() - .flat_map(move |d| { - let names_iter = names.clone().into_iter(); - - d.as_ref().map(move |dur| { - let dur = dur.clone(); - - Box::new(iproduct!(format_time(&dur), names_iter) - .map(move |(time, name)| Some((dur, format!("{} {}", name, time))))) as Box<dyn Iterator<Item=Option<(Duration, String)>>> - }).unwrap_or_else(|| Box::new(::std::iter::once(None))) - }); - - result.collect::<Vec<Option<(Duration, String)>>>() - }) - .collect::<Vec<Vec<Option<(Duration, String)>>>>(); - - let start_iters = &elems[0]; - let dur_iters = &elems[1]; - let end_iters = &elems[2]; - - iproduct!(start_iters, dur_iters, end_iters) - .for_each(|(start, dur, end)| { - let s = vec![start, dur, end] - .into_iter() - .filter_map(|o| { - o.as_ref().map(|(_, formatted)| formatted.to_owned()) - }) - .collect::<Vec<_>>() - .join(" "); - - println!("testing {}", s); - - let (parse_start, parse_end) = parse_times(s); - - match start { - Some((dur, _)) => assert_eq!(*dur, parse_start.unwrap()), - None => assert_eq!(None, parse_start), - } - - match end { - Some((d, _)) => assert_eq!(*d, parse_end.unwrap()), - None => { - match dur { - Some((d, _)) => { - match start { - Some((s, _)) => assert_eq!(parse_end.unwrap(), *s + *d), - None => assert_eq!(None, parse_end), - } - }, - None => assert_eq!(None, parse_end), - } - } - } - }); - } -} - -pub fn play(ctx: &mut Context, msg: &Message, mut args: Args) -> Result<()> { - if args.len() == 0 { - return _resume(ctx, msg); - } - - let url = match args.single::<String>() { - Ok(url) => url, - Err(_) => return send(msg.channel_id, "BAD LINK", msg.tts), - }; - - _play(ctx, msg, &url) -} - -pub fn pause(ctx: &mut Context, msg: &Message, _: Args) -> Result<()> { - let queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap(); - - let done = || send(msg.channel_id, "r u srs", msg.tts); - let playing = { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => return done(), - }; - - let audio = current_item.audio.lock(); - audio.playing - }; - - if !playing { - return done(); - } - - { - let queue = queue_lock.write().unwrap(); - let ref audio = queue.playing.clone().unwrap().audio; - audio.lock().pause(); - } - - Ok(()) -} - -pub fn resume(ctx: &mut Context, msg: &Message, _: Args) -> Result<()> { - _resume(ctx, msg) -} - -fn _resume(ctx: &mut Context, msg: &Message) -> Result<()> { - let queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap(); - - let done = || send(msg.channel_id, "r u srs", msg.tts); - let playing = { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => { - done()?; - return Ok(()); - }, - }; - - let audio = current_item.audio.lock(); - audio.playing - }; - - if playing { - done()?; - return Ok(()); - } - - { - let queue = queue_lock.write().unwrap(); - let ref audio = queue.playing.clone().unwrap().audio; - audio.lock().play(); - } - - Ok(()) -} - -pub fn skip(ctx: &mut Context, _msg: &Message, _args: Args) -> Result<()> { - let data = ctx.data.lock(); - - let mgr_lock = data.get::<VoiceManager>().cloned().unwrap(); - let mut manager = mgr_lock.lock(); - - let queue_lock = data.get::<PlayQueue>().cloned().unwrap(); - - if let Some(handler) = manager.get_mut(*TARGET_GUILD_ID) { - handler.stop(); - let mut play_queue = queue_lock.write().unwrap(); - play_queue.playing = None; - } else { - debug!("got skip with no handler attached"); - } - - Ok(()) -} - -pub fn die(ctx: &mut Context, msg: &Message, _: Args) -> Result<()> { - let data = ctx.data.lock(); - - let mgr_lock = data.get::<VoiceManager>().cloned().unwrap(); - let mut manager = mgr_lock.lock(); - - let queue_lock = data.get::<PlayQueue>().cloned().unwrap(); - - { - let mut play_queue = queue_lock.write().unwrap(); - - play_queue.playing = None; - play_queue.queue.clear(); - } - - if let Some(handler) = manager.get_mut(*TARGET_GUILD_ID) { - handler.stop(); - handler.leave(); - } else { - send(msg.channel_id, "YOU die", msg.tts)?; - debug!("got die with no handler attached"); - } - - Ok(()) -} - -pub fn list(ctx: &mut Context, msg: &Message, _: Args) -> Result<()> { - let queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap(); - let play_queue = queue_lock.read().unwrap(); - - let channel_tmp = msg.channel().unwrap().guild().unwrap(); - let channel = channel_tmp.read(); - - match play_queue.playing { - Some(ref info) => { - let audio = info.audio.lock(); - let status = if audio.playing { "playing" } else { "paused:" }; - - let playing_info = match info.init_args.data { - Left(ref url) => format!(" `{}`", url), - Right(_) => "memeing".to_owned(), - }; - - send(msg.channel_id, &format!("Currently {} {} ({})", status, playing_info, info.init_args.initiator), msg.tts)?; - }, - None => { - debug!("`list` called with no items in queue"); - send(msg.channel_id, "Nothing is playing you meme", msg.tts)?; - return Ok(()); - }, - } - - play_queue.queue.iter() - .for_each(|info| { - let playing_info = match info.data { - Left(ref url) => format!("`{}`", url), - Right(_) => "meme".to_owned(), - }; - - let _ = channel.say(&format!("{} ({})", playing_info, info.initiator)); - }); - - Ok(()) -} diff --git a/src/commands/sound.rs b/src/commands/sound_levels.rs index 7465e2e..fee3e37 100644 --- a/src/commands/sound.rs +++ b/src/commands/sound_levels.rs @@ -2,12 +2,11 @@ use serenity::{ framework::standard::Args, model::channel::Message, prelude::*, - }; use crate::{ + audio::{PlayQueue, VoiceManager}, commands::{ - playback::{PlayQueue, VoiceManager}, send, }, Result, diff --git a/src/main.rs b/src/main.rs index feca86f..7202556 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,7 @@ mod db; mod commands; mod util; +mod audio; pub type Result<T> = ::std::result::Result<T, Error>; @@ -82,8 +83,8 @@ fn run() -> Result<()> { let token = &dotenv::var("THULANI_TOKEN").map_err(|_| format_err!("missing token"))?; let mut client = Client::new(token, Handler)?; - commands::VoiceManager::register(&mut client); - commands::PlayQueue::register(&mut client); + audio::VoiceManager::register(&mut client); + audio::PlayQueue::register(&mut client); let owner_id = must_env_lookup::<u64>("OWNER_ID"); let mut framework = StandardFramework::new() |
