diff --git a/Cargo.lock b/Cargo.lock index 19710aa..d3eb832 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,7 @@ dependencies = [ "futures", "regex", "reqwest", + "serde_json", "songbird", "symphonia", "tokio", diff --git a/Cargo.toml b/Cargo.toml index e8adfd8..ec4c009 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ twilight-standby = "0.15" twilight-cache-inmemory = "0.15" twilight-util = { version = "0.15", features=["builder"] } dotenv = "0.15.0" +serde_json = "1.0" diff --git a/src/main.rs b/src/main.rs index 913d3b1..28b6ca5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,23 @@ use dotenv::dotenv; use futures::StreamExt; +use serde_json::Value; use songbird::{ input::{Compose, YoutubeDl}, shards::TwilightMap, + typemap::TypeMapKey, Songbird, }; use std::{ - env, error::Error, future::Future, num::NonZeroU64, ops::Sub, sync::Arc, time::Duration, + env, + error::Error, + future::Future, + io::{BufRead, BufReader}, + num::NonZeroU64, + ops::Sub, + sync::Arc, + time::Duration, }; +use tokio::process::Command; use tracing::debug; use twilight_cache_inmemory::InMemoryCache; use twilight_gateway::{ @@ -30,11 +40,21 @@ struct StateRef { standby: Standby, } +struct Metadata { + title: Option, + artist: Option, +} +struct MetadataMap; +impl TypeMapKey for MetadataMap { + type Value = Metadata; +} + enum ChatCommand { Play(Message, String), Stop(Message), Leave(Message), Join(Message), + Queue(Message), NotImplemented, } @@ -52,6 +72,7 @@ fn parse_command(event: Event) -> Option { ["!stop"] | ["!stop", _] => Some(ChatCommand::Stop(msg_create.0)), ["!leave"] | ["!leave", _] => Some(ChatCommand::Leave(msg_create.0)), ["!join"] | ["!join", _] => Some(ChatCommand::Join(msg_create.0)), + ["!queue"] | ["!queue", _] => Some(ChatCommand::Queue(msg_create.0)), _ => Some(ChatCommand::NotImplemented), } } @@ -153,6 +174,7 @@ async fn main() -> Result<(), Box> { Some(ChatCommand::Stop(msg)) => spawn(stop(msg, Arc::clone(&state))), Some(ChatCommand::Leave(msg)) => spawn(leave(msg, Arc::clone(&state))), Some(ChatCommand::Join(msg)) => spawn(join(msg, Arc::clone(&state))), + Some(ChatCommand::Queue(msg)) => spawn(queue(msg, Arc::clone(&state))), _ => {} } } @@ -175,6 +197,12 @@ async fn join(msg: Message, state: State) -> Result<(), Box Result<(), Box Result, Box> { + let output = Command::new("yt-dlp") + .args(vec![&url, "--flat-playlist", "-j"]) + .output() + .await?; + + let reader = BufReader::new(output.stdout.as_slice()); + let urls = reader + .lines() + .flatten() + .map(|line| { + let entry: Value = serde_json::from_str(&line).unwrap(); + entry + .get("webpage_url") + .unwrap() + .as_str() + .unwrap() + .to_string() + }) + .collect(); + + Ok(urls) +} + +async fn queue(msg: Message, state: State) -> Result<(), Box> { + tracing::debug!( + "queue command in channel {} by {}", + msg.channel_id, + msg.author.name + ); + let guild_id = msg.guild_id.unwrap(); + + if let Some(call_lock) = state.songbird.get(guild_id) { + let call = call_lock.lock().await; + let queue = call.queue().current_queue(); + let mut message = String::new(); + message.push_str("Currently playing:\n"); + for track in queue { + let map = track.typemap().read().await; + let metadata = map.get::().unwrap(); + message.push_str( + format!( + "* {}\n", + metadata.title.clone().unwrap_or("Unknown".to_string()), + ) + .as_str(), + ); + } + state + .http + .create_message(msg.channel_id) + .content(&message)? + .await?; + } + Ok(()) +} + async fn play( msg: Message, state: State, @@ -204,39 +291,42 @@ async fn play( let guild_id = msg.guild_id.unwrap(); - let mut src = YoutubeDl::new(reqwest::Client::new(), query); - if let Ok(metadata) = src.aux_metadata().await { - debug!("metadata: {:?}", metadata); - - state - .http - .create_message(msg.channel_id) - .content(&format!( - "Playing **{:?}** by **{:?}**", - metadata.title.as_ref().unwrap_or(&"".to_string()), - metadata.artist.as_ref().unwrap_or(&"".to_string()), - ))? - .await?; - - if let Some(call_lock) = state.songbird.get(guild_id) { - let mut call = call_lock.lock().await; - let _handle = call.enqueue_with_preload( - src.into(), - metadata.duration.map(|duration| -> Duration { - if duration.as_secs() > 5 { - duration.sub(Duration::from_secs(5)) - } else { - duration - } - }), - ); - } + let urls = if query.contains("list=") { + get_playlist_urls(query).await? } else { - state - .http - .create_message(msg.channel_id) - .content("Didn't find any results")? - .await?; + vec![query] + }; + + for url in urls { + let mut src = YoutubeDl::new(reqwest::Client::new(), url.to_string()); + if let Ok(metadata) = src.aux_metadata().await { + debug!("metadata: {:?}", metadata); + + if let Some(call_lock) = state.songbird.get(guild_id) { + let mut call = call_lock.lock().await; + let handle = call.enqueue_with_preload( + src.into(), + metadata.duration.map(|duration| -> Duration { + if duration.as_secs() > 5 { + duration.sub(Duration::from_secs(5)) + } else { + duration + } + }), + ); + let mut x = handle.typemap().write().await; + x.insert::(Metadata { + title: metadata.title, + artist: metadata.artist, + }); + } + } else { + state + .http + .create_message(msg.channel_id) + .content("Cannot find any results")? + .await?; + } } Ok(())