aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Perry <np@nathanperry.dev>2024-05-08 14:30:21 -0400
committerNathan Perry <np@nathanperry.dev>2024-05-08 14:30:21 -0400
commitfb088b4ff56ed76dabf4e638cb92bc1b9275fbcc (patch)
tree53cb51db079edafd756cae6a3d5fc4fae829429c
parent47817c4166937af24041a93e56ad9f841bf1e8f1 (diff)
clippy fixes
-rw-r--r--src/bot.rs1
-rw-r--r--src/commands/meme/create.rs2
-rw-r--r--src/commands/meme/history.rs4
-rw-r--r--src/commands/meme/invoke.rs30
-rw-r--r--src/commands/meme/mod.rs11
-rw-r--r--src/commands/mod.rs2
-rw-r--r--src/db/mod.rs4
-rw-r--r--src/game.rs98
-rw-r--r--src/util.rs376
9 files changed, 270 insertions, 258 deletions
diff --git a/src/bot.rs b/src/bot.rs
index 5d52259..db37754 100644
--- a/src/bot.rs
+++ b/src/bot.rs
@@ -48,7 +48,6 @@ use serenity::{
};
use songbird::{
Call,
- CoreEvent,
Event,
EventContext,
SerenityInit,
diff --git a/src/commands/meme/create.rs b/src/commands/meme/create.rs
index ef6090e..2cd9465 100644
--- a/src/commands/meme/create.rs
+++ b/src/commands/meme/create.rs
@@ -156,7 +156,7 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe
.arg("-i")
.arg(youtube_url)
.args(duration_opts)
- .args(&[
+ .args([
"-ac", "2", "-ar", "48000", "-f", "opus", "-acodec", "libopus", "-b:a", "96k", "-fs",
"5M", "-",
])
diff --git a/src/commands/meme/history.rs b/src/commands/meme/history.rs
index 33a2de2..e2953d1 100644
--- a/src/commands/meme/history.rs
+++ b/src/commands/meme/history.rs
@@ -128,7 +128,7 @@ pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes
InvocationRecord::last_n(&mut conn, n)?
};
- if records.len() == 0 {
+ if records.is_empty() {
info!("no memes in history");
return util::send(ctx, msg.channel_id, "i don't remember anything :(", msg.tts)
.map_err(CommandError::from)
@@ -393,7 +393,7 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
})
.join("\n");
- if result.len() == 0 {
+ if result.is_empty() {
info!("no memes matched query");
return util::send(ctx, msg.channel_id, "no match".to_owned(), msg.tts)
diff --git a/src/commands/meme/invoke.rs b/src/commands/meme/invoke.rs
index 13996da..2db9e83 100644
--- a/src/commands/meme/invoke.rs
+++ b/src/commands/meme/invoke.rs
@@ -76,7 +76,7 @@ async fn _meme(
args: Args,
audio_playback: AudioPlayback,
) -> CommandResult {
- if args.len() == 0 || audio_playback != AudioPlayback::Optional {
+ if args.is_empty() || audio_playback != AudioPlayback::Optional {
return rand_meme(ctx, msg, audio_playback).await;
}
@@ -133,18 +133,15 @@ async fn rand_meme(
Ok(())
},
Err(e) => {
- match e.downcast_ref::<DieselError>() {
- Some(NotFound) => {
- info!("random meme not found");
- return util::send(ctx, message.channel_id, "i don't know any :(", message.tts)
- .map_err(CommandError::from)
- .await;
- },
- _ => {},
+ if let Some(NotFound) = e.downcast_ref::<DieselError>() {
+ info!("random meme not found");
+ return util::send(ctx, message.channel_id, "i don't know any :(", message.tts)
+ .map_err(CommandError::from)
+ .await;
}
util::send(ctx, message.channel_id, "HELP", message.tts).await?;
- return Err(e.into());
+ Err(e.into())
},
}
}
@@ -163,14 +160,11 @@ pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResu
send_meme(ctx, &meme, &mut conn, msg).await
},
Err(e) => {
- match e.downcast_ref::<DieselError>() {
- Some(NotFound) => {
- info!("rare meme not found");
- return util::send(ctx, msg.channel_id, "i don't know any :(", msg.tts)
- .map_err(CommandError::from)
- .await;
- },
- _ => {},
+ if let Some(NotFound) = e.downcast_ref::<DieselError>() {
+ info!("rare meme not found");
+ return util::send(ctx, msg.channel_id, "i don't know any :(", msg.tts)
+ .map_err(CommandError::from)
+ .await;
}
util::send(ctx, msg.channel_id, "THE MEME MARKET IS IN FREEFALL", msg.tts)
diff --git a/src/commands/meme/mod.rs b/src/commands/meme/mod.rs
index fe69b1c..c40e80a 100644
--- a/src/commands/meme/mod.rs
+++ b/src/commands/meme/mod.rs
@@ -74,7 +74,7 @@ async fn send_meme(
msg: &Message,
) -> CommandResult {
let should_tts =
- t.content.as_ref().map(|t| t.len() > 0).unwrap_or(false) && random::<u32>() % 25 == 0;
+ t.content.as_ref().map(|t| !t.is_empty()).unwrap_or(false) && random::<u32>() % 25 == 0;
debug!("sending meme (tts: {}): {:?}", should_tts, t);
@@ -85,7 +85,7 @@ async fn send_meme(
let ret = CreateMessage::default().tts(should_tts);
match t.content {
- Some(ref text) if text.len() > 0 => ret.content(text),
+ Some(ref text) if !text.is_empty() => ret.content(text),
_ => ret,
}
};
@@ -98,11 +98,10 @@ async fn send_meme(
msg.channel_id.send_files(ctx, vec![att], cmsg).await?;
},
- None => match t.content {
- Some(_) => {
+ None => {
+ if t.content.is_some() {
msg.channel_id.send_message(ctx, cmsg).await?;
- },
- None => {},
+ }
},
};
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index 3f69a3b..8eac09c 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -36,7 +36,7 @@ pub fn register_commands(f: StandardFramework) -> StandardFramework {
result.help(&help::HELP).unrecognised_command(|ctx, msg, unrec| {
Box::pin(async move {
- let url = match msg.content.split_whitespace().skip(1).next() {
+ let url = match msg.content.split_whitespace().nth(1) {
Some(x) if x.starts_with("http") => x,
_ => {
info!("bad command formatting: '{}'", unrec);
diff --git a/src/db/mod.rs b/src/db/mod.rs
index 94953d5..e7ff17f 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -47,7 +47,7 @@ static MIGRATE: std::sync::Once = std::sync::Once::new();
lazy_static! {
static ref DB_URL: String =
- env::var("DATABASE_URL").expect("no database url in environment").into();
+ env::var("DATABASE_URL").expect("no database url in environment");
static ref DB_CONFIG: Config = Config::from_str(&DB_URL).expect("parsing db url as config");
static ref CONN_MGR: ConnectionManager<PgConnection> = ConnectionManager::new(DB_URL.clone());
static ref RAW_CONN_MGR: RawPgConnMgr<NoTls> = RawPgConnMgr::new(DB_CONFIG.clone(), NoTls);
@@ -236,7 +236,7 @@ pub fn rare_meme(conn: &mut PgConnection, audio: bool) -> Result<Meme> {
.map(|row| (row.get::<_, i32>(0), row.get::<_, f64>(1) as i64))
.collect::<Vec<_>>();
- if elems.len() == 0 {
+ if elems.is_empty() {
return Err(anyhow!("no rare memes found"));
}
diff --git a/src/game.rs b/src/game.rs
index d3b0785..4011a5d 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -45,6 +45,7 @@ use serenity::{
},
prelude::*,
};
+use tap::Pipe;
use url::Url;
use crate::{
@@ -88,7 +89,7 @@ lazy_static! {
let default_path = PathBuf::from_str("user_id_mapping").unwrap();
let mapping_path = CONFIG.user_id_mapping.as_ref().unwrap_or(&default_path);
- fs::read_to_string(mapping_path).unwrap()
+ fs::read_to_string(mapping_path).unwrap_or("{}".to_owned())
};
static ref USER_INFO_MAP: FnvHashMap<String, ProfileInfo> = {
let v: Vec<UserInfo> = serde_json::from_str(&USER_MAP_STR).unwrap();
@@ -146,11 +147,11 @@ impl FromStr for GameStatus {
fn from_str(s: &str) -> Result<Self> {
use std::char;
- if s.starts_with("y") {
+ if s.starts_with('y') {
Ok(GameStatus::Installed)
} else if s.starts_with("n/i") {
Ok(GameStatus::NotInstalled)
- } else if s.starts_with("n") {
+ } else if s.starts_with('n') {
Ok(GameStatus::NotOwned)
} else if s.chars().all(char::is_whitespace) {
Ok(GameStatus::Unknown)
@@ -182,7 +183,7 @@ pub enum UserLookupError {
}
pub fn get_user_id<S: AsRef<str>>(g: &Guild, s: S) -> StdResult<UserId, UserLookupError> {
- let s = s.as_ref().trim_start_matches("@").to_lowercase();
+ let s = s.as_ref().trim_start_matches('@').to_lowercase();
if let Some(info) = USER_INFO_MAP.get(&s) {
return Ok(UserId::new(info.discord_user_id));
@@ -210,7 +211,7 @@ pub fn get_user_id<S: AsRef<str>>(g: &Guild, s: S) -> StdResult<UserId, UserLook
let opts = nicks
.into_iter()
- .chain(usernames.into_iter())
+ .chain(usernames)
.map(|(member, _)| member.user.id)
.collect::<FnvHashSet<_>>();
@@ -236,7 +237,6 @@ async fn _game(
let users = {
let guild =
msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?;
- let guild = guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild"))?;
let user_args: Vec<String> = if args.rest().is_empty() {
Vec::new()
@@ -244,49 +244,69 @@ async fn _game(
args.quoted().iter::<String>().collect::<StdResult<Vec<_>, ArgError<Infallible>>>()?
};
+ use serenity::futures::StreamExt;
+
let mut users = user_args
.into_iter()
+ .pipe(serenity::futures::stream::iter)
.filter_map(|u| {
- use std::borrow::Borrow;
+ let guild = &guild;
+ async move {
+ use std::borrow::Borrow;
- let possible = get_user_id(guild.borrow(), &u);
+ let possible = {
+ let Ok(guild) =
+ guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild"))
+ else {
+ error!("failed retrieving guild");
+ return None;
+ };
- debug!("parsed userid {:?}", possible);
+ get_user_id(guild.borrow(), &u)
+ };
- match possible {
- Err(UserLookupError::NotFound) => {
- let _ = util::send(
- ctx,
- msg.channel_id,
- &format!("didn't recognize {}", &u),
- msg.tts,
- );
- None
- },
- Ok(x) => Some(x),
- Err(UserLookupError::Ambiguous(x)) => {
- let _ = util::send(
- ctx,
- msg.channel_id,
- &format!("too many matches ({}) for {}", x, &u),
- msg.tts,
- );
- None
- },
+ debug!("parsed userid {:?}", possible);
+
+ match possible {
+ Err(UserLookupError::NotFound) => {
+ let _ = util::send(
+ ctx,
+ msg.channel_id,
+ &format!("didn't recognize {}", &u),
+ msg.tts,
+ )
+ .await;
+ None
+ },
+ Ok(x) => Some(x),
+ Err(UserLookupError::Ambiguous(x)) => {
+ let _ = util::send(
+ ctx,
+ msg.channel_id,
+ &format!("too many matches ({}) for {}", x, &u),
+ msg.tts,
+ )
+ .await;
+ None
+ },
+ }
}
})
- .filter_map(|uid| {
+ .filter_map(|uid| async move {
let res = DISCORD_MAP.get(&uid).map(|s| s.to_lowercase());
- if let None = res {
- let _ = info!("user {} is not recognized", uid);
+ if res.is_none() {
+ info!("user {} is not recognized", uid);
}
res
})
- .collect::<FnvHashSet<_>>();
+ .collect::<FnvHashSet<_>>()
+ .await;
+
+ if users.is_empty() {
+ let guild = guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild"))?;
- if users.len() == 0 {
let pairs = guild
.voice_states
.iter()
@@ -311,9 +331,9 @@ async fn _game(
users
};
- let inferred = users.len() == 0;
+ let inferred = users.is_empty();
- if inferred && users.len() < 2 || !inferred && users.len() < 1 {
+ if inferred && users.len() < 2 || !inferred && users.is_empty() {
info!("too few known users to make game comparison");
util::send(ctx, msg.channel_id, "yer too lonely", msg.tts).await?;
return Ok(());
@@ -375,7 +395,7 @@ async fn _game(
.collect::<Vec<_>>();
let mut games_in_common = {
- let game_map = user_games.values().nth(0).unwrap();
+ let game_map = user_games.values().next().unwrap();
statuses.iter().fold(iter::empty().collect::<FnvHashSet<_>>(), |acc, s| {
acc.union(&game_map[s]).cloned().collect()
@@ -444,7 +464,7 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command
let arg_user = args.single_quoted::<String>();
let user = if arg_user.is_err() {
- msg.author.id.clone()
+ msg.author.id
} else {
use std::borrow::Borrow;
@@ -566,7 +586,7 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command
})
.join("\n");
- if found_games.len() > 0 {
+ if !found_games.is_empty() {
util::send(
ctx,
msg.channel_id,
diff --git a/src/util.rs b/src/util.rs
index 0fd746b..e88e7b0 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -1,188 +1,188 @@
-use chrono::Duration;
-use serenity::{
- client::Context,
- model::{
- id::{
- ChannelId,
- MessageId,
- },
- permissions::Permissions,
- },
-};
-use std::process::Stdio;
-
-use lazy_static::lazy_static;
-use log::debug;
-use regex::{
- Match,
- Regex,
-};
-use serenity::all::{
- CreateMessage,
- Message,
-};
-use url::Url;
-
-use crate::{
- commands::songbird,
- Result,
- CONFIG,
-};
-
-pub async fn currently_playing(ctx: &Context, msg: &Message) -> bool {
- let (_sb, call) = songbird(ctx, msg).await.expect("no songbird");
-
- let call = call.lock().await;
- call.queue().current().is_some()
-}
-
-pub async fn users_listening(ctx: &Context) -> Result<bool> {
- let channel = CONFIG.discord.voice_channel().to_channel(&ctx).await?;
-
- let res = channel
- .guild()
- .and_then(|ch| ch.guild(&ctx))
- .map(|g| {
- (&g.voice_states)
- .into_iter()
- .any(|(_, state)| state.channel_id == Some(CONFIG.discord.voice_channel()))
- })
- .unwrap_or(false);
-
- Ok(res)
-}
-
-#[inline]
-pub async fn send(
- ctx: &Context,
- channel: ChannelId,
- text: impl AsRef<str>,
- tts: bool,
-) -> Result<()> {
- send_result(ctx, channel, text, tts).await.map(|_| ())
-}
-
-pub async fn send_result(
- ctx: &Context,
- channel: ChannelId,
- text: impl AsRef<str>,
- tts: bool,
-) -> Result<MessageId> {
- let text = text.as_ref();
- debug!("sending message {:?} to channel {:?} (tts: {})", text, channel, tts);
-
- let result = channel.send_message(ctx, CreateMessage::default().content(text).tts(tts)).await?;
- Ok(result.id)
-}
-
-lazy_static! {
- static ref REQUIRED_PERMS: Permissions = Permissions::EMBED_LINKS
- | Permissions::ADD_REACTIONS
- | Permissions::SEND_MESSAGES
- | Permissions::SEND_TTS_MESSAGES
- | Permissions::MENTION_EVERYONE
- | Permissions::USE_EXTERNAL_EMOJIS
- | Permissions::CONNECT
- | Permissions::SPEAK
- | Permissions::CHANGE_NICKNAME
- | Permissions::USE_VAD
- | Permissions::ATTACH_FILES;
-}
-
-lazy_static! {
- pub static ref OAUTH_URL: Url = Url::parse(&format!(
- "https://discordapp.com/api/oauth2/authorize?scope=bot&permissions={}&client_id={}",
- REQUIRED_PERMS.bits(),
- CONFIG.discord.auth.client_id,
- ))
- .unwrap();
-}
-
-pub async fn ytdl_url(uri: &str) -> Result<String> {
- use serde_json::Value;
- use tokio::process::Command;
-
- lazy_static! {
- static ref YTDL_COMMAND: String = {
- let result = CONFIG.ytdl.clone().unwrap_or("youtube-dl".to_owned());
- log::debug!("got ytdl: {}", result);
-
- result
- };
- }
-
- let args = [
- "-f",
- "webm[abr>0]/bestaudio/best",
- "--no-playlist",
- "--print-json",
- "--skip-download",
- uri,
- ];
-
- debug!("downloading info for uri: {uri}");
-
- let mut command = Command::new(&*YTDL_COMMAND);
- command.args(&args).stdin(Stdio::null());
-
- debug!("running command: {command:?}");
-
- let out = command.output().await?;
-
- if !out.status.success() {
- return Err(anyhow::anyhow!("running ytdl: {out:?}"));
- }
-
- let value = serde_json::from_reader(&out.stdout[..])?;
- let mut obj = match value {
- Value::Object(obj) => obj,
- other => return Err(anyhow::anyhow!("ytdl output not object: {other:?}")),
- };
-
- match obj.remove("url") {
- Some(v) => match v {
- Value::String(uri) => Ok(uri),
- other => Err(anyhow::anyhow!("url not string: {other:?}")),
- },
- None => Err(anyhow::anyhow!("no url")),
- }
-}
-pub fn parse_times<A: AsRef<str>>(s: A) -> (Option<Duration>, Option<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();
- }
-
- 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)
-}
+use chrono::Duration;
+use serenity::{
+ client::Context,
+ model::{
+ id::{
+ ChannelId,
+ MessageId,
+ },
+ permissions::Permissions,
+ },
+};
+use std::process::Stdio;
+
+use lazy_static::lazy_static;
+use log::debug;
+use regex::{
+ Match,
+ Regex,
+};
+use serenity::all::{
+ CreateMessage,
+ Message,
+};
+use url::Url;
+
+use crate::{
+ commands::songbird,
+ Result,
+ CONFIG,
+};
+
+pub async fn currently_playing(ctx: &Context, msg: &Message) -> bool {
+ let (_sb, call) = songbird(ctx, msg).await.expect("no songbird");
+
+ let call = call.lock().await;
+ call.queue().current().is_some()
+}
+
+pub async fn users_listening(ctx: &Context) -> Result<bool> {
+ let channel = CONFIG.discord.voice_channel().to_channel(&ctx).await?;
+
+ let res = channel
+ .guild()
+ .and_then(|ch| ch.guild(&ctx))
+ .map(|g| {
+ g.voice_states
+ .iter()
+ .any(|(_, state)| state.channel_id == Some(CONFIG.discord.voice_channel()))
+ })
+ .unwrap_or(false);
+
+ Ok(res)
+}
+
+#[inline]
+pub async fn send(
+ ctx: &Context,
+ channel: ChannelId,
+ text: impl AsRef<str>,
+ tts: bool,
+) -> Result<()> {
+ send_result(ctx, channel, text, tts).await.map(|_| ())
+}
+
+pub async fn send_result(
+ ctx: &Context,
+ channel: ChannelId,
+ text: impl AsRef<str>,
+ tts: bool,
+) -> Result<MessageId> {
+ let text = text.as_ref();
+ debug!("sending message {:?} to channel {:?} (tts: {})", text, channel, tts);
+
+ let result = channel.send_message(ctx, CreateMessage::default().content(text).tts(tts)).await?;
+ Ok(result.id)
+}
+
+lazy_static! {
+ static ref REQUIRED_PERMS: Permissions = Permissions::EMBED_LINKS
+ | Permissions::ADD_REACTIONS
+ | Permissions::SEND_MESSAGES
+ | Permissions::SEND_TTS_MESSAGES
+ | Permissions::MENTION_EVERYONE
+ | Permissions::USE_EXTERNAL_EMOJIS
+ | Permissions::CONNECT
+ | Permissions::SPEAK
+ | Permissions::CHANGE_NICKNAME
+ | Permissions::USE_VAD
+ | Permissions::ATTACH_FILES;
+}
+
+lazy_static! {
+ pub static ref OAUTH_URL: Url = Url::parse(&format!(
+ "https://discordapp.com/api/oauth2/authorize?scope=bot&permissions={}&client_id={}",
+ REQUIRED_PERMS.bits(),
+ CONFIG.discord.auth.client_id,
+ ))
+ .unwrap();
+}
+
+pub async fn ytdl_url(uri: &str) -> Result<String> {
+ use serde_json::Value;
+ use tokio::process::Command;
+
+ lazy_static! {
+ static ref YTDL_COMMAND: String = {
+ let result = CONFIG.ytdl.clone().unwrap_or("youtube-dl".to_owned());
+ log::debug!("got ytdl: {}", result);
+
+ result
+ };
+ }
+
+ let args = [
+ "-f",
+ "webm[abr>0]/bestaudio/best",
+ "--no-playlist",
+ "--print-json",
+ "--skip-download",
+ uri,
+ ];
+
+ debug!("downloading info for uri: {uri}");
+
+ let mut command = Command::new(&*YTDL_COMMAND);
+ command.args(args).stdin(Stdio::null());
+
+ debug!("running command: {command:?}");
+
+ let out = command.output().await?;
+
+ if !out.status.success() {
+ return Err(anyhow::anyhow!("running ytdl: {out:?}"));
+ }
+
+ let value = serde_json::from_reader(&out.stdout[..])?;
+ let mut obj = match value {
+ Value::Object(obj) => obj,
+ other => return Err(anyhow::anyhow!("ytdl output not object: {other:?}")),
+ };
+
+ match obj.remove("url") {
+ Some(v) => match v {
+ Value::String(uri) => Ok(uri),
+ other => Err(anyhow::anyhow!("url not string: {other:?}")),
+ },
+ None => Err(anyhow::anyhow!("no url")),
+ }
+}
+pub fn parse_times<A: AsRef<str>>(s: A) -> (Option<Duration>, Option<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();
+ }
+
+ 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)
+}