TrackQueues: Convenience methods and extension (#7)
* Adds a uuid field to tracks and handles to make it easier to identify and match event sources after the fact. * Adds optional feature "builtin-queue" to expose a queue on every driver, as a convenience for users who can guarantee they'll need a queue for every driver/call. * Adds methods to queues to allow access to the currently running track handle, remove a specified queue entry, as well as to mutate the underlying queue from a closure.
This commit is contained in:
10
Cargo.toml
10
Cargo.toml
@@ -94,6 +94,11 @@ default-features = false
|
|||||||
optional = true
|
optional = true
|
||||||
version = "2"
|
version = "2"
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
optional = true
|
||||||
|
version = "0.8"
|
||||||
|
features = ["v4"]
|
||||||
|
|
||||||
[dependencies.xsalsa20poly1305]
|
[dependencies.xsalsa20poly1305]
|
||||||
optional = true
|
optional = true
|
||||||
version = "0.6"
|
version = "0.6"
|
||||||
@@ -135,9 +140,9 @@ driver = [
|
|||||||
"tokio/sync",
|
"tokio/sync",
|
||||||
"tokio/time",
|
"tokio/time",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
"xsalsa20poly1305",
|
"xsalsa20poly1305",
|
||||||
]
|
]
|
||||||
youtube-dlc = []
|
|
||||||
rustls = ["async-tungstenite/tokio-rustls"]
|
rustls = ["async-tungstenite/tokio-rustls"]
|
||||||
native = ["async-tungstenite/tokio-native-tls"]
|
native = ["async-tungstenite/tokio-native-tls"]
|
||||||
serenity-rustls = ["serenity/rustls_backend", "rustls", "gateway", "serenity-deps"]
|
serenity-rustls = ["serenity/rustls_backend", "rustls", "gateway", "serenity-deps"]
|
||||||
@@ -149,6 +154,9 @@ simd-zlib = ["twilight-gateway/simd-zlib"]
|
|||||||
stock-zlib = ["twilight-gateway/stock-zlib"]
|
stock-zlib = ["twilight-gateway/stock-zlib"]
|
||||||
serenity-deps = ["async-trait"]
|
serenity-deps = ["async-trait"]
|
||||||
|
|
||||||
|
youtube-dlc = []
|
||||||
|
builtin-queue = []
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "mixing"
|
name = "mixing"
|
||||||
path = "benches/mixing.rs"
|
path = "benches/mixing.rs"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ tracing-subscriber = "0.2"
|
|||||||
tracing-futures = "0.2"
|
tracing-futures = "0.2"
|
||||||
|
|
||||||
[dependencies.songbird]
|
[dependencies.songbird]
|
||||||
|
features = ["builtin-queue"]
|
||||||
path = "../../../"
|
path = "../../../"
|
||||||
|
|
||||||
[dependencies.serenity]
|
[dependencies.serenity]
|
||||||
|
|||||||
@@ -10,35 +10,32 @@
|
|||||||
//! features = ["cache", "framework", "standard_framework", "voice"]
|
//! features = ["cache", "framework", "standard_framework", "voice"]
|
||||||
//! ```
|
//! ```
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
env,
|
env,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
sync::{atomic::{AtomicUsize, Ordering}, Arc}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
async_trait,
|
async_trait,
|
||||||
client::{Client, Context, EventHandler},
|
client::{Client, Context, EventHandler},
|
||||||
http::Http,
|
|
||||||
framework::{
|
framework::{
|
||||||
StandardFramework,
|
|
||||||
standard::{
|
standard::{
|
||||||
Args, CommandResult,
|
|
||||||
macros::{command, group},
|
macros::{command, group},
|
||||||
|
Args,
|
||||||
|
CommandResult,
|
||||||
},
|
},
|
||||||
|
StandardFramework,
|
||||||
},
|
},
|
||||||
model::{
|
http::Http,
|
||||||
channel::Message,
|
model::{channel::Message, gateway::Ready, misc::Mentionable, prelude::ChannelId},
|
||||||
gateway::Ready,
|
|
||||||
misc::Mentionable,
|
|
||||||
prelude::{ChannelId, GuildId},
|
|
||||||
},
|
|
||||||
Result as SerenityResult,
|
Result as SerenityResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use songbird::{
|
use songbird::{
|
||||||
input,
|
input,
|
||||||
tracks::TrackQueue,
|
|
||||||
Event,
|
Event,
|
||||||
EventContext,
|
EventContext,
|
||||||
EventHandler as VoiceEventHandler,
|
EventHandler as VoiceEventHandler,
|
||||||
@@ -46,15 +43,6 @@ use songbird::{
|
|||||||
TrackEvent,
|
TrackEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
// This imports `typemap`'s `Key` as `TypeMapKey`.
|
|
||||||
use serenity::prelude::*;
|
|
||||||
|
|
||||||
struct VoiceQueueManager;
|
|
||||||
|
|
||||||
impl TypeMapKey for VoiceQueueManager {
|
|
||||||
type Value = Arc<Mutex<HashMap<GuildId, TrackQueue>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Handler;
|
struct Handler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -65,7 +53,9 @@ impl EventHandler for Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[group]
|
#[group]
|
||||||
#[commands(deafen, join, leave, mute, play_fade, queue, skip, stop, ping, undeafen, unmute)]
|
#[commands(
|
||||||
|
deafen, join, leave, mute, play_fade, queue, skip, stop, ping, undeafen, unmute
|
||||||
|
)]
|
||||||
struct General;
|
struct General;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -73,12 +63,10 @@ async fn main() {
|
|||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
// Configure the client with your Discord bot token in the environment.
|
// Configure the client with your Discord bot token in the environment.
|
||||||
let token = env::var("DISCORD_TOKEN")
|
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
|
||||||
.expect("Expected a token in the environment");
|
|
||||||
|
|
||||||
let framework = StandardFramework::new()
|
let framework = StandardFramework::new()
|
||||||
.configure(|c| c
|
.configure(|c| c.prefix("~"))
|
||||||
.prefix("~"))
|
|
||||||
.group(&GENERAL_GROUP);
|
.group(&GENERAL_GROUP);
|
||||||
|
|
||||||
let mut client = Client::builder(&token)
|
let mut client = Client::builder(&token)
|
||||||
@@ -88,15 +76,10 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Err creating client");
|
.expect("Err creating client");
|
||||||
|
|
||||||
// Obtain a lock to the data owned by the client, and insert the client's
|
let _ = client
|
||||||
// voice manager into it. This allows the voice manager to be accessible by
|
.start()
|
||||||
// event handlers and framework commands.
|
.await
|
||||||
{
|
.map_err(|why| println!("Client ended: {:?}", why));
|
||||||
let mut data = client.data.write().await;
|
|
||||||
data.insert::<VoiceQueueManager>(Arc::new(Mutex::new(HashMap::new())));
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = client.start().await.map_err(|why| println!("Client ended: {:?}", why));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -104,8 +87,10 @@ async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let manager = songbird::get(ctx).await
|
let manager = songbird::get(ctx)
|
||||||
.expect("Songbird Voice client placed in at initialisation.").clone();
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
let handler_lock = match manager.get(guild_id) {
|
let handler_lock = match manager.get(guild_id) {
|
||||||
Some(handler) => handler,
|
Some(handler) => handler,
|
||||||
@@ -122,7 +107,11 @@ async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|||||||
check_msg(msg.channel_id.say(&ctx.http, "Already deafened").await);
|
check_msg(msg.channel_id.say(&ctx.http, "Already deafened").await);
|
||||||
} else {
|
} else {
|
||||||
if let Err(e) = handler.deafen(true).await {
|
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, format!("Failed: {:?}", e))
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Deafened").await);
|
check_msg(msg.channel_id.say(&ctx.http, "Deafened").await);
|
||||||
@@ -138,26 +127,32 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
|
|||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let channel_id = guild
|
let channel_id = guild
|
||||||
.voice_states.get(&msg.author.id)
|
.voice_states
|
||||||
|
.get(&msg.author.id)
|
||||||
.and_then(|voice_state| voice_state.channel_id);
|
.and_then(|voice_state| voice_state.channel_id);
|
||||||
|
|
||||||
|
|
||||||
let connect_to = match channel_id {
|
let connect_to = match channel_id {
|
||||||
Some(channel) => channel,
|
Some(channel) => channel,
|
||||||
None => {
|
None => {
|
||||||
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let manager = songbird::get(ctx).await
|
let manager = songbird::get(ctx)
|
||||||
.expect("Songbird Voice client placed in at initialisation.").clone();
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
let (handle_lock, success) = manager.join(guild_id, connect_to).await;
|
let (handle_lock, success) = manager.join(guild_id, connect_to).await;
|
||||||
|
|
||||||
if let Ok(_channel) = success {
|
if let Ok(_channel) = success {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, &format!("Joined {}", connect_to.mention())).await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, &format!("Joined {}", connect_to.mention()))
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
let chan_id = msg.channel_id;
|
let chan_id = msg.channel_id;
|
||||||
|
|
||||||
@@ -167,17 +162,28 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
|
|||||||
|
|
||||||
handle.add_global_event(
|
handle.add_global_event(
|
||||||
Event::Track(TrackEvent::End),
|
Event::Track(TrackEvent::End),
|
||||||
TrackEndNotifier { chan_id, http: send_http },
|
TrackEndNotifier {
|
||||||
|
chan_id,
|
||||||
|
http: send_http,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let send_http = ctx.http.clone();
|
let send_http = ctx.http.clone();
|
||||||
|
|
||||||
handle.add_global_event(
|
handle.add_global_event(
|
||||||
Event::Periodic(Duration::from_secs(60), None),
|
Event::Periodic(Duration::from_secs(60), None),
|
||||||
ChannelDurationNotifier { chan_id, count: Default::default(), http: send_http },
|
ChannelDurationNotifier {
|
||||||
|
chan_id,
|
||||||
|
count: Default::default(),
|
||||||
|
http: send_http,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Error joining the channel").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Error joining the channel")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -192,7 +198,11 @@ struct TrackEndNotifier {
|
|||||||
impl VoiceEventHandler for TrackEndNotifier {
|
impl VoiceEventHandler for TrackEndNotifier {
|
||||||
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
||||||
if let EventContext::Track(track_list) = ctx {
|
if let EventContext::Track(track_list) = ctx {
|
||||||
check_msg(self.chan_id.say(&self.http, &format!("Tracks ended: {}.", track_list.len())).await);
|
check_msg(
|
||||||
|
self.chan_id
|
||||||
|
.say(&self.http, &format!("Tracks ended: {}.", track_list.len()))
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
@@ -209,7 +219,17 @@ struct ChannelDurationNotifier {
|
|||||||
impl VoiceEventHandler for ChannelDurationNotifier {
|
impl VoiceEventHandler for ChannelDurationNotifier {
|
||||||
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
|
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
|
||||||
let count_before = self.count.fetch_add(1, Ordering::Relaxed);
|
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);
|
check_msg(
|
||||||
|
self.chan_id
|
||||||
|
.say(
|
||||||
|
&self.http,
|
||||||
|
&format!(
|
||||||
|
"I've been in this channel for {} minutes!",
|
||||||
|
count_before + 1
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -221,13 +241,19 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
|
|||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let manager = songbird::get(ctx).await
|
let manager = songbird::get(ctx)
|
||||||
.expect("Songbird Voice client placed in at initialisation.").clone();
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
let has_handler = manager.get(guild_id).is_some();
|
let has_handler = manager.get(guild_id).is_some();
|
||||||
|
|
||||||
if has_handler {
|
if has_handler {
|
||||||
if let Err(e) = manager.remove(guild_id).await {
|
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, format!("Failed: {:?}", e))
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
|
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
|
||||||
@@ -244,8 +270,10 @@ async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
|
|||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let manager = songbird::get(ctx).await
|
let manager = songbird::get(ctx)
|
||||||
.expect("Songbird Voice client placed in at initialisation.").clone();
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
let handler_lock = match manager.get(guild_id) {
|
let handler_lock = match manager.get(guild_id) {
|
||||||
Some(handler) => handler,
|
Some(handler) => handler,
|
||||||
@@ -262,7 +290,11 @@ async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
|
|||||||
check_msg(msg.channel_id.say(&ctx.http, "Already muted").await);
|
check_msg(msg.channel_id.say(&ctx.http, "Already muted").await);
|
||||||
} else {
|
} else {
|
||||||
if let Err(e) = handler.mute(true).await {
|
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, format!("Failed: {:?}", e))
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Now muted").await);
|
check_msg(msg.channel_id.say(&ctx.http, "Now muted").await);
|
||||||
@@ -284,14 +316,22 @@ async fn play_fade(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||||||
let url = match args.single::<String>() {
|
let url = match args.single::<String>() {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Must provide a URL to a video or audio").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Must provide a URL to a video or audio")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if !url.starts_with("http") {
|
if !url.starts_with("http") {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Must provide a valid URL").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Must provide a valid URL")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -299,8 +339,10 @@ async fn play_fade(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let manager = songbird::get(ctx).await
|
let manager = songbird::get(ctx)
|
||||||
.expect("Songbird Voice client placed in at initialisation.").clone();
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
if let Some(handler_lock) = manager.get(guild_id) {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
let mut handler = handler_lock.lock().await;
|
let mut handler = handler_lock.lock().await;
|
||||||
@@ -318,7 +360,7 @@ async fn play_fade(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||||||
|
|
||||||
// This handler object will allow you to, as needed,
|
// This handler object will allow you to, as needed,
|
||||||
// control the audio track via events and further commands.
|
// control the audio track via events and further commands.
|
||||||
let song = handler.play_source(source.into());
|
let song = handler.play_source(source);
|
||||||
let send_http = ctx.http.clone();
|
let send_http = ctx.http.clone();
|
||||||
let chan_id = msg.channel_id;
|
let chan_id = msg.channel_id;
|
||||||
|
|
||||||
@@ -326,7 +368,10 @@ async fn play_fade(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||||||
// periodically make a track quieter until it can be no longer heard.
|
// periodically make a track quieter until it can be no longer heard.
|
||||||
let _ = song.add_event(
|
let _ = song.add_event(
|
||||||
Event::Periodic(Duration::from_secs(5), Some(Duration::from_secs(7))),
|
Event::Periodic(Duration::from_secs(5), Some(Duration::from_secs(7))),
|
||||||
SongFader { chan_id, http: send_http },
|
SongFader {
|
||||||
|
chan_id,
|
||||||
|
http: send_http,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let send_http = ctx.http.clone();
|
let send_http = ctx.http.clone();
|
||||||
@@ -335,12 +380,19 @@ async fn play_fade(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||||||
// either due to hitting the end of the bytestream or stopped by user code.
|
// either due to hitting the end of the bytestream or stopped by user code.
|
||||||
let _ = song.add_event(
|
let _ = song.add_event(
|
||||||
Event::Track(TrackEvent::End),
|
Event::Track(TrackEvent::End),
|
||||||
SongEndNotifier { chan_id, http: send_http },
|
SongEndNotifier {
|
||||||
|
chan_id,
|
||||||
|
http: send_http,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Playing song").await);
|
check_msg(msg.channel_id.say(&ctx.http, "Playing song").await);
|
||||||
} else {
|
} else {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to play in").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Not in a voice channel to play in")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -379,7 +431,11 @@ struct SongEndNotifier {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl VoiceEventHandler for SongEndNotifier {
|
impl VoiceEventHandler for SongEndNotifier {
|
||||||
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
|
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
|
||||||
check_msg(self.chan_id.say(&self.http, "Song faded out completely!").await);
|
check_msg(
|
||||||
|
self.chan_id
|
||||||
|
.say(&self.http, "Song faded out completely!")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -391,14 +447,22 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||||||
let url = match args.single::<String>() {
|
let url = match args.single::<String>() {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Must provide a URL to a video or audio").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Must provide a URL to a video or audio")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if !url.starts_with("http") {
|
if !url.starts_with("http") {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Must provide a valid URL").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Must provide a valid URL")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -406,10 +470,10 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let manager = songbird::get(ctx).await
|
let manager = songbird::get(ctx)
|
||||||
.expect("Songbird Voice client placed in at initialisation.").clone();
|
.await
|
||||||
let queues_lock = ctx.data.read().await.get::<VoiceQueueManager>().cloned().expect("Expected VoiceQueueManager in ShareMap.");
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
let mut track_queues = queues_lock.lock().await;
|
.clone();
|
||||||
|
|
||||||
if let Some(handler_lock) = manager.get(guild_id) {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
let mut handler = handler_lock.lock().await;
|
let mut handler = handler_lock.lock().await;
|
||||||
@@ -424,16 +488,22 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// We need to ensure that this guild has a TrackQueue created for it.
|
handler.enqueue_source(source);
|
||||||
let queue = track_queues.entry(guild_id)
|
|
||||||
.or_default();
|
|
||||||
|
|
||||||
// Queueing a track is this easy!
|
check_msg(
|
||||||
queue.add_source(source, &mut handler);
|
msg.channel_id
|
||||||
|
.say(
|
||||||
check_msg(msg.channel_id.say(&ctx.http, format!("Added song to queue: position {}", queue.len())).await);
|
&ctx.http,
|
||||||
|
format!("Added song to queue: position {}", handler.queue().len()),
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to play in").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Not in a voice channel to play in")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -445,15 +515,30 @@ async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let queues_lock = ctx.data.read().await.get::<VoiceQueueManager>().cloned().expect("Expected VoiceQueueManager in ShareMap.");
|
let manager = songbird::get(ctx)
|
||||||
let mut track_queues = queues_lock.lock().await;
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
if let Some(queue) = track_queues.get_mut(&guild_id) {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
let queue = handler.queue();
|
||||||
let _ = queue.skip();
|
let _ = queue.skip();
|
||||||
|
|
||||||
check_msg(msg.channel_id.say(&ctx.http, format!("Song skipped: {} in queue.", queue.len())).await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(
|
||||||
|
&ctx.http,
|
||||||
|
format!("Song skipped: {} in queue.", queue.len()),
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to play in").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Not in a voice channel to play in")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -465,15 +550,23 @@ async fn stop(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let queues_lock = ctx.data.read().await.get::<VoiceQueueManager>().cloned().expect("Expected VoiceQueueManager in ShareMap.");
|
let manager = songbird::get(ctx)
|
||||||
let mut track_queues = queues_lock.lock().await;
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
if let Some(queue) = track_queues.get_mut(&guild_id) {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
let queue = handler.queue();
|
||||||
let _ = queue.stop();
|
let _ = queue.stop();
|
||||||
|
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Queue cleared.").await);
|
check_msg(msg.channel_id.say(&ctx.http, "Queue cleared.").await);
|
||||||
} else {
|
} else {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to play in").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Not in a voice channel to play in")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -485,18 +578,28 @@ async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let manager = songbird::get(ctx).await
|
let manager = songbird::get(ctx)
|
||||||
.expect("Songbird Voice client placed in at initialisation.").clone();
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
if let Some(handler_lock) = manager.get(guild_id) {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
let mut handler = handler_lock.lock().await;
|
let mut handler = handler_lock.lock().await;
|
||||||
if let Err(e) = handler.deafen(false).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, format!("Failed: {:?}", e))
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Undeafened").await);
|
check_msg(msg.channel_id.say(&ctx.http, "Undeafened").await);
|
||||||
} else {
|
} else {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to undeafen in").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Not in a voice channel to undeafen in")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -507,18 +610,28 @@ async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|||||||
async fn unmute(ctx: &Context, msg: &Message) -> CommandResult {
|
async fn unmute(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
let manager = songbird::get(ctx).await
|
let manager = songbird::get(ctx)
|
||||||
.expect("Songbird Voice client placed in at initialisation.").clone();
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
if let Some(handler_lock) = manager.get(guild_id) {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
let mut handler = handler_lock.lock().await;
|
let mut handler = handler_lock.lock().await;
|
||||||
if let Err(e) = handler.mute(false).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, format!("Failed: {:?}", e))
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Unmuted").await);
|
check_msg(msg.channel_id.say(&ctx.http, "Unmuted").await);
|
||||||
} else {
|
} else {
|
||||||
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to unmute in").await);
|
check_msg(
|
||||||
|
msg.channel_id
|
||||||
|
.say(&ctx.http, "Not in a voice channel to unmute in")
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ use connection::error::Result;
|
|||||||
pub use crypto::*;
|
pub use crypto::*;
|
||||||
pub use decode_mode::DecodeMode;
|
pub use decode_mode::DecodeMode;
|
||||||
|
|
||||||
|
#[cfg(feature = "builtin-queue")]
|
||||||
|
use crate::tracks::TrackQueue;
|
||||||
use crate::{
|
use crate::{
|
||||||
events::EventData,
|
events::EventData,
|
||||||
input::Input,
|
input::Input,
|
||||||
tracks::{Track, TrackHandle},
|
tracks::{self, Track, TrackHandle},
|
||||||
ConnectionInfo,
|
ConnectionInfo,
|
||||||
Event,
|
Event,
|
||||||
EventHandler,
|
EventHandler,
|
||||||
@@ -34,11 +36,16 @@ use tracing::instrument;
|
|||||||
|
|
||||||
/// The control object for a Discord voice connection, handling connection,
|
/// The control object for a Discord voice connection, handling connection,
|
||||||
/// mixing, encoding, en/decryption, and event generation.
|
/// mixing, encoding, en/decryption, and event generation.
|
||||||
|
///
|
||||||
|
/// When compiled with the `"builtin-queue"` feature, each driver includes a track queue
|
||||||
|
/// as a convenience to prevent the additional overhead of per-guild state management.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Driver {
|
pub struct Driver {
|
||||||
config: Config,
|
config: Config,
|
||||||
self_mute: bool,
|
self_mute: bool,
|
||||||
sender: Sender<CoreMessage>,
|
sender: Sender<CoreMessage>,
|
||||||
|
#[cfg(feature = "builtin-queue")]
|
||||||
|
queue: TrackQueue,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Driver {
|
impl Driver {
|
||||||
@@ -53,6 +60,8 @@ impl Driver {
|
|||||||
config,
|
config,
|
||||||
self_mute: false,
|
self_mute: false,
|
||||||
sender,
|
sender,
|
||||||
|
#[cfg(feature = "builtin-queue")]
|
||||||
|
queue: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,6 +235,42 @@ impl Driver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "builtin-queue")]
|
||||||
|
impl Driver {
|
||||||
|
/// Returns a reference to this driver's built-in queue.
|
||||||
|
///
|
||||||
|
/// Requires the `"builtin-queue"` feature.
|
||||||
|
/// Queue additions should be made via [`enqueue`] and
|
||||||
|
/// [`enqueue_source`].
|
||||||
|
///
|
||||||
|
/// [`enqueue`]: #method.enqueue
|
||||||
|
/// [`enqueue_source`]: #method.enqueue_source
|
||||||
|
pub fn queue(&self) -> &TrackQueue {
|
||||||
|
&self.queue
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an audio [`Input`] to this driver's built-in queue.
|
||||||
|
///
|
||||||
|
/// Requires the `"builtin-queue"` feature.
|
||||||
|
///
|
||||||
|
/// [`Input`]: ../input/struct.input.html
|
||||||
|
pub fn enqueue_source(&mut self, source: Input) {
|
||||||
|
let (mut track, _) = tracks::create_player(source);
|
||||||
|
self.queue.add_raw(&mut track);
|
||||||
|
self.play(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an existing [`Track`] to this driver's built-in queue.
|
||||||
|
///
|
||||||
|
/// Requires the `"builtin-queue"` feature.
|
||||||
|
///
|
||||||
|
/// [`Track`]: ../tracks/struct.track.html
|
||||||
|
pub fn enqueue(&mut self, mut track: Track) {
|
||||||
|
self.queue.add_raw(&mut track);
|
||||||
|
self.play(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Driver {
|
impl Default for Driver {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(Default::default())
|
Self::new(Default::default())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use tokio::sync::{
|
|||||||
mpsc::{error::SendError, UnboundedSender},
|
mpsc::{error::SendError, UnboundedSender},
|
||||||
oneshot,
|
oneshot,
|
||||||
};
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
/// Handle for safe control of a [`Track`] track from other threads, outside
|
/// Handle for safe control of a [`Track`] track from other threads, outside
|
||||||
@@ -18,6 +19,7 @@ use tokio::sync::{
|
|||||||
pub struct TrackHandle {
|
pub struct TrackHandle {
|
||||||
command_channel: UnboundedSender<TrackCommand>,
|
command_channel: UnboundedSender<TrackCommand>,
|
||||||
seekable: bool,
|
seekable: bool,
|
||||||
|
uuid: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackHandle {
|
impl TrackHandle {
|
||||||
@@ -25,10 +27,11 @@ impl TrackHandle {
|
|||||||
/// the underlying [`Input`] supports seek operations.
|
/// the underlying [`Input`] supports seek operations.
|
||||||
///
|
///
|
||||||
/// [`Input`]: ../input/struct.Input.html
|
/// [`Input`]: ../input/struct.Input.html
|
||||||
pub fn new(command_channel: UnboundedSender<TrackCommand>, seekable: bool) -> Self {
|
pub fn new(command_channel: UnboundedSender<TrackCommand>, seekable: bool, uuid: Uuid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
command_channel,
|
command_channel,
|
||||||
seekable,
|
seekable,
|
||||||
|
uuid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +152,11 @@ impl TrackHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns this handle's (and track's) unique identifier.
|
||||||
|
pub fn uuid(&self) -> Uuid {
|
||||||
|
self.uuid
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Send a raw command to the [`Track`] object.
|
/// Send a raw command to the [`Track`] object.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use tokio::sync::{
|
|||||||
},
|
},
|
||||||
oneshot::Receiver as OneshotReceiver,
|
oneshot::Receiver as OneshotReceiver,
|
||||||
};
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Control object for audio playback.
|
/// Control object for audio playback.
|
||||||
///
|
///
|
||||||
@@ -117,6 +118,9 @@ pub struct Track {
|
|||||||
|
|
||||||
/// Count of remaining loops.
|
/// Count of remaining loops.
|
||||||
pub loops: LoopState,
|
pub loops: LoopState,
|
||||||
|
|
||||||
|
/// Unique identifier for this track.
|
||||||
|
pub(crate) uuid: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Track {
|
impl Track {
|
||||||
@@ -131,6 +135,8 @@ impl Track {
|
|||||||
commands: UnboundedReceiver<TrackCommand>,
|
commands: UnboundedReceiver<TrackCommand>,
|
||||||
handle: TrackHandle,
|
handle: TrackHandle,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let uuid = handle.uuid();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
playing: Default::default(),
|
playing: Default::default(),
|
||||||
volume: 1.0,
|
volume: 1.0,
|
||||||
@@ -141,6 +147,7 @@ impl Track {
|
|||||||
commands,
|
commands,
|
||||||
handle,
|
handle,
|
||||||
loops: LoopState::Finite(0),
|
loops: LoopState::Finite(0),
|
||||||
|
uuid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +348,11 @@ impl Track {
|
|||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns this track's unique identifier.
|
||||||
|
pub fn uuid(&self) -> Uuid {
|
||||||
|
self.uuid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a [`Track`] object to pass into the audio context, and a [`TrackHandle`]
|
/// Creates a [`Track`] object to pass into the audio context, and a [`TrackHandle`]
|
||||||
@@ -354,9 +366,11 @@ impl Track {
|
|||||||
pub fn create_player(source: Input) -> (Track, TrackHandle) {
|
pub fn create_player(source: Input) -> (Track, TrackHandle) {
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
let can_seek = source.is_seekable();
|
let can_seek = source.is_seekable();
|
||||||
let player = Track::new_raw(source, rx, TrackHandle::new(tx.clone(), can_seek));
|
let handle = TrackHandle::new(tx, can_seek, Uuid::new_v4());
|
||||||
|
|
||||||
(player, TrackHandle::new(tx, can_seek))
|
let player = Track::new_raw(source, rx, handle.clone());
|
||||||
|
|
||||||
|
(player, handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alias for most result-free calls to a [`TrackHandle`].
|
/// Alias for most result-free calls to a [`TrackHandle`].
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{collections::VecDeque, sync::Arc};
|
use std::{collections::VecDeque, ops::Deref, sync::Arc};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
/// A simple queue for several audio sources, designed to
|
/// A simple queue for several audio sources, designed to
|
||||||
/// play in sequence.
|
/// play in sequence.
|
||||||
///
|
///
|
||||||
/// This makes use of [`TrackEvent`]s to determine when the current
|
/// This makes use of [`TrackEvent`]s to determine when the current
|
||||||
/// song or audio file has finished before playing the next entry.
|
/// song or audio file has finished before playing the next entry.
|
||||||
///
|
///
|
||||||
|
/// One of these is automatically included via [`Driver::queue`] when
|
||||||
|
/// the `"builtin-queue"` feature is enabled.
|
||||||
|
///
|
||||||
/// `examples/serenity/voice_events_queue` demonstrates how a user might manage,
|
/// `examples/serenity/voice_events_queue` demonstrates how a user might manage,
|
||||||
/// track and use this to run a song queue in many guilds in parallel.
|
/// track and use this to run a song queue in many guilds in parallel.
|
||||||
/// This code is trivial to extend if extra functionality is needed.
|
/// This code is trivial to extend if extra functionality is needed.
|
||||||
@@ -50,15 +52,37 @@ use tracing::{info, warn};
|
|||||||
/// queue.add_source(source, &mut driver);
|
/// queue.add_source(source, &mut driver);
|
||||||
/// # };
|
/// # };
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
///
|
///
|
||||||
/// [`TrackEvent`]: ../events/enum.TrackEvent.html
|
/// [`TrackEvent`]: ../events/enum.TrackEvent.html
|
||||||
|
/// [`Driver::queue`]: ../driver/struct.Driver.html#method.queue
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct TrackQueue {
|
pub struct TrackQueue {
|
||||||
// NOTE: the choice of a parking lot mutex is quite deliberate
|
// NOTE: the choice of a parking lot mutex is quite deliberate
|
||||||
inner: Arc<Mutex<TrackQueueCore>>,
|
inner: Arc<Mutex<TrackQueueCore>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
/// Reference to a track which is known to be part of a queue.
|
||||||
|
///
|
||||||
|
/// Instances *should not* be moved from one queue to another.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Queued(TrackHandle);
|
||||||
|
|
||||||
|
impl Deref for Queued {
|
||||||
|
type Target = TrackHandle;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Queued {
|
||||||
|
/// Clones the inner handle
|
||||||
|
pub fn handle(&self) -> TrackHandle {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
/// Inner portion of a [`TrackQueue`].
|
/// Inner portion of a [`TrackQueue`].
|
||||||
///
|
///
|
||||||
/// This abstracts away thread-safety from the user,
|
/// This abstracts away thread-safety from the user,
|
||||||
@@ -66,7 +90,7 @@ pub struct TrackQueue {
|
|||||||
///
|
///
|
||||||
/// [`TrackQueue`]: struct.TrackQueue.html
|
/// [`TrackQueue`]: struct.TrackQueue.html
|
||||||
struct TrackQueueCore {
|
struct TrackQueueCore {
|
||||||
tracks: VecDeque<TrackHandle>,
|
tracks: VecDeque<Queued>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct QueueHandler {
|
struct QueueHandler {
|
||||||
@@ -77,13 +101,33 @@ struct QueueHandler {
|
|||||||
impl EventHandler for QueueHandler {
|
impl EventHandler for QueueHandler {
|
||||||
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
||||||
let mut inner = self.remote_lock.lock();
|
let mut inner = self.remote_lock.lock();
|
||||||
|
|
||||||
|
// Due to possibility that users might remove, reorder,
|
||||||
|
// or dequeue+stop tracks, we need to verify that the FIRST
|
||||||
|
// track is the one who has ended.
|
||||||
|
let front_ended = match ctx {
|
||||||
|
EventContext::Track(ts) => {
|
||||||
|
// This slice should have exactly one entry.
|
||||||
|
// If the ended track has same id as the queue head, then
|
||||||
|
// we can progress the queue.
|
||||||
|
let queue_uuid = inner.tracks.front().map(|handle| handle.uuid());
|
||||||
|
let ended_uuid = ts.first().map(|handle| handle.1.uuid());
|
||||||
|
|
||||||
|
queue_uuid.is_some() && queue_uuid == ended_uuid
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !front_ended {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let _old = inner.tracks.pop_front();
|
let _old = inner.tracks.pop_front();
|
||||||
|
|
||||||
info!("Queued track ended: {:?}.", ctx);
|
info!("Queued track ended: {:?}.", ctx);
|
||||||
info!("{} tracks remain.", inner.tracks.len());
|
info!("{} tracks remain.", inner.tracks.len());
|
||||||
|
|
||||||
// If any audio files die unexpectedly, then keep going until we
|
// Keep going until we find one track which works, or we run out.
|
||||||
// find one which works, or we run out.
|
|
||||||
let mut keep_looking = true;
|
let mut keep_looking = true;
|
||||||
while keep_looking && !inner.tracks.is_empty() {
|
while keep_looking && !inner.tracks.is_empty() {
|
||||||
if let Some(new) = inner.tracks.front() {
|
if let Some(new) = inner.tracks.front() {
|
||||||
@@ -113,8 +157,8 @@ impl TrackQueue {
|
|||||||
|
|
||||||
/// Adds an audio source to the queue, to be played in the channel managed by `handler`.
|
/// Adds an audio source to the queue, to be played in the channel managed by `handler`.
|
||||||
pub fn add_source(&self, source: Input, handler: &mut Driver) {
|
pub fn add_source(&self, source: Input, handler: &mut Driver) {
|
||||||
let (audio, audio_handle) = tracks::create_player(source);
|
let (audio, _) = tracks::create_player(source);
|
||||||
self.add(audio, audio_handle, handler);
|
self.add(audio, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a [`Track`] object to the queue, to be played in the channel managed by `handler`.
|
/// Adds a [`Track`] object to the queue, to be played in the channel managed by `handler`.
|
||||||
@@ -124,11 +168,19 @@ impl TrackQueue {
|
|||||||
///
|
///
|
||||||
/// [`Track`]: struct.Track.html
|
/// [`Track`]: struct.Track.html
|
||||||
/// [`voice::create_player`]: fn.create_player.html
|
/// [`voice::create_player`]: fn.create_player.html
|
||||||
pub fn add(&self, mut track: Track, track_handle: TrackHandle, handler: &mut Driver) {
|
pub fn add(&self, mut track: Track, handler: &mut Driver) {
|
||||||
|
self.add_raw(&mut track);
|
||||||
|
handler.play(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn add_raw(&self, track: &mut Track) {
|
||||||
info!("Track added to queue.");
|
info!("Track added to queue.");
|
||||||
let remote_lock = self.inner.clone();
|
let remote_lock = self.inner.clone();
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
|
|
||||||
|
let track_handle = track.handle.clone();
|
||||||
|
|
||||||
if !inner.tracks.is_empty() {
|
if !inner.tracks.is_empty() {
|
||||||
track.pause();
|
track.pause();
|
||||||
}
|
}
|
||||||
@@ -142,8 +194,23 @@ impl TrackQueue {
|
|||||||
track.position,
|
track.position,
|
||||||
);
|
);
|
||||||
|
|
||||||
handler.play(track);
|
inner.tracks.push_back(Queued(track_handle));
|
||||||
inner.tracks.push_back(track_handle);
|
}
|
||||||
|
|
||||||
|
/// Returns a handle to the currently playing track.
|
||||||
|
pub fn current(&self) -> Option<TrackHandle> {
|
||||||
|
let inner = self.inner.lock();
|
||||||
|
|
||||||
|
inner.tracks.front().map(|h| h.handle())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to remove a track from the specified index.
|
||||||
|
///
|
||||||
|
/// The returned entry can be readded to *this* queue via [`modify_queue`].
|
||||||
|
///
|
||||||
|
/// [`modify_queue`]: #method.modify_queue
|
||||||
|
pub fn dequeue(&self, index: usize) -> Option<Queued> {
|
||||||
|
self.modify_queue(|vq| vq.remove(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of tracks currently in the queue.
|
/// Returns the number of tracks currently in the queue.
|
||||||
@@ -160,6 +227,18 @@ impl TrackQueue {
|
|||||||
inner.tracks.is_empty()
|
inner.tracks.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allows modification of the inner queue (i.e., deletion, reordering).
|
||||||
|
///
|
||||||
|
/// Users must be careful to `stop` removed tracks, so as to prevent
|
||||||
|
/// resource leaks.
|
||||||
|
pub fn modify_queue<F, O>(&self, func: F) -> O
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut VecDeque<Queued>) -> O,
|
||||||
|
{
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
func(&mut inner.tracks)
|
||||||
|
}
|
||||||
|
|
||||||
/// Pause the track at the head of the queue.
|
/// Pause the track at the head of the queue.
|
||||||
pub fn pause(&self) -> TrackResult {
|
pub fn pause(&self) -> TrackResult {
|
||||||
let inner = self.inner.lock();
|
let inner = self.inner.lock();
|
||||||
@@ -183,14 +262,14 @@ impl TrackQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stop the currently playing track, and clears the queue.
|
/// Stop the currently playing track, and clears the queue.
|
||||||
pub fn stop(&self) -> TrackResult {
|
pub fn stop(&self) {
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
|
|
||||||
let out = inner.stop_current();
|
for track in inner.tracks.drain(..) {
|
||||||
|
// Errors when removing tracks don't really make
|
||||||
inner.tracks.clear();
|
// a difference: an error just implies it's already gone.
|
||||||
|
let _ = track.stop();
|
||||||
out
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Skip to the next track in the queue, if it exists.
|
/// Skip to the next track in the queue, if it exists.
|
||||||
|
|||||||
Reference in New Issue
Block a user