This change is made with queue users in mind. Since sources of this kind *know* how to (re)create themselves, they can avoid being created at all until needed. This also adds machinery to preload tracks *before* they are needed, for gapless playback on queues and so on. Queues make use of the event system to do this.
652 lines
17 KiB
Rust
652 lines
17 KiB
Rust
//! Example demonstrating how to make use of individual track audio events,
|
|
//! and how to use the `TrackQueue` system.
|
|
//!
|
|
//! Requires the "cache", "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 = ["cache", "framework", "standard_framework", "voice"]
|
|
//! ```
|
|
use std::{
|
|
env,
|
|
sync::{
|
|
atomic::{AtomicUsize, Ordering},
|
|
Arc,
|
|
},
|
|
time::Duration,
|
|
};
|
|
|
|
use serenity::{
|
|
async_trait,
|
|
client::{Client, Context, EventHandler},
|
|
framework::{
|
|
standard::{
|
|
macros::{command, group},
|
|
Args,
|
|
CommandResult,
|
|
},
|
|
StandardFramework,
|
|
},
|
|
http::Http,
|
|
model::{channel::Message, gateway::Ready, misc::Mentionable, prelude::ChannelId},
|
|
Result as SerenityResult,
|
|
};
|
|
|
|
use songbird::{
|
|
input::{
|
|
self,
|
|
restartable::Restartable,
|
|
},
|
|
Event,
|
|
EventContext,
|
|
EventHandler as VoiceEventHandler,
|
|
SerenityInit,
|
|
TrackEvent,
|
|
};
|
|
|
|
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_fade, queue, skip, stop, 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 framework = StandardFramework::new()
|
|
.configure(|c| c.prefix("~"))
|
|
.group(&GENERAL_GROUP);
|
|
|
|
let mut client = Client::builder(&token)
|
|
.event_handler(Handler)
|
|
.framework(framework)
|
|
.register_songbird()
|
|
.await
|
|
.expect("Err creating client");
|
|
|
|
let _ = client
|
|
.start()
|
|
.await
|
|
.map_err(|why| println!("Client ended: {:?}", why));
|
|
}
|
|
|
|
#[command]
|
|
async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).await.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).await.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 (handle_lock, success) = manager.join(guild_id, connect_to).await;
|
|
|
|
if let Ok(_channel) = success {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, &format!("Joined {}", connect_to.mention()))
|
|
.await,
|
|
);
|
|
|
|
let chan_id = msg.channel_id;
|
|
|
|
let send_http = ctx.http.clone();
|
|
|
|
let mut handle = handle_lock.lock().await;
|
|
|
|
handle.add_global_event(
|
|
Event::Track(TrackEvent::End),
|
|
TrackEndNotifier {
|
|
chan_id,
|
|
http: send_http,
|
|
},
|
|
);
|
|
|
|
let send_http = ctx.http.clone();
|
|
|
|
handle.add_global_event(
|
|
Event::Periodic(Duration::from_secs(60), None),
|
|
ChannelDurationNotifier {
|
|
chan_id,
|
|
count: Default::default(),
|
|
http: send_http,
|
|
},
|
|
);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Error joining the channel")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
struct TrackEndNotifier {
|
|
chan_id: ChannelId,
|
|
http: Arc<Http>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for TrackEndNotifier {
|
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
|
if let EventContext::Track(track_list) = ctx {
|
|
check_msg(
|
|
self.chan_id
|
|
.say(&self.http, &format!("Tracks ended: {}.", track_list.len()))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
struct ChannelDurationNotifier {
|
|
chan_id: ChannelId,
|
|
count: Arc<AtomicUsize>,
|
|
http: Arc<Http>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for ChannelDurationNotifier {
|
|
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
|
|
let count_before = self.count.fetch_add(1, Ordering::Relaxed);
|
|
check_msg(
|
|
self.chan_id
|
|
.say(
|
|
&self.http,
|
|
&format!(
|
|
"I've been in this channel for {} minutes!",
|
|
count_before + 1
|
|
),
|
|
)
|
|
.await,
|
|
);
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).await.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).await.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_fade(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(());
|
|
},
|
|
};
|
|
|
|
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).await.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 source = match input::ytdl(&url).await {
|
|
Ok(source) => source,
|
|
Err(why) => {
|
|
println!("Err starting source: {:?}", why);
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
// This handler object will allow you to, as needed,
|
|
// control the audio track via events and further commands.
|
|
let song = handler.play_source(source);
|
|
let send_http = ctx.http.clone();
|
|
let chan_id = msg.channel_id;
|
|
|
|
// This shows how to periodically fire an event, in this case to
|
|
// periodically make a track quieter until it can be no longer heard.
|
|
let _ = song.add_event(
|
|
Event::Periodic(Duration::from_secs(5), Some(Duration::from_secs(7))),
|
|
SongFader {
|
|
chan_id,
|
|
http: send_http,
|
|
},
|
|
);
|
|
|
|
let send_http = ctx.http.clone();
|
|
|
|
// This shows how to fire an event once an audio track completes,
|
|
// either due to hitting the end of the bytestream or stopped by user code.
|
|
let _ = song.add_event(
|
|
Event::Track(TrackEvent::End),
|
|
SongEndNotifier {
|
|
chan_id,
|
|
http: send_http,
|
|
},
|
|
);
|
|
|
|
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 SongFader {
|
|
chan_id: ChannelId,
|
|
http: Arc<Http>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for SongFader {
|
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
|
if let EventContext::Track(&[(state, track)]) = ctx {
|
|
let _ = track.set_volume(state.volume / 2.0);
|
|
|
|
if state.volume < 1e-2 {
|
|
let _ = track.stop();
|
|
check_msg(self.chan_id.say(&self.http, "Stopping song...").await);
|
|
Some(Event::Cancel)
|
|
} else {
|
|
check_msg(self.chan_id.say(&self.http, "Volume reduced.").await);
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SongEndNotifier {
|
|
chan_id: ChannelId,
|
|
http: Arc<Http>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for SongEndNotifier {
|
|
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
|
|
check_msg(
|
|
self.chan_id
|
|
.say(&self.http, "Song faded out completely!")
|
|
.await,
|
|
);
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn queue(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(());
|
|
},
|
|
};
|
|
|
|
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).await.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;
|
|
|
|
// Here, we use lazy restartable sources to make sure that we don't pay
|
|
// for decoding, playback on tracks which aren't actually live yet.
|
|
let source = match Restartable::ytdl(url, true).await {
|
|
Ok(source) => source,
|
|
Err(why) => {
|
|
println!("Err starting source: {:?}", why);
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
handler.enqueue_source(source.into());
|
|
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(
|
|
&ctx.http,
|
|
format!("Added song to queue: position {}", handler.queue().len()),
|
|
)
|
|
.await,
|
|
);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Not in a voice channel to play in")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).await.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 handler = handler_lock.lock().await;
|
|
let queue = handler.queue();
|
|
let _ = queue.skip();
|
|
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(
|
|
&ctx.http,
|
|
format!("Song skipped: {} in queue.", queue.len()),
|
|
)
|
|
.await,
|
|
);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Not in a voice channel to play in")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn stop(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).await.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 handler = handler_lock.lock().await;
|
|
let queue = handler.queue();
|
|
let _ = queue.stop();
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Queue cleared.").await);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Not in a voice channel to play in")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild = msg.guild(&ctx.cache).await.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).await.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);
|
|
}
|
|
}
|