aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNathan Perry <avaglir@gmail.com>2019-02-16 14:52:54 -0500
committerNathan Perry <avaglir@gmail.com>2019-02-16 14:52:54 -0500
commit966a4934552a43d2a1de0e314bacd7bada0a6845 (patch)
tree44a895eb5859ff0390f1008f2beed2de4b18e548 /src
parent978945e16046fb28b2351ee637fd267c38df93ac (diff)
clean up project structure
- Move audio-related code into its own top-level module, separating out playback commands into their own file in `commands`. - Rename `sound` commands module to `sound_levels`.
Diffstat (limited to 'src')
-rw-r--r--src/audio/mod.rs81
-rw-r--r--src/audio/play_queue.rs (renamed from src/commands/playback/types.rs)152
-rw-r--r--src/audio/timeutil.rs201
-rw-r--r--src/audio/ytdl.rs117
-rw-r--r--src/commands/meme.rs10
-rw-r--r--src/commands/mod.rs11
-rw-r--r--src/commands/playback.rs224
-rw-r--r--src/commands/playback/mod.rs472
-rw-r--r--src/commands/sound_levels.rs (renamed from src/commands/sound.rs)3
-rw-r--r--src/main.rs5
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()