use std::process::Stdio; use anyhow::anyhow; use diesel::result::Error as DieselError; use lazy_static::lazy_static; use log::{ debug, error, warn, }; use serenity::{ all::ReactionType, framework::standard::{ macros::command, Args, CommandError, CommandResult, Delimiter, }, futures::TryFutureExt, model::channel::Message, prelude::*, }; use tap::Pipe; use tokio::{ io::AsyncReadExt, process::Command, }; use url::Url; use crate::{ db::{ connection, Audio, Image, NewMeme, }, parse_times, util, FFMPEG_COMMAND, }; lazy_static! { static ref DELIMS: Vec = vec![' '.into(), '\n'.into(), '\t'.into()]; } #[command] pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let mut args = Args::new(args.rest(), DELIMS.as_ref()); let title = args.single_quoted::()?; let text = args.rest().to_owned(); let text = if text.is_empty() { None } else { Some(text) }; let mut conn = connection().await?; let image = msg.attachments.first(); if image.is_none() && text.is_none() { warn!("tried to create non-audio meme with no image or text"); return util::send(ctx, msg.channel_id, "hahAA it's empty xdddd", msg.tts) .map_err(CommandError::from) .await; } let mut image_id = None; if let Some(att) = image { let data = att.download().await?; image_id = Some(Image::create(&mut conn, &att.filename, data, msg.author.id.get()).await?); }; let save_result = NewMeme { title, content: text, image_id, audio_id: None, metadata_id: 0, } .save(&mut conn, msg.author.id.get()) .await .map(|_| {}); use diesel::result::DatabaseErrorKind; match save_result { Ok(_) => { msg.react(&ctx, ReactionType::Unicode("👌".to_string())).await?; }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::() { error!("tried to create meme that already exists"); msg.react(&ctx, ReactionType::Unicode("❌".to_owned())).await?; return util::send(ctx, msg.channel_id, "that meme already exists", msg.tts) .map_err(CommandError::from) .await; } return Err(e.into()); }, } Ok(()) } #[command] pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { debug!("running addaudiomeme"); let mut args = Args::new(args.rest(), DELIMS.as_ref()); let title = args.single_quoted::()?; let audio_str = args.single_quoted::()?; let elems = audio_str.split_whitespace().collect::>(); if elems.is_empty() { util::send(ctx, msg.channel_id, "are you stupid", msg.tts).await?; return Err(anyhow!("no audio link was provided").into()); } let audio_link = Url::parse(elems[0])?; let opts = elems[1..].join(" "); let (start, end) = parse_times(opts); let youtube_url = util::ytdl_url(audio_link.as_str()).await?; debug!("got download url: {youtube_url}"); let duration_opts = if let Some(e) = end { vec![ "-ss".to_owned(), start.map_or_else( || "00:00:00".to_owned(), |s| { format!( "{:02}:{:02}:{:02}", s.num_hours(), s.num_minutes() % 60, s.num_seconds() % 60 ) }, ), "-to".to_owned(), format!("{:02}:{:02}:{:02}", e.num_hours(), e.num_minutes() % 60, e.num_seconds() % 60), ] } else { vec![] }; let ffmpeg_command = Command::new(&*FFMPEG_COMMAND) .arg("-i") .arg(youtube_url) .args(duration_opts) .args([ "-ac", "2", "-ar", "48000", "-f", "opus", "-acodec", "libopus", "-b:a", "96k", "-fs", "5M", "-", ]) .stdout(Stdio::piped()) .stderr(Stdio::null()) .stdin(Stdio::null()) .spawn()?; let mut audio_reader = ffmpeg_command.stdout.unwrap(); let text = args.rest().to_owned(); let text = if text.is_empty() { None } else { Some(text) }; let mut conn = connection().await?; let image_att = msg.attachments.first().ok_or(anyhow!("no attachment")); let mut image_id = None; if let Ok(att) = image_att { let data = att.download().await?; image_id = Image::create(&mut conn, &att.filename, data, msg.author.id.get()).await?.pipe(Some); } let mut audio_data = Vec::new(); let bytes = audio_reader.read_to_end(&mut audio_data).await?; debug!("downloaded audio ({} bytes)", audio_data.len()); if bytes == 0 { debug!("read 0 bytes from audio reader"); return util::send(ctx, msg.channel_id, "🔇🔇🔇🔕🔕🔕🔕🔕🔇🔕🔕🔇🔕🔕📣📢📣📢📣", msg.tts) .map_err(CommandError::from) .await; } let audio_id = Audio::create(&mut conn, audio_data, msg.author.id.get()).await?; let save_result = NewMeme { title, content: text, image_id, audio_id: Some(audio_id), metadata_id: 0, } .save(&mut conn, msg.author.id.get()) .await .map(|_| {}); use diesel::result::DatabaseErrorKind; match save_result { Ok(_) => { msg.react(&ctx, ReactionType::Unicode("👌".to_owned())).await?; }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::() { error!("tried to create meme that already exists"); msg.react(&ctx, ReactionType::Unicode("❌".to_owned())).await?; return util::send(ctx, msg.channel_id, "that meme already exists", msg.tts) .map_err(CommandError::from) .await; } return Err(e.into()); }, } Ok(()) }