This extensive PR rewrites the internal mixing logic of the driver to use symphonia for parsing and decoding audio data, and rubato to resample audio. Existing logic to decode DCA and Opus formats/data have been reworked as plugins for symphonia. The main benefit is that we no longer need to keep yt-dlp and ffmpeg processes alive, saving a lot of memory and CPU: all decoding can be done in Rust! In exchange, we now need to do a lot of the HTTP handling and resumption ourselves, but this is still a huge net positive. `Input`s have been completely reworked such that all default (non-cached) sources are lazy by default, and are no longer covered by a special-case `Restartable`. These now span a gamut from a `Compose` (lazy), to a live source, to a fully `Parsed` source. As mixing is still sync, this includes adapters for `AsyncRead`/`AsyncSeek`, and HTTP streams. `Track`s have been reworked so that they only contain initialisation state for each track. `TrackHandles` are only created once a `Track`/`Input` has been handed over to the driver, replacing `create_player` and related functions. `TrackHandle::action` now acts on a `View` of (im)mutable state, and can request seeks/readying via `Action`. Per-track event handling has also been improved -- we can now determine and propagate the reason behind individual track errors due to the new backend. Some `TrackHandle` commands (seek etc.) benefit from this, and now use internal callbacks to signal completion. Due to associated PRs on felixmcfelix/songbird from avid testers, this includes general clippy tweaks, API additions, and other repo-wide cleanup. Thanks go out to the below co-authors. Co-authored-by: Gnome! <45660393+GnomedDev@users.noreply.github.com> Co-authored-by: Alakh <36898190+alakhpc@users.noreply.github.com>
418 lines
11 KiB
Rust
418 lines
11 KiB
Rust
//! Requires the "client", "standard_framework", and "voice" features be enabled in your
|
|
//! Cargo.toml, like so:
|
|
//!
|
|
//! ```toml
|
|
//! [dependencies.serenity]
|
|
//! git = "https://github.com/serenity-rs/serenity.git"
|
|
//! features = ["client", "standard_framework", "voice"]
|
|
//! ```
|
|
use std::env;
|
|
|
|
// This trait adds the `register_songbird` and `register_songbird_with` methods
|
|
// to the client builder below, making it easy to install this voice client.
|
|
// The voice client can be retrieved in any command using `songbird::get(ctx).await`.
|
|
use songbird::SerenityInit;
|
|
|
|
// Event related imports to detect track creation failures.
|
|
use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler, TrackEvent};
|
|
|
|
use std::time::Duration;
|
|
|
|
// Import the `Context` to handle commands.
|
|
use serenity::client::Context;
|
|
|
|
use serenity::{
|
|
async_trait,
|
|
client::{Client, EventHandler},
|
|
framework::{
|
|
standard::{
|
|
macros::{command, group},
|
|
Args, CommandResult,
|
|
},
|
|
StandardFramework,
|
|
},
|
|
model::{channel::Message, gateway::Ready},
|
|
prelude::GatewayIntents,
|
|
Result as SerenityResult,
|
|
};
|
|
|
|
struct Handler;
|
|
|
|
#[async_trait]
|
|
impl EventHandler for Handler {
|
|
async fn ready(&self, _: Context, ready: Ready) {
|
|
println!("{} is connected!", ready.user.name);
|
|
}
|
|
}
|
|
|
|
#[group]
|
|
#[commands(deafen, join, leave, mute, play, ping, undeafen, unmute)]
|
|
struct General;
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
tracing_subscriber::fmt::init();
|
|
|
|
// Configure the client with your Discord bot token in the environment.
|
|
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
|
|
|
|
let mut framework = StandardFramework::new()
|
|
.group(&GENERAL_GROUP);
|
|
|
|
framework.configure(|c| c.prefix("~"));
|
|
|
|
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
|
|
|
|
let mut client = Client::builder(&token, intents)
|
|
.event_handler(Handler)
|
|
.framework(framework)
|
|
.register_songbird()
|
|
.await
|
|
.expect("Err creating client");
|
|
|
|
tokio::spawn(async move {
|
|
let _ = client
|
|
.start()
|
|
.await
|
|
.map_err(|why| println!("Client ended: {:?}", why));
|
|
});
|
|
|
|
tokio::signal::ctrl_c().await;
|
|
println!("Received Ctrl-C, shutting down.");
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).unwrap();
|
|
let guild_id = guild.id;
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let handler_lock = match manager.get(guild_id) {
|
|
Some(handler) => handler,
|
|
None => {
|
|
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
if handler.is_deaf() {
|
|
check_msg(msg.channel_id.say(&ctx.http, "Already deafened").await);
|
|
} else {
|
|
if let Err(e) = handler.deafen(true).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Deafened").await);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).unwrap();
|
|
let guild_id = guild.id;
|
|
|
|
let channel_id = guild
|
|
.voice_states
|
|
.get(&msg.author.id)
|
|
.and_then(|voice_state| voice_state.channel_id);
|
|
|
|
let connect_to = match channel_id {
|
|
Some(channel) => channel,
|
|
None => {
|
|
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let (handler_lock, _success) = manager.join(guild_id, connect_to).await;
|
|
|
|
// Attach an event handler to see notifications of all track errors.
|
|
let mut handler = handler_lock.lock().await;
|
|
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
struct TrackErrorNotifier;
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for TrackErrorNotifier {
|
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
|
if let EventContext::Track(track_list) = ctx {
|
|
for (state, handle) in *track_list {
|
|
println!(
|
|
"Track {:?} encountered an error: {:?}",
|
|
handle.uuid(),
|
|
state.playing
|
|
);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).unwrap();
|
|
let guild_id = guild.id;
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
let has_handler = manager.get(guild_id).is_some();
|
|
|
|
if has_handler {
|
|
if let Err(e) = manager.remove(guild_id).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
|
|
} else {
|
|
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).unwrap();
|
|
let guild_id = guild.id;
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let handler_lock = match manager.get(guild_id) {
|
|
Some(handler) => handler,
|
|
None => {
|
|
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
if handler.is_mute() {
|
|
check_msg(msg.channel_id.say(&ctx.http, "Already muted").await);
|
|
} else {
|
|
if let Err(e) = handler.mute(true).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Now muted").await);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
|
|
check_msg(msg.channel_id.say(&ctx.http, "Pong!").await);
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
let url = match args.single::<String>() {
|
|
Ok(url) => url,
|
|
Err(_) => {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Must provide a URL to a video or audio")
|
|
.await,
|
|
);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
let time = match args.single::<f64>() {
|
|
Ok(t) => t,
|
|
Err(_) => {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Must provide a valid f64 timestamp to jump to.")
|
|
.await,
|
|
);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
if !url.starts_with("http") {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Must provide a valid URL")
|
|
.await,
|
|
);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
let guild = msg.guild(&ctx.cache).unwrap();
|
|
let guild_id = guild.id;
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
let jar = reqwest::cookie::Jar::default();
|
|
jar.add_cookie_str("CONSENT=YES+; Path=/; Domain=youtube.com; Secure; Expires=Fri, 01 Jan 2038 00:00:00 GMT;", &"https://youtube.com".parse().unwrap());
|
|
|
|
let src = songbird::input::YoutubeDl::new_ytdl_like("yt-dlp", reqwest::Client::builder().cookie_provider(std::sync::Arc::new(jar)).build().unwrap(), url);
|
|
|
|
let h = handler.play_input(src.into());
|
|
|
|
h.add_event(
|
|
Event::Delayed(Duration::from_secs(10)),
|
|
SkipHandler {
|
|
skip_to: Duration::from_secs_f64(time),
|
|
},
|
|
);
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Playing song").await);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Not in a voice channel to play in")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
struct SkipHandler {
|
|
skip_to: Duration,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for SkipHandler {
|
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
|
if let EventContext::Track(&[(state, handle)]) = ctx {
|
|
println!("Current position is {:?}", state.position);
|
|
println!("Seeking to {:?}", self.skip_to);
|
|
let resp = handle.seek_time(self.skip_to);
|
|
println!("Seek response {:?}", resp);
|
|
return None;
|
|
}
|
|
|
|
println!("This wasn't supposed to happen.");
|
|
None
|
|
}
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).unwrap();
|
|
let guild_id = guild.id;
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
if let Err(e) = handler.deafen(false).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Undeafened").await);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Not in a voice channel to undeafen in")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn unmute(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).unwrap();
|
|
let guild_id = guild.id;
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
if let Err(e) = handler.mute(false).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Unmuted").await);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Not in a voice channel to unmute in")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Checks that a message successfully sent; if not, then logs why to stdout.
|
|
fn check_msg(result: SerenityResult<Message>) {
|
|
if let Err(why) = result {
|
|
println!("Error sending message: {:?}", why);
|
|
}
|
|
}
|