diff --git a/Cargo.lock b/Cargo.lock index 166a884..c061fe4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,61 @@ dependencies = [ "paste", ] +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower 0.5.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -652,7 +707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1251,7 +1306,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -1270,7 +1325,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -1283,10 +1338,16 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8588661a8607108a5ca69cab034063441a0413a0b041c13618a7dd348021ef6f" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "serde", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1596,6 +1657,19 @@ dependencies = [ "webpki-roots 0.26.6", ] +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.9" @@ -1664,6 +1738,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.5.0" @@ -1671,7 +1755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -2108,6 +2192,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "maybe-async" version = "0.2.10" @@ -2517,7 +2607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.5.0", ] [[package]] @@ -2753,7 +2843,7 @@ checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d" dependencies = [ "autocfg", "equivalent", - "indexmap", + "indexmap 2.5.0", ] [[package]] @@ -2765,6 +2855,59 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.79", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "prost-types" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" +dependencies = [ + "prost", +] + [[package]] name = "protobuf" version = "3.5.1" @@ -2798,7 +2941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b0e9b447d099ae2c4993c0cbb03c7a9d6c937b17f2d56cfc0b1550e6fcfdb76" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.5.0", "log", "protobuf", "protobuf-support", @@ -3976,6 +4119,8 @@ dependencies = [ "rustls 0.23.13", "serenity", "songbird", + "spoticord_api", + "spoticord_api_grpc", "spoticord_config", "spoticord_database", "spoticord_player", @@ -3983,6 +4128,34 @@ dependencies = [ "spoticord_stats", "spoticord_utils", "tokio", + "tonic", +] + +[[package]] +name = "spoticord_api" +version = "0.1.0" +dependencies = [ + "axum", + "env_logger", + "log", + "prost", + "spoticord_api_grpc", + "spoticord_database", + "spoticord_session", + "thiserror", + "tokio", + "tonic", +] + +[[package]] +name = "spoticord_api_grpc" +version = "0.1.0" +dependencies = [ + "bytes", + "prost", + "tokio", + "tonic", + "tonic-build", ] [[package]] @@ -4645,6 +4818,92 @@ dependencies = [ "tokio", ] +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -4870,7 +5129,7 @@ checksum = "5dece5c06268af6a9ff4541788601e560a4284ffebfb357f713d676f13b964db" dependencies = [ "chrono", "dashmap", - "hashbrown", + "hashbrown 0.14.5", "mini-moka", "parking_lot", "secrecy", diff --git a/Cargo.toml b/Cargo.toml index 0b780a6..247b2c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ members = [ "spoticord_session", "spoticord_utils", "spoticord_stats", + "spoticord_api", + "spoticord_api_grpc", ] [features] @@ -30,6 +32,8 @@ spoticord_player = { path = "./spoticord_player" } spoticord_session = { path = "./spoticord_session" } spoticord_utils = { path = "./spoticord_utils" } spoticord_stats = { path = "./spoticord_stats", optional = true } +spoticord_api = { path = "./spoticord_api"} +spoticord_api_grpc = { path = "./spoticord_api_grpc"} anyhow = "1.0.86" dotenvy = "0.15.7" @@ -40,6 +44,7 @@ serenity = "0.12.2" songbird = { version = "0.4.3", features = ["simd-json"] } tokio = { version = "1.39.3", features = ["full"] } rustls = { version = "0.23.13", features = ["aws-lc-rs"] } +tonic = "0.12.3" [profile.release] opt-level = 3 diff --git a/spoticord_api/Cargo.toml b/spoticord_api/Cargo.toml new file mode 100644 index 0000000..95a341e --- /dev/null +++ b/spoticord_api/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "spoticord_api" +version = "0.1.0" +edition = "2021" + +[dependencies] +spoticord_database = { path = "../spoticord_database" } +spoticord_session = { path = "../spoticord_session" } +spoticord_api_grpc = { path = "../spoticord_api_grpc"} +prost = "0.13.3" +tokio = {version = "1.38.0", features = ["io-util", "net", "rt-multi-thread"]} +axum = "0.7.5" +env_logger = "0.11.3" +log = "0.4.21" +thiserror = "1.0.61" +tonic = "0.12.3" diff --git a/spoticord_api/src/lib.rs b/spoticord_api/src/lib.rs new file mode 100644 index 0000000..fb25578 --- /dev/null +++ b/spoticord_api/src/lib.rs @@ -0,0 +1,39 @@ +use log::info; +use spoticord_api_grpc::spoticord_api::service::PlayPlaylistRequest; +use spoticord_session::manager::{SessionManager, SessionQuery}; +use tonic::{Request, Response, Status}; + +pub struct SpoticordApi { + pub session: SessionManager, +} + +#[tonic::async_trait] +impl spoticord_api_grpc::spoticord_api::service::spoticord_api_server::SpoticordApi + for SpoticordApi +{ + async fn play_playlist( + &self, + request: Request, + ) -> Result, Status> { + let playlist = request.into_inner(); + info!( + "Playing {} for {}", + playlist.playlist_uri, playlist.discord_user_id + ); + + let session = self + .session + .get_session(SessionQuery::Owner(playlist.discord_user_id.into())) + .unwrap(); + + session.queue_playlist(playlist.playlist_uri).await.unwrap(); + + let response = spoticord_api_grpc::spoticord_api::service::Response { + resp: Option::from( + spoticord_api_grpc::spoticord_api::service::response::Resp::Success(true), + ), + }; + + Ok(Response::new(response)) + } +} diff --git a/spoticord_api_grpc/Cargo.toml b/spoticord_api_grpc/Cargo.toml new file mode 100644 index 0000000..3fd5f45 --- /dev/null +++ b/spoticord_api_grpc/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "spoticord_api_grpc" +version = "0.1.0" +edition = "2021" + +[dependencies] +bytes = "1.6.0" +tonic = "0.12.3" +prost = "0.13.3" +tokio = {version = "1.38.0", features = ["io-util", "test-util"]} + +[build-dependencies] +tonic-build = "0.12.3" diff --git a/spoticord_api_grpc/build.rs b/spoticord_api_grpc/build.rs new file mode 100644 index 0000000..4d7e3c5 --- /dev/null +++ b/spoticord_api_grpc/build.rs @@ -0,0 +1,6 @@ +use std::io::Result; + +fn main() -> Result<()> { + tonic_build::configure().compile_protos(&["src/service.proto"], &["src/"])?; + Ok(()) +} diff --git a/spoticord_api_grpc/src/lib.rs b/spoticord_api_grpc/src/lib.rs new file mode 100644 index 0000000..7db2d6f --- /dev/null +++ b/spoticord_api_grpc/src/lib.rs @@ -0,0 +1,21 @@ +pub mod spoticord_api { + pub mod service { + include!(concat!(env!("OUT_DIR"), "/spoticord_api.service.rs")); + } +} + +#[cfg(test)] +mod test { + use crate::spoticord_api::service::{PlayModes, PlayPlaylistRequest}; + + #[test] + fn test_build_play_playlist() { + let play_playlist = PlayPlaylistRequest { + order: PlayModes::InOrder.into(), + playlist_uri: "test".to_string(), + }; + + assert_eq!(play_playlist.playlist_uri, "test"); + assert_eq!(play_playlist.order, PlayModes::InOrder.into()); + } +} diff --git a/spoticord_api_grpc/src/service.proto b/spoticord_api_grpc/src/service.proto new file mode 100644 index 0000000..a32776d --- /dev/null +++ b/spoticord_api_grpc/src/service.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package spoticord_api.service; + + +enum PlayModes { + SHUFFLE = 0; + IN_ORDER = 1; +} + +message PlayPlaylistRequest { + uint64 discord_user_id = 1; + PlayModes order = 2; + string playlist_uri = 3; +} + +enum Error { + SpotifyError = 0; + BotError = 1; + UserNotRegistered = 2; +} + +message Response { + oneof resp { + bool success = 1; + Error error = 2; + } +} + +service SpoticordApi { + rpc PlayPlaylist(PlayPlaylistRequest) returns (Response); +} \ No newline at end of file diff --git a/spoticord_database/src/schema.rs b/spoticord_database/src/schema.rs index 835fe59..2386905 100644 --- a/spoticord_database/src/schema.rs +++ b/spoticord_database/src/schema.rs @@ -35,8 +35,4 @@ diesel::table! { diesel::joinable!(account -> user (user_id)); diesel::joinable!(link_request -> user (user_id)); -diesel::allow_tables_to_appear_in_same_query!( - account, - link_request, - user, -); +diesel::allow_tables_to_appear_in_same_query!(account, link_request, user,); diff --git a/spoticord_player/src/lib.rs b/spoticord_player/src/lib.rs index ca23d93..f927f76 100644 --- a/spoticord_player/src/lib.rs +++ b/spoticord_player/src/lib.rs @@ -2,6 +2,10 @@ pub mod info; use anyhow::Result; use info::PlaybackInfo; +use librespot::connect::spirc::SpircLoadCommand; +use librespot::core::SpotifyId; +use librespot::metadata::{Metadata, Playlist}; +use librespot::protocol::spirc::TrackRef; use librespot::{ connect::{config::ConnectConfig, spirc::Spirc}, core::{http_client::HttpClientError, Session as SpotifySession, SessionConfig}, @@ -34,6 +38,7 @@ enum PlayerCommand { GetPlaybackInfo(oneshot::Sender>), GetLyrics(oneshot::Sender>), + QueuePlaylist { uri: String }, Shutdown, } @@ -213,6 +218,7 @@ impl Player { PlayerCommand::GetLyrics(tx) => self.get_lyrics(tx).await, PlayerCommand::Shutdown => self.commands.close(), + PlayerCommand::QueuePlaylist { uri } => self.queue_playlist(uri).await, }; } @@ -300,6 +306,34 @@ impl Player { _ = tx.send(Some(lyrics)); } + + async fn queue_playlist(&self, playlist_uri: String) { + let playlist = Playlist::get(&self.session, &SpotifyId::from_uri(&playlist_uri).unwrap()) + .await + .unwrap(); + + let tracks = playlist + .tracks() + .map(|track_id| { + let mut track = TrackRef::new(); + track.set_gid(Vec::from(track_id.to_raw())); + track + }) + .collect(); + + self.spirc.activate().unwrap(); + + self.spirc + .load(SpircLoadCommand { + context_uri: playlist_uri, + start_playing: true, + shuffle: true, + repeat: true, + playing_track_index: 0, + tracks, + }) + .unwrap(); + } } impl Drop for Player { @@ -354,4 +388,11 @@ impl PlayerHandle { pub async fn shutdown(&self) { _ = self.commands.send(PlayerCommand::Shutdown).await; } + + pub async fn queue_playlist(&self, playlist_uri: String) { + self.commands + .send(PlayerCommand::QueuePlaylist { uri: playlist_uri }) + .await + .unwrap() + } } diff --git a/spoticord_session/src/lib.rs b/spoticord_session/src/lib.rs index 4e57f56..010dea5 100644 --- a/spoticord_session/src/lib.rs +++ b/spoticord_session/src/lib.rs @@ -42,6 +42,9 @@ pub enum SessionCommand { ShutdownPlayer, Disconnect, DisconnectTimedOut, + QueuePlaylist { + playlist_uri: String, + }, } pub struct Session { @@ -236,7 +239,6 @@ impl Session { SessionCommand::GetOwner(sender) => _ = sender.send(self.owner), SessionCommand::GetPlayer(sender) => _ = sender.send(self.player.clone()), SessionCommand::GetActive(sender) => _ = sender.send(self.active), - SessionCommand::CreatePlaybackEmbed(handle, interaction, behavior) => { match PlaybackEmbed::create(self, handle, interaction, behavior).await { Ok(opt_handle) => { @@ -290,6 +292,9 @@ impl Session { return ControlFlow::Break(()); } + SessionCommand::QueuePlaylist { playlist_uri } => { + self.player.queue_playlist(playlist_uri).await; + } }; ControlFlow::Continue(()) @@ -551,6 +556,14 @@ impl SessionHandle { error!("Failed to send command: {why}"); } } + + pub async fn queue_playlist(&self, playlist_uri: String) -> Result<()> { + self.commands + .send(SessionCommand::QueuePlaylist { playlist_uri }) + .await?; + + Ok(()) + } } #[async_trait] diff --git a/src/bot.rs b/src/bot.rs index d587b4d..838edbb 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, Result}; use log::{debug, info}; use poise::{serenity_prelude, Framework, FrameworkContext, FrameworkOptions}; use serenity::all::{ActivityData, FullEvent, Ready, ShardManager}; +use spoticord_api::SpoticordApi; use spoticord_database::Database; use spoticord_session::manager::SessionManager; @@ -84,7 +85,7 @@ async fn event_handler( ctx: &serenity_prelude::Context, event: &FullEvent, _framework: FrameworkContext<'_, Data, anyhow::Error>, - _data: &Data, + data: &Data, ) -> Result<()> { if let FullEvent::Ready { data_about_bot } = event { if let Some(shard) = data_about_bot.shard { @@ -95,7 +96,19 @@ async fn event_handler( } ctx.set_activity(Some(ActivityData::listening(spoticord_config::MOTD))); - } + + let api_server = SpoticordApi { + session: data.clone(), + }; + + tokio::spawn(async move { + tonic::transport::Server::builder() + .add_service(spoticord_api_grpc::spoticord_api::service::spoticord_api_server::SpoticordApiServer::new(api_server)) + .serve("127.0.0.1:8080".parse().unwrap()) + .await + .unwrap(); + }); + }; Ok(()) }