Bladeren bron

adding more functionality

Zhizhou Ma 3 jaren geleden
bovenliggende
commit
820393f099

+ 1 - 0
.spotify_token_cache.json

@@ -0,0 +1 @@
+{"access_token":"BQBJjw-UD_aNwnXEveJ7v3inISHrPFQXdxM2msuhRqzOje5ZOV_KpkR8jIyIyhk3jH1pV3E8waQ7dTGvSe4","expires_in":3600,"expires_at":"2021-06-11T07:05:31.838345500Z","refresh_token":null,"scope":""}

+ 360 - 0
Cargo.lock

@@ -33,6 +33,27 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "async-stream"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "async-trait"
 version = "0.1.50"
@@ -185,6 +206,7 @@ dependencies = [
  "libc",
  "num-integer",
  "num-traits 0.2.14",
+ "rustc-serialize",
  "serde",
  "time",
  "winapi 0.3.9",
@@ -210,6 +232,22 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "core-foundation"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
+
 [[package]]
 name = "cpufeatures"
 version = "0.1.4"
@@ -244,6 +282,41 @@ dependencies = [
  "cfg-if 0.1.10",
 ]
 
+[[package]]
+name = "darling"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "dashmap"
 version = "4.0.2"
@@ -254,6 +327,37 @@ dependencies = [
  "num_cpus",
 ]
 
+[[package]]
+name = "derive_builder"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73"
+dependencies = [
+ "derive_builder_core",
+ "syn",
+]
+
 [[package]]
 name = "digest"
 version = "0.9.0"
@@ -275,6 +379,12 @@ dependencies = [
  "syntex",
 ]
 
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.28"
@@ -324,6 +434,21 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
 [[package]]
 name = "form_urlencoded"
 version = "1.0.1"
@@ -506,6 +631,15 @@ version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
 
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "hermit-abi"
 version = "0.1.18"
@@ -588,6 +722,25 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes 1.0.1",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
 [[package]]
 name = "idna"
 version = "0.2.3"
@@ -734,6 +887,17 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
 
+[[package]]
+name = "maybe-async"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "memchr"
 version = "2.4.0"
@@ -797,6 +961,24 @@ dependencies = [
  "getrandom 0.2.3",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log 0.4.14",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
 [[package]]
 name = "ntapi"
 version = "0.3.6"
@@ -849,6 +1031,8 @@ name = "octave_rust"
 version = "0.1.0"
 dependencies = [
  "lazy_static",
+ "rand 0.8.3",
+ "rspotify",
  "serenity",
  "songbird",
  "tokio",
@@ -869,6 +1053,39 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
+[[package]]
+name = "openssl"
+version = "0.10.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7830286ad6a3973c0f1d9b73738f69c76b739301d0229c4b96501695cbe4c8"
+dependencies = [
+ "bitflags 1.2.1",
+ "cfg-if 1.0.0",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.11.1"
@@ -1126,6 +1343,15 @@ version = "0.6.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
 
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "reqwest"
 version = "0.11.3"
@@ -1141,12 +1367,14 @@ dependencies = [
  "http-body",
  "hyper",
  "hyper-rustls",
+ "hyper-tls",
  "ipnet",
  "js-sys",
  "lazy_static",
  "log 0.4.14",
  "mime",
  "mime_guess",
+ "native-tls",
  "percent-encoding",
  "pin-project-lite",
  "rustls",
@@ -1154,7 +1382,9 @@ dependencies = [
  "serde_json",
  "serde_urlencoded",
  "tokio",
+ "tokio-native-tls",
  "tokio-rustls",
+ "tokio-socks",
  "url",
  "wasm-bindgen",
  "wasm-bindgen-futures",
@@ -1178,6 +1408,28 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "rspotify"
+version = "0.10.0"
+source = "git+https://github.com/ramsayleung/rspotify#0c859ffe6a50712351304c9457fdcc69be737a83"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "base64 0.13.0",
+ "chrono",
+ "derive_builder",
+ "futures",
+ "getrandom 0.2.3",
+ "log 0.4.14",
+ "maybe-async",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "strum",
+ "thiserror",
+ "url",
+]
+
 [[package]]
 name = "rustc-serialize"
 version = "0.3.24"
@@ -1219,6 +1471,16 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "scoped-tls"
 version = "1.0.0"
@@ -1241,6 +1503,29 @@ dependencies = [
  "untrusted",
 ]
 
+[[package]]
+name = "security-framework"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
+dependencies = [
+ "bitflags 1.2.1",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "serde"
 version = "1.0.126"
@@ -1462,6 +1747,33 @@ dependencies = [
  "loom",
 ]
 
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "strum"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "subtle"
 version = "2.4.0"
@@ -1528,6 +1840,20 @@ dependencies = [
  "unicode-xid 0.0.3",
 ]
 
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "rand 0.8.3",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "term"
 version = "0.4.6"
@@ -1623,6 +1949,16 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-rustls"
 version = "0.22.0"
@@ -1634,6 +1970,18 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "tokio-socks"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
+dependencies = [
+ "either",
+ "futures-util",
+ "thiserror",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-util"
 version = "0.6.7"
@@ -1827,6 +2175,12 @@ dependencies = [
  "tinyvec",
 ]
 
+[[package]]
+name = "unicode-segmentation"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
+
 [[package]]
 name = "unicode-xid"
 version = "0.0.3"
@@ -1888,6 +2242,12 @@ version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0"
 
+[[package]]
+name = "vcpkg"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "025ce40a007e1907e58d5bc1a594def78e5573bb0b1160bc389634e8f12e4faa"
+
 [[package]]
 name = "version_check"
 version = "0.9.3"

+ 2 - 0
Cargo.toml

@@ -8,6 +8,8 @@ edition = "2018"
 
 [dependencies]
 lazy_static = "1.4.0"
+rand = "0.8.3"
+rspotify = {git = "https://github.com/ramsayleung/rspotify"}
 serenity = {version = "0.10", features = ["standard_framework", "voice", "rustls_backend", "cache", "framework"] }
 songbird = "0.1.6"
 tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "time", "sync"] }

+ 166 - 4
src/audio/audio.rs

@@ -2,13 +2,19 @@ use std::{
     sync::Arc,
     collections::HashMap,
 };
+use rspotify::model::idtypes::Track;
 use serenity::{
     client::Context, 
     framework::standard::{
         Args, CommandResult,
         macros::{command, group},
     },
-    http::Http, model::{channel::Message, id::{ChannelId, GuildId}}};
+    http::Http, model::{channel::Message, id::{ChannelId, GuildId}}
+};
+use songbird::tracks::{
+    TrackHandle,
+    TrackCommand,
+};
 use tokio::{
     sync::Mutex,
 };
@@ -17,8 +23,14 @@ use super::{
     audio_state::AudioState,
 };
 
+use crate::util::{
+    send_message,
+    send_embed,
+    message_react,
+};
+
 #[group]
-#[commands(play)]
+#[commands(join,play,skip,pause,resume,shuffle,clear,splay,queue)]
 struct Audio;
 
 lazy_static! {
@@ -45,6 +57,7 @@ async fn get_audio_state(ctx: &Context, msg: &Message) -> Option<Arc<AudioState>
             let channel_id = match channel_id{
                 Some(channel_id) => channel_id,
                 None => {
+                    send_embed(ctx, msg, "Error: please be in a voice channel").await;
                     return None;
                 }
             };
@@ -66,6 +79,41 @@ async fn get_audio_state(ctx: &Context, msg: &Message) -> Option<Arc<AudioState>
     }
 }
 
+async fn remove_audio_state(ctx: &Context, msg: &Message) -> Result<(), String> {
+    let guild = msg.guild(&ctx.cache).await.unwrap();
+    let guild_id = guild.id;
+
+    let mut audio_states = AUDIO_STATES.lock().await;
+
+    if let Some(state) = audio_states.remove(&guild_id){
+        AudioState::cleanup(state).await;
+        Ok(())
+    }else{
+        Err("bot is not currently active".to_string())
+    }
+}
+
+#[command]
+async fn join(ctx: &Context, msg: &Message) -> CommandResult{
+    let audio_state = get_audio_state(ctx, msg).await;
+    if let Some(_) = audio_state{
+        message_react(ctx, msg, "🥳").await;
+    }
+
+    Ok(())
+}
+
+#[command]
+#[aliases("leave")]
+async fn disconnect(ctx: &Context, msg: &Message) -> CommandResult{
+    match remove_audio_state(ctx, msg).await{
+        Ok(()) => message_react(ctx, msg, "👋").await,
+        Err(why) => send_embed(ctx, msg, &format!("Error: {}", why)).await,
+    };
+
+    Ok(())
+}
+
 #[command]
 async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult{
     let query = args.rest();
@@ -75,10 +123,124 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult{
     let audio_state = match audio_state{
         Some(audio_state) => audio_state,
         None => return Ok(())
-        
     };
 
-    AudioState::add_audio(audio_state, query).await;
+    AudioState::add_audio(audio_state, query, false).await;
+
+    message_react(ctx, msg, "🎶").await;
+
+    Ok(())
+}
+
+#[command]
+async fn splay(ctx: &Context, msg: &Message, args: Args) -> CommandResult{
+    let query = args.rest();
+    println!("{}",query);
+
+    let audio_state = get_audio_state(ctx, msg).await;
+    let audio_state = match audio_state{
+        Some(audio_state) => audio_state,
+        None => return Ok(())
+    };
+
+    AudioState::add_audio(audio_state, query, true).await;
+
+    message_react(ctx, msg, "🔀").await;
+    message_react(ctx, msg, "🎶").await;
+
+    Ok(())
+}
+
+#[command]
+async fn skip(ctx: &Context, msg: &Message) -> CommandResult{
+    let audio_state = get_audio_state(ctx, msg).await;
+    let audio_state = match audio_state{
+        Some(audio_state) => audio_state,
+        None => return Ok(())
+    };
+
+    if let Err(why) = AudioState::send_track_command(audio_state, TrackCommand::Stop).await {
+        send_embed(ctx, msg, &format!("Error: {}", why)).await;
+    }else {
+        message_react(ctx, msg, "↪").await;
+    };
+    Ok(())
+}
+
+#[command]
+async fn pause(ctx: &Context, msg: &Message) -> CommandResult{
+    let audio_state = get_audio_state(ctx, msg).await;
+    let audio_state = match audio_state{
+        Some(audio_state) => audio_state,
+        None => return Ok(())
+    };
+
+    if let Err(why) = AudioState::send_track_command(audio_state, TrackCommand::Pause).await {
+        send_embed(ctx, msg, &format!("Error: {}", why)).await;
+    }else {
+        message_react(ctx, msg, "⏸").await;
+    };
+    Ok(())
+}
+
+#[command]
+async fn resume(ctx: &Context, msg: &Message) -> CommandResult{
+    let audio_state = get_audio_state(ctx, msg).await;
+    let audio_state = match audio_state{
+        Some(audio_state) => audio_state,
+        None => return Ok(())
+    };
+
+    if let Err(why) = AudioState::send_track_command(audio_state, TrackCommand::Play).await {
+        send_embed(ctx, msg, &format!("Error: {}", why)).await;
+    }else {
+        message_react(ctx, msg, "▶").await;
+    };
+    Ok(())
+}
+
+#[command]
+async fn shuffle(ctx: &Context, msg: &Message) -> CommandResult{
+    let audio_state = get_audio_state(ctx, msg).await;
+    let audio_state = match audio_state{
+        Some(audio_state) => audio_state,
+        None => return Ok(())
+    };
+
+    if let Err(why) = AudioState::shuffle(audio_state).await {
+        send_embed(ctx, msg, &format!("Error: {}", why)).await;
+    } else{
+        message_react(ctx, msg, "🔀").await;
+    };
+    Ok(())
+}
+
+#[command]
+async fn clear(ctx: &Context, msg: &Message) -> CommandResult{
+    let audio_state = get_audio_state(ctx, msg).await;
+    let audio_state = match audio_state{
+        Some(audio_state) => audio_state,
+        None => return Ok(())
+    };
+
+    if let Err(why) = AudioState::clear(audio_state.clone()).await {
+        send_embed(ctx, msg, &format!("Error: {}", why)).await;
+    } else{
+        message_react(ctx, msg, "🗑").await;
+    };
+
+    Ok(())
+}
+
+#[command]
+async fn queue(ctx: &Context, msg: &Message) -> CommandResult{
+    let audio_state = get_audio_state(ctx, msg).await;
+    let audio_state = match audio_state{
+        Some(audio_state) => audio_state,
+        None => return Ok(())
+    };
+
+    send_embed(ctx, msg,&AudioState::get_string(audio_state).await).await;
 
     Ok(())
 }

+ 82 - 33
src/audio/audio_state.rs

@@ -1,30 +1,39 @@
 use std::{
     sync::Arc,
 };
+use rand::seq::SliceRandom;
 use super::{
     song_queue::SongQueue,
     song::{
         Song,
-        SongMetadata,
     },
+    query::process_query,
+    subprocess::ffmpeg_pcm,
 };
-use songbird::{
-    Call,
-    input,
-    EventHandler as VoiceEventHandler,
-    Event,
-    EventContext,
-    TrackEvent,
+use songbird::{Call, Event, EventContext, EventHandler as VoiceEventHandler, TrackEvent, 
+    input::{
+        self,
+        reader::Reader,
+    }, 
+    tracks::{
+        TrackHandle,
+        TrackCommand,
+    }
+
 };
+use tokio::sync::Mutex;
 use serenity::{
     async_trait,
     prelude::{
         Mutex as SerenityMutex,
     }
 };
+
 pub struct AudioState{
     queue: SongQueue,
     handler: Arc<SerenityMutex<Call>>,
+    current_song: Mutex<Option<Song>>,
+    track_handle: Mutex<Option<TrackHandle>>,
 }
 
 impl AudioState{
@@ -32,6 +41,8 @@ impl AudioState{
         let audio_state = AudioState{
             queue: SongQueue::new(),
             handler: handler.clone(),
+            current_song: Mutex::new(None),
+            track_handle: Mutex::new(None),
         };
         let audio_state = Arc::new(audio_state);
         {
@@ -48,28 +59,23 @@ impl AudioState{
         println!("hi3");
         let url = song.get_url().await;
         println!("hi4 {}", url);
-        let source = input::ffmpeg_optioned(url, &[
-            "-reconnect","1",
-            "-reconnect_streamed","1",
-            "-reconnect_delay_max","5",
-        ], &[
-            "-f","s16le",
-            "-ar","48000",
-            "-ac","2",
-            "-acodec","pcm_f32le",
-            "-"
-        ]);
-        let source = match source.await {
+        let source = ffmpeg_pcm(url).await;
+        let source = match source {
             Ok(source) => source,
             Err(why) => {
-                panic!("Err AudioState::play_audio: {:?}", why);
+                println!("Error in AudioState::play_audio: {}",why);
+                return
             }
         };
-        
+        let reader = Reader::Extension(source);
+        let source = input::Input::float_pcm(true, reader);
+
+        println!("hi4.5");
         let mut handler = audio_state.handler.lock().await;
-        let song = handler.play_source(source);
+        
+        let handle = handler.play_source(source);
 
-        if let Err(why) = song.add_event(
+        if let Err(why) = handle.add_event(
             Event::Track(TrackEvent::End),
             SongEndNotifier{
                 audio_state: audio_state.clone(),
@@ -77,18 +83,60 @@ impl AudioState{
         ){
             panic!("Err AudioState::play_audio: {:?}", why);
         }
+        
+        let mut current_song = audio_state.current_song.lock().await;
+        *current_song = Some(song);
+        let mut track_handle = audio_state.track_handle.lock().await;
+        *track_handle = Some(handle);
+        println!("hi5");
+    }
+
+    pub async fn add_audio(audio_state: Arc<AudioState>, query: &str, shuffle: bool){
+        let mut songs = match process_query(query).await{
+            Ok(songs) => songs,
+            Err(why) => {
+                println!("Error add_audio: {}", why);
+                return;
+            },
+        };
+        if shuffle {
+            songs.shuffle(&mut rand::thread_rng());
+        }
+        audio_state.queue.push(songs).await;
+    }
+
+    pub async fn send_track_command(audio_state: Arc<AudioState>, cmd: TrackCommand) -> Result<(), String>{
+        let track_handle = audio_state.track_handle.lock().await;
+        match &*track_handle {
+            Some(track_handle) => {
+                match track_handle.send(cmd){
+                    Ok(()) => Ok(()),
+                    Err(why) => Err(format!("{:?}",why))
+                }
+            },
+            None => Err("no song currently playing".to_string())
+        }
+    }
+
+    pub async fn shuffle(audio_state: Arc<AudioState>) -> Result<(), String>{
+        audio_state.queue.shuffle().await
+    }
+
+    pub async fn clear(audio_state: Arc<AudioState>) -> Result<(), String>{
+        audio_state.queue.clear().await
+    }
+
+    pub async fn cleanup(audio_state: Arc<AudioState>) {
+        audio_state.queue.cleanup().await;
     }
 
-    pub async fn add_audio(audio_state: Arc<AudioState>, query: &str){
-        let metadata = SongMetadata{
-            artist: None,
-            title: None,
-            duration: None,
-            query: Some(query.to_string()),
+    pub async fn get_string(audio_state: Arc<AudioState>) -> String {
+        let current_song = audio_state.current_song.lock().await;
+        let current_song = match &*current_song {
+            Some(song) => song.get_string().await,
+            None => "*Not playing*\n".to_string(),
         };
-        let song_work = Song::new_load(metadata).unwrap();
-        println!("hi1");
-        audio_state.queue.push(vec![song_work]).await;
+        format!("**Current Song:**\n{}\n**Queue:**\n{}", current_song, audio_state.queue.get_string().await)
     }
 }
 
@@ -101,6 +149,7 @@ struct SongEndNotifier {
 #[async_trait]
 impl VoiceEventHandler for SongEndNotifier {
     async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
+        println!("SongEndNotifier event has started");
         AudioState::play_audio(self.audio_state.clone()).await;
 
         None

+ 8 - 3
src/audio/loader.rs

@@ -3,7 +3,7 @@ use tokio::{
 };
 use super::{
     work::Work,
-    youtube::ytdl,
+    subprocess::ytdl,
 };
 
 pub struct Loader {
@@ -43,6 +43,12 @@ impl Loader{
             }
         }
     }
+
+    pub async fn cleanup(&mut self) {
+        if let Err(why) = self.kill.send(()).await{
+            println!("Error on Loader::cleanup: {}",why);
+        };
+    }
     
     pub fn new() -> Loader{
         let (work_tx, work_sx) = mpsc::channel(200);
@@ -63,5 +69,4 @@ impl Loader{
         kill.recv().await;
         f.abort();
     }
-}
-
+}

+ 3 - 1
src/audio/mod.rs

@@ -4,7 +4,9 @@ pub mod audio_state;
 mod song;
 mod loader;
 mod song_queue;
-mod youtube;
+mod subprocess;
 mod work;
+mod spotify;
+mod query;
 
 pub use audio::*;

+ 49 - 0
src/audio/query.rs

@@ -0,0 +1,49 @@
+use super::{
+    spotify::get_playlist,
+    song::{
+        Song,
+        SongMetadata,
+    },
+    work::Work,
+};
+
+pub async fn process_query(query: &str) -> Result<Vec<(Song, Option<Work>)>, String>{
+    if query.contains("spotify") && query.contains("/playlist/"){
+        let split: Vec<&str> = query
+            .split("/playlist/")
+            .filter(|s| s.len() > 0)
+            .collect();
+        if split.len() != 2 {
+            return Err("invalid spotify playlist URL".to_string());
+        }
+        let playlist_id = split[1];
+        let playlist_id = playlist_id
+            .split("?")
+            .filter(|s| s.len() > 0)
+            .next()
+            .expect("Logical error: process_query's playlist_id contains items?");
+        let songs = match get_playlist(playlist_id).await{
+            Ok(songs) => songs,
+            Err(why) => return Err(why),
+        };
+        return Ok(songs);
+    } else {
+        let data = if query.contains("watch?v=") {
+            (Some(query.to_string()), None)
+        } else {
+            (None, Some(query.to_string()))
+        };
+        let metadata = SongMetadata{
+            artist: None,
+            title: None,
+            duration: None,
+            search_query: data.1,
+            youtube_url: data.0,
+        };
+        let song = match Song::new_load(metadata){
+            Some(song) => song,
+            None => return Err("failed to get song from YouTube".to_string()),
+        };
+        return Ok(vec![song]);
+    };
+}

+ 38 - 11
src/audio/song.rs

@@ -10,43 +10,50 @@ pub enum SongUrlState{
     Proc{
         is_loaded: Arc<Mutex<bool>>,
         receiver: mpsc::Receiver<String>,
+        work: Work,
     }
 }
 
 pub struct SongMetadata{
     pub artist: Option<String>,
     pub title: Option<String>,
-    pub query: Option<String>,
-    pub duration: Option<u32>,
+    pub search_query: Option<String>,
+    pub youtube_url: Option<String>,
+    pub duration: Option<u64>,
 }
 
 pub struct Song{
-    url_state: SongUrlState,
+    pub url_state: SongUrlState,
     url: Arc<Mutex<Option<String>>>,
     metadata: SongMetadata,
 }
 
 impl Song{
-    pub fn new_load(metadata: SongMetadata) -> Option<(Song, Work)>{
-        let query = metadata.query.clone()?;
+    pub fn new_load(metadata: SongMetadata) -> Option<(Song, Option<Work>)>{
+        let query = match metadata.youtube_url.clone(){
+            Some(url) => url,
+            None => format!("ytsearch:{}", metadata.search_query.clone()?),
+        };
 
         let (tx, rx) = mpsc::channel(1);
         let is_loaded = Arc::new(Mutex::new(false));
-        let url_state = SongUrlState::Proc{
-            is_loaded: is_loaded.clone(),
-            receiver: rx,
-        };
         let work = Work{
             sender: tx,
             is_loaded: is_loaded.clone(),
-            query,
+            query: query.clone(),
+        };
+        let url_state = SongUrlState::Proc{
+            is_loaded: is_loaded.clone(),
+            receiver: rx,
+            work: work.clone(),
         };
+        
         let song = Song{
             url_state,
             url: Arc::new(Mutex::new(None)),
             metadata,
         };
-        Some((song, work))
+        Some((song, Some(work)))
     }
     pub fn get_metadata(&self) -> &SongMetadata {
         return &self.metadata;
@@ -70,6 +77,26 @@ impl Song{
             },
         };
     }
+    pub async fn get_string(&self) -> String{
+        let metadata = &self.metadata;
+        let artist = match &metadata.artist{
+            Some(artist) => artist,
+            None => "unknown",
+        };
+        let title = match &metadata.artist{
+            Some(title) => title,
+            None => "unknown",
+        };
+        let duration = match &metadata.duration{
+            Some(duration) => {
+                let mins = duration / 60;
+                let secs = duration - mins * 60;
+                format!("{}:{:0>2}", mins, secs)
+            },
+            None => "unknown duration".to_string(),
+        };
+        format!("{} by {} | {}", title, artist, &duration)
+    }
 }
 
 /*

+ 62 - 6
src/audio/song_queue.rs

@@ -4,13 +4,17 @@ use std::{
     mem::drop,
 };
 use tokio::sync::{Semaphore, Mutex};
+use rand::seq::SliceRandom;
 use super::{
-    song::Song,
+    song::{
+        Song,
+        SongUrlState,
+    },
     loader::Loader,
     work::Work,
 };
 pub struct SongQueue{
-    loader: Loader,
+    loader: Arc<Mutex<Loader>>,
     queue: Arc<Mutex<VecDeque<Song>>>,
     queue_sem: Semaphore,
 }
@@ -18,24 +22,76 @@ pub struct SongQueue{
 impl SongQueue{
     pub fn new() -> SongQueue {
         SongQueue{
-            loader: Loader::new(),
+            loader: Arc::new(Mutex::new(Loader::new())),
             queue: Arc::new(Mutex::new(VecDeque::new())),
             queue_sem: Semaphore::new(0),
         }
     }
-    pub async fn push(&self, songs: Vec<(Song, Work)>){
+    pub async fn push(&self, songs: Vec<(Song, Option<Work>)>){
         let mut queue = self.queue.lock().await;
         let count = songs.len();
+        let loader = self.loader.lock().await;
         for item in songs.into_iter(){
             queue.push_back(item.0);
-            self.loader.add_work(item.1).await;
+            match item.1{
+                Some(work) => loader.add_work(work).await,
+                None =>{},
+            };
+            //self.loader.add_work(item.1).await;
         }
         drop(queue);
         self.queue_sem.add_permits(count);
     }
     pub async fn pop(&self) -> Song {
-        self.queue_sem.acquire().await.expect("Error SongQueue.pop: semaphore acquire failed").forget();
+        self.queue_sem.acquire().await.expect("Error SongQueue.pop: semaphore acquire() failed").forget();
         let mut queue = self.queue.lock().await;
         queue.pop_front().expect("Error SongQueue.pop: semaphore sync failure")
     }
+    pub async fn shuffle(&self) -> Result<(), String> {
+        let mut queue = self.queue.lock().await;
+        if queue.len() == 0 {
+            return Err("queue is empty".to_string());
+        }
+        queue.make_contiguous().shuffle(&mut rand::thread_rng());
+
+        self.reset_loader().await;
+        let loader = self.loader.lock().await;
+
+        for song in queue.iter(){
+            match &song.url_state{
+                SongUrlState::Proc{work,..} => loader.add_work(work.clone()).await,
+            };
+        };
+
+        Ok(())
+    }
+    pub async fn clear(&self) -> Result<(), String>{
+        let mut queue = self.queue.lock().await;
+        if queue.len() == 0 {
+            return Err("queue is empty".to_string());
+        };
+        queue.clear();
+        Ok(())
+    }
+    async fn reset_loader(&self) {
+        let mut loader = self.loader.lock().await;
+        loader.cleanup().await;
+        *loader = Loader::new();
+    }
+    pub async fn cleanup(&self) {
+        let mut loader = self.loader.lock().await;
+        loader.cleanup().await;
+    }
+    pub async fn get_string(&self) -> String{
+        let queue = self.queue.lock().await;
+        if queue.len() == 0 {
+            return "*empty*".to_string();
+        };
+        let mut s = String::new();
+        for song in queue.iter(){
+            s += &song.get_string().await;
+            s += "\n";
+        }
+        s
+    }
 }

+ 75 - 0
src/audio/spotify.rs

@@ -0,0 +1,75 @@
+use rspotify::{
+    client::SpotifyBuilder,
+    model::{
+        Id,
+        PlayableItem,
+    },
+    oauth2::CredentialsBuilder,
+};
+
+use super::{
+    song::{
+        Song,
+        SongMetadata,
+    },
+    work::Work,
+};
+
+pub async fn get_playlist(playlist_id: &str) -> Result<Vec<(Song, Option<Work>)>, String>{
+    let creds = CredentialsBuilder::default()
+        .id("5f573c9620494bae87890c0f08a60293")
+        .secret("212476d9b0f3472eaa762d90b19b0ba8")
+        .build()
+        .unwrap();
+    let mut spotify = SpotifyBuilder::default()
+        .credentials(creds)
+        //.oauth(oauth)
+        .build()
+        .unwrap();
+    if let Err(why) = spotify.request_client_token().await{
+        println!("error: {}", why);
+    };
+    let playlist_id = Id::from_id(playlist_id);
+    let playlist_id = match playlist_id{
+        Ok(playlist_id) => playlist_id,
+        Err(why) => {
+            return Err(format!("spotify::get_playlist: {:?}", why));
+        }
+    };
+    let tracks = spotify.playlist(playlist_id, None, None).await;
+    let tracks = match tracks{
+        Ok(tracks) => tracks,
+        Err(why)=>{
+            println!("Error in spotify.get_playlist: {:?}", why);
+            return Err(format!("spotify::get_playlist: {:?}", why));
+        }
+    };
+    let mut songs = Vec::new();
+    let tracks = tracks.tracks.items;
+    for data in tracks.iter() {
+        let track = match &data.track{
+            Some(PlayableItem::Track(track)) => track,
+            Some(_) => continue,
+            None => continue,
+        };
+        let artist = &track.artists[0].name;
+        let title = &track.name;
+        let metadata = SongMetadata{
+            search_query: Some(get_query_string(artist, title)),
+            artist: Some(artist.clone()),
+            title: Some(title.clone()),
+            youtube_url: None,
+            duration: Some(track.duration.as_secs()),
+        };
+        match Song::new_load(metadata){
+            Some(data) => songs.push(data),
+            None => continue,
+        };
+    }
+    println!("spotdl function finished");
+    Ok(songs)
+}
+
+fn get_query_string(artist: &str, title: &str) -> String{
+    format!("{} {} lyrics", artist, title)
+}

+ 60 - 0
src/audio/subprocess.rs

@@ -0,0 +1,60 @@
+use std::{
+    io::{BufReader, Read},
+    process::{
+        Command,
+        Stdio,
+    },
+};
+use tokio::process::Command as TokioCommand;
+
+pub async fn ytdl(query: &str) -> String{
+    let mut cmd = TokioCommand::new("youtube-dl");
+    let cmd = cmd
+                       .arg("-x")
+                       .arg("--skip-download")
+                       .arg("--get-url")
+                       .arg("--audio-quality").arg("128k")
+                       .arg(query);
+    let out = cmd.output().await.unwrap();
+    println!("ytdl process finished");
+    String::from_utf8(out.stdout).unwrap()
+}
+//  -> Result<Box<dyn Read + Send>, String>
+pub async fn ffmpeg_pcm(url: String) -> Result<Box<dyn Read + Send>, String>{
+    /*let res = tokio::task::spawn_blocking(move ||{
+        
+    }).await.unwrap();
+    res*/
+    let mut cmd = Command::new("ffmpeg");
+    let cmd = cmd
+        .arg("-reconnect").arg("1")
+        .arg("-reconnect_streamed").arg("1")
+        .arg("-reconnect_delay_max").arg("5")
+        .arg("-i").arg(url)
+        .arg("-f").arg("s16le")
+        .arg("-ar").arg("48000")
+        .arg("-ac").arg("2")
+        .arg("-acodec").arg("pcm_f32le")
+        .arg("pipe:1")
+        .stdout(Stdio::piped())
+        .stderr(Stdio::null());
+    let child = match cmd.spawn(){
+        Ok(child) => child,
+        Err(error) => {
+            return Err(format!("{}",error));
+        }
+    };
+    let out = match child.stdout{
+        Some(out) => out,
+        None => return Err("subprocess::ffmpeg_pcm: failed to get child stdout".to_string()),
+    };
+    let buf = BufReader::with_capacity(16384, out);
+    let buf: Box<dyn Read + Send> = Box::new(buf);
+    Ok(buf)
+}
+
+
+/*
+fn ffmpeg(url: &str){
+    
+}*/

+ 1 - 0
src/audio/work.rs

@@ -3,6 +3,7 @@ use std::{
 };
 use tokio::sync::{mpsc, Mutex};
 
+#[derive(Clone)]
 pub struct Work{
     pub sender: mpsc::Sender<String>,
     pub query: String,

+ 0 - 27
src/audio/youtube.rs

@@ -1,27 +0,0 @@
-
-use tokio::process::Command;
-
-pub async fn ytdl(query: &str) -> String{
-    let mut cmd = Command::new("youtube-dl");
-    let cmd = cmd
-                       .arg("-x")
-                       .arg("--skip-download")
-                       .arg("--get-url")
-                       .arg("--audio-quality").arg("128k")
-                       .arg(format!("ytsearch:{}", query));
-    let out = cmd.output().await.unwrap();
-    println!("ytdl process finished");
-    String::from_utf8(out.stdout).unwrap()
-}
-/*
-fn ffmpeg(url: &str){
-    let cmd = Command::new("ffmpeg")
-                       .arg("-reconnect").arg("1")
-                       .arg("-reconnect_streamed").arg("1")
-                       .arg("-reconnect_delay_max").arg("5")
-                       .arg("-i").arg(url)
-                       .arg("-f").arg("s16le")
-                       .arg("-ar").arg("48000")
-                       .arg("-ac").arg("2")
-                       .arg("pipe:1");
-}*/

+ 3 - 1
src/main.rs

@@ -1,3 +1,4 @@
+use std::env;
 use serenity::{async_trait, client::{Client, Context, EventHandler}, framework::{
         StandardFramework,
     }, model::gateway::Ready};
@@ -7,6 +8,7 @@ use songbird::{
 };
 
 mod audio;
+mod util;
 
 struct Handler;
 
@@ -20,7 +22,7 @@ impl EventHandler for Handler{
 
 #[tokio::main]
 async fn main() {
-    let token = "ODQyMzUyNjcxMTE2NDkyODIy.YJ0EDg.JP9PZHKGc23ZF_5W-j-gipnbYW8";
+    let token = env::var("OCTAVE_BOT_TOKEN").expect("Error: token not found");
     let framework = StandardFramework::new()
         .configure(|c|{
             c.prefix("a.")

+ 7 - 0
src/util/mod.rs

@@ -0,0 +1,7 @@
+pub mod util;
+
+pub use util::{
+    send_message,
+    send_embed,
+    message_react,
+};

+ 35 - 0
src/util/util.rs

@@ -0,0 +1,35 @@
+use serenity::{
+    client::Context, 
+    model::{channel::Message, prelude::ReactionType}
+};
+
+pub async fn send_message(ctx: &Context, msg: &Message, text: &str){
+    let res = msg.channel_id.send_message(&ctx.http, |m| {
+        m.content(text);
+        m
+    }).await;
+    if let Err(why) = res{
+        println!("Error sending embed: {:?}", why);
+    };
+}
+
+pub async fn send_embed(ctx: &Context, msg: &Message, text: &str){
+    let res = msg.channel_id.send_message(&ctx.http, |m| {
+        m.embed(|e| {
+            e.colour(0xf542bf);
+            e.description(text);
+            e
+        });
+        m
+    }).await;
+    if let Err(why) = res{
+        println!("Error sending embed: {:?}", why);
+    };
+}
+
+pub async fn message_react(ctx: &Context, msg: &Message, emoji: &str){
+    let res = msg.react(&ctx.http, ReactionType::Unicode(emoji.to_string())).await;
+    if let Err(why) = res{
+        println!("Error reacting to message: {:?}", why);
+    }
+}