diff --git a/Cargo.lock b/Cargo.lock index f28bf97..4c49dff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" version = "1.0.71" @@ -189,6 +238,48 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clap" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8f255e4b8027970e78db75e78831229c9815fdbfa67eb1a1b777a62e24b4a0" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd4f3c17c83b0ba34ffbc4f8bbd74f079413f747f84a6f89292f138057e36ab" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + [[package]] name = "cmake" version = "0.1.50" @@ -198,6 +289,23 @@ dependencies = [ "cc", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + [[package]] name = "command_attr" version = "0.4.1" @@ -318,11 +426,12 @@ name = "disconic" version = "0.2.1" dependencies = [ "anyhow", + "clap", "dotenv", - "env_logger", "log", "reqwest", "serenity", + "simple_logger", "songbird", "sunk", "symphonia", @@ -364,16 +473,24 @@ dependencies = [ ] [[package]] -name = "env_logger" -version = "0.9.3" +name = "errno" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", ] [[package]] @@ -573,6 +690,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -591,6 +714,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "http" version = "0.2.9" @@ -625,12 +754,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.26" @@ -720,12 +843,35 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnet" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.6" @@ -759,6 +905,12 @@ version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "lock_api" version = "0.4.10" @@ -925,6 +1077,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -1283,6 +1444,20 @@ dependencies = [ "version_check", ] +[[package]] +name = "rustix" +version = "0.37.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rustls" version = "0.20.8" @@ -1574,6 +1749,19 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_logger" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78beb34673091ccf96a8816fce8bfd30d1292c7621ca2bcb5f2ba0fae4f558d" +dependencies = [ + "atty", + "colored", + "log", + "time", + "windows-sys 0.42.0", +] + [[package]] name = "slab" version = "0.4.8" @@ -1690,6 +1878,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -1894,15 +2088,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.40" @@ -1940,6 +2125,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", + "libc", + "num_threads", "serde", "time-core", "time-macros", @@ -2329,6 +2516,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.3.3" @@ -2505,15 +2698,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index e121637..01f051a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ license = "AGPL-3.0-or-later" [dependencies.log] version = "0.4" -[dependencies.env_logger] -version = "0.9" +[dependencies.simple_logger] +version = "4.0" [dependencies.reqwest] version = "0.11" @@ -49,3 +49,7 @@ version = "1.0" [dependencies.dotenv] version = "0.15" + +[dependencies.clap] +version = "4.0" +features = [ "derive", "env" ] diff --git a/example.env b/example.env new file mode 100755 index 0000000..07847fa --- /dev/null +++ b/example.env @@ -0,0 +1,8 @@ +# You may configure the app with CLI args or env vars +# If using env vars, using dotenv is convenient; just copy this file to .env +# and edit it accordingly. +DISCONIC_SUBSONIC_URL="https://example.com" +DISCONIC_SUBSONIC_USER="admin" +DISCONIC_SUBSONIC_PASSWORD="1234" +DISCONIC_DISCORD_TOKEN="xxx" +DISCONIC_LOG_LEVEL="warn" diff --git a/module.nix b/module.nix index 915a3a5..ca42d40 100644 --- a/module.nix +++ b/module.nix @@ -30,22 +30,24 @@ in { type = types.path; description = "File path containing discord token"; }; + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Extra arguments to pass to disconic."; + }; }; config = mkIf cfg.enable { systemd.services.disconic = { description = "Disconic, a Discord Subsonic Bot"; wantedBy = [ "multi-user.target" ]; - serviceConfig = { - ExecStart = "${cfg.package}/bin/disconic"; - Restart = "on-failure"; - Environment = [ - "SUBSONIC_URL=${cfg.subsonicUrl}" - "SUBSONIC_USER=${cfg.subsonicUser}" - "SUBSONIC_PASSWORD_FILE=${cfg.subsonicPasswordFile}" - "DISCORD_TOKEN_FILE=${cfg.discordTokenFile}" - ]; - }; + serviceConfig.ExecStart = lib.escapeShellArgs ([ + (lib.getExe cfg.package) + "--subsonic-url=${cfg.subsonicUrl}" + "--subsonic-user=${cfg.subsonicUser}" + "--subsonic-password=$(cat ${cfg.subsonicPasswordFile})" + "--discord-token=$(cat ${cfg.discordTokenFile}" + ] ++ cfg.extraArgs); }; }; } diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 4cd820e..0000000 --- a/src/client.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::{Context as ErrContext, Result}; -use serenity::{ - client::Client as DiscordClient, - framework::standard::StandardFramework, - prelude::{GatewayIntents, TypeMapKey}, -}; -use songbird::SerenityInit; -use sunk::Client as SubsonicClient; - -use std::{env, fs, io}; - -use crate::discord::{after_hook, Handler, GENERAL_GROUP}; - -pub struct Client { - ss_url: String, - ss_user: String, - ss_password: String, - discord_token: String, -} - -impl Client { - pub async fn from_env() -> Result { - // Convert to std::io::Error, allowing usage of and_then - let convert_err = |e| io::Error::new(io::ErrorKind::Other, e); - - let ss_url = env::var("SUBSONIC_URL")?; - let ss_user = env::var("SUBSONIC_USER")?; - let ss_password = env::var("SUBSONIC_PASSWORD").or_else(|_| { - env::var("SUBSONIC_PASSWORD_FILE") - .map_err(convert_err) - .and_then(fs::read_to_string) - .map(|s| s.trim().to_owned()) - })?; - let discord_token = env::var("DISCORD_TOKEN").or_else(|_| { - env::var("DISCORD_TOKEN_FILE") - .map_err(convert_err) - .and_then(fs::read_to_string) - .map(|s| s.trim().to_owned()) - })?; - - Ok(Self { - ss_url, - ss_user, - ss_password, - discord_token, - }) - } - - pub async fn discord(&self, ss: SubsonicClient) -> Result { - let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; - let framework = StandardFramework::new() - .group(&GENERAL_GROUP) - .after(after_hook); - - framework.configure(|c| c.prefix("~")); - - let client = DiscordClient::builder(&self.discord_token, intents) - .event_handler(Handler) - .framework(framework) - .type_map_insert::(ss) - .register_songbird() - .await?; - - Ok(client) - } - - pub async fn subsonic(&self) -> Result { - let client = SubsonicClient::new(&self.ss_url, &self.ss_user, &self.ss_password)?; - log::info!("Created subsonic client: {client:?}"); - // Check that connection works - client - .ping() - .await - .with_context(|| "Could not connect to subsonic server.")?; - - Ok(client) - } -} - -pub struct MusicClient; -impl TypeMapKey for MusicClient { - type Value = SubsonicClient; -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..1110001 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,67 @@ +use crate::handles::SubsonicClientHandle; +use anyhow::{Context as ErrContext, Result}; +use clap::Parser; +use log::LevelFilter; +use serenity::{ + client::Client as DiscordClient, framework::standard::StandardFramework, + prelude::GatewayIntents, +}; +use simple_logger::SimpleLogger; +use songbird::SerenityInit; +use sunk::Client as SubsonicClient; + +use crate::discord::{after_hook, Handler, GENERAL_GROUP}; + +#[derive(Parser, Clone)] +pub struct Config { + #[clap(long, env = "DISCONIC_SUBSONIC_URL")] + subsonic_url: String, + #[clap(long, env = "DISCONIC_SUBSONIC_USER")] + subsonic_user: String, + #[clap(long, env = "DISCONIC_SUBSONIC_PASSWORD")] + subsonic_password: String, + #[clap(long, env = "DISCONIC_DISCORD_TOKEN")] + discord_token: String, + #[clap(long, env = "DISCONIC_LOG_LEVEL", default_value = "warn")] + log_level: LevelFilter, +} + +impl Config { + pub async fn discord(&self, ss: SubsonicClient) -> Result { + let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; + let framework = StandardFramework::new() + .group(&GENERAL_GROUP) + .after(after_hook); + + framework.configure(|c| c.prefix("~")); + + let client = DiscordClient::builder(&self.discord_token, intents) + .event_handler(Handler) + .framework(framework) + .type_map_insert::(ss) + .register_songbird() + .await?; + + Ok(client) + } + + pub async fn subsonic(&self) -> Result { + let client = SubsonicClient::new( + &self.subsonic_url, + &self.subsonic_user, + &self.subsonic_password, + )?; + log::info!("Created subsonic client: {client:?}"); + // Check that connection works + client + .ping() + .await + .with_context(|| "Could not connect to subsonic server.")?; + + Ok(client) + } + + pub fn logger(&self) -> SimpleLogger { + SimpleLogger::new().with_level(self.log_level) + } +} diff --git a/src/discord.rs b/src/discord.rs index 4540378..f0d2163 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -8,7 +8,6 @@ use serenity::{ Args, CommandResult, }, model::{channel::Message, gateway::Ready}, - prelude::TypeMapKey, utils::MessageBuilder, }; use songbird::{ @@ -25,7 +24,7 @@ use tokio::sync::Mutex; use std::sync::Arc; -use crate::MusicClient; +use crate::handles::{SubsonicClientHandle, SubsonicSongHandle}; #[group] #[commands( @@ -38,7 +37,7 @@ pub struct Handler; #[async_trait] impl EventHandler for Handler { async fn ready(&self, _: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); + log::info!("{} is connected!", ready.user.name); } } @@ -76,7 +75,7 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult { async fn song(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let data = ctx.data.read().await; let music_client = data - .get::() + .get::() .expect("Couldn't retrieve music client"); let search_size = SearchPage::new().with_size(1); @@ -114,7 +113,7 @@ async fn song(ctx: &Context, msg: &Message, args: Args) -> CommandResult { async fn album(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let data = ctx.data.read().await; let music_client = data - .get::() + .get::() .expect("Couldn't retrieve music client"); let search_size = SearchPage::new().with_size(1); @@ -153,7 +152,7 @@ async fn album(ctx: &Context, msg: &Message, args: Args) -> CommandResult { async fn random(ctx: &Context, msg: &Message) -> CommandResult { let data = ctx.data.read().await; let music_client = data - .get::() + .get::() .expect("Couldn't retrieve music client"); let result = Song::random(music_client, 1).await?; @@ -334,11 +333,6 @@ async fn remove(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { // ========================== // ========================== -struct SongHandler; -impl TypeMapKey for SongHandler { - type Value = Song; -} - async fn queue_song( ctx: &Context, msg: &Message, @@ -351,7 +345,7 @@ async fn queue_song( let track = load_song(song, client).await?; let track_handle = handler.enqueue(track).await; let mut type_map = track_handle.typemap().write().await; - type_map.insert::(song.clone()); + type_map.insert::(song.clone()); Ok(()) } @@ -394,7 +388,7 @@ async fn get_song(track: &TrackHandle) -> Result { .typemap() .read() .await - .get::() + .get::() .map(ToOwned::to_owned) .ok_or_else(|| anyhow!("Sound information not found"))?; Ok(song) diff --git a/src/handles.rs b/src/handles.rs new file mode 100644 index 0000000..f7ae464 --- /dev/null +++ b/src/handles.rs @@ -0,0 +1,12 @@ +use serenity::prelude::TypeMapKey; +use sunk::{song::Song as SubsonicSong, Client as SubsonicClient}; + +pub struct SubsonicClientHandle; +impl TypeMapKey for SubsonicClientHandle { + type Value = SubsonicClient; +} + +pub struct SubsonicSongHandle; +impl TypeMapKey for SubsonicSongHandle { + type Value = SubsonicSong; +} diff --git a/src/lib.rs b/src/lib.rs index 15e4176..871f51c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -pub mod client; -pub use client::{Client, MusicClient}; - +pub mod config; pub mod discord; +pub mod handles; diff --git a/src/main.rs b/src/main.rs index fadb2a9..f876f15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,18 @@ use anyhow::Result; +use clap::Parser; -use disconic::Client; +use disconic::config::Config; #[tokio::main] async fn main() -> Result<()> { dotenv::dotenv().ok(); - env_logger::init(); - let client = Client::from_env().await?; - let subsonic = client.subsonic().await?; - let mut discord = client.discord(subsonic).await?; + let config = Config::parse(); + let logger = config.logger(); + let subsonic = config.subsonic().await?; + let mut discord = config.discord(subsonic).await?; + logger.init()?; discord.start().await?; Ok(())