Compare commits

...

7 commits

8 changed files with 3423 additions and 186 deletions

4
.gitignore vendored
View file

@ -3,10 +3,6 @@
debug/ debug/
target/ target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk

3243
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -6,21 +6,24 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.69" anyhow = "1.0.69"
parking_lot = "0.12.1" parking_lot = "0.12.1"
poise = "0.5.2" poise = "0.6.1"
tracing = "0.1.37" tracing = "0.1.37"
tracing-futures = "0.2.5" tracing-futures = "0.2.5"
openai = "1.0.0-alpha.8" openai = "1.0.0-alpha.8"
rand = "0.8.5" rand = "0.8.5"
reqwest = "0.11.23"
songbird = "0.4.0"
thiserror = "1.0.39" thiserror = "1.0.39"
async-openai = "0.18.1"
[dependencies.tracing-subscriber] [dependencies.symphonia]
version = "0.3.16" version = "0.5.2"
features = ["fmt", "env-filter", "std"] features = ["mkv"]
[dependencies.songbird]
version = "0.3.1"
features = ["yt-dlp"]
[dependencies.tokio] [dependencies.tokio]
version = "1.26.0" version = "1.26.0"
features = ["macros", "rt-multi-thread", "signal"] features = ["macros", "rt-multi-thread", "signal"]
[dependencies.tracing-subscriber]
version = "0.3.18"
features = ["fmt", "env-filter", "std"]

View file

@ -1,22 +1,15 @@
FROM rust:1.68 as builder FROM rust:1.75-alpine3.19 as builder
ARG OPENAI_KEY ARG OPENAI_API_KEY
RUN apt-get update && apt-get --no-install-recommends install -y libopus-dev RUN apk add --no-cache musl-dev opus-dev
WORKDIR /usr/src/myapp WORKDIR /usr/src/myapp
COPY . . COPY . .
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
RUN cargo install --path . RUN cargo install --path .
FROM debian:bullseye-slim FROM alpine:3.19
COPY --from=mwader/static-ffmpeg:6.0 /ffmpeg /usr/local/bin/ RUN apk add --no-cache opus ca-certificates yt-dlp
RUN apt-get update && apt-get --no-install-recommends install -y \
libopus0 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ADD https://github.com/anpage/yt-dlp/releases/download/2023.03.23.040309/yt-dlp_linux /usr/local/bin/yt-dlp
RUN chmod 755 /usr/local/bin/yt-dlp
COPY --from=builder /usr/local/cargo/bin/dj_kitty_cat /usr/local/bin/dj_kitty_cat COPY --from=builder /usr/local/cargo/bin/dj_kitty_cat /usr/local/bin/dj_kitty_cat
RUN useradd djkk RUN useradd djkc
WORKDIR /opt WORKDIR /opt
RUN chown djkk:djkk /opt RUN chown djkc:djkc /opt
USER djkk USER djkc
CMD ["dj_kitty_cat"] CMD ["dj_kitty_cat"]

View file

@ -1,17 +1,20 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use poise::serenity_prelude::{EmbedMessageBuilding, MessageBuilder}; use poise::{
serenity_prelude::{CreateEmbed, EmbedMessageBuilding, MessageBuilder},
CreateReply,
};
use songbird::input::{Input, YoutubeDl};
use tracing::{debug, log::warn}; use tracing::{debug, log::warn};
use crate::{personality, CommandContext, Error}; use crate::{personality, CommandContext, Error};
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn join(ctx: CommandContext<'_>) -> Result<(), Error> { pub async fn join(ctx: CommandContext<'_>) -> Result<(), Error> {
let Some(guild) = ctx.guild() else { let Some((guild_id, Some(channel_id))) = ctx.guild().and_then(|g| {
ctx.say("You're not in a server, silly.").await?; g.voice_states
return Ok(()); .get(&ctx.author().id)
}; .map(|vs| (g.id, vs.channel_id))
}) else {
let Some(Some(channel_id)) = guild.voice_states.get(&ctx.author().id).map(|vs| vs.channel_id) else {
ctx.say("You're not in a voice channel, silly.").await?; ctx.say("You're not in a voice channel, silly.").await?;
return Ok(()); return Ok(());
}; };
@ -20,7 +23,7 @@ pub async fn join(ctx: CommandContext<'_>) -> Result<(), Error> {
.await .await
.context("Expected a songbird manager")? .context("Expected a songbird manager")?
.clone(); .clone();
let _handler = manager.join(guild.id, channel_id).await; let _handler = manager.join(guild_id, channel_id).await;
ctx.say("Joining your channel!").await?; ctx.say("Joining your channel!").await?;
@ -29,7 +32,7 @@ pub async fn join(ctx: CommandContext<'_>) -> Result<(), Error> {
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn leave(ctx: CommandContext<'_>) -> Result<(), Error> { pub async fn leave(ctx: CommandContext<'_>) -> Result<(), Error> {
let Some(guild) = ctx.guild() else { let Some(guild_id) = ctx.guild().map(|g| g.id) else {
ctx.say("You're not in a server, silly.").await?; ctx.say("You're not in a server, silly.").await?;
return Ok(()); return Ok(());
}; };
@ -39,12 +42,12 @@ pub async fn leave(ctx: CommandContext<'_>) -> Result<(), Error> {
.context("Expected a songbird manager")? .context("Expected a songbird manager")?
.clone(); .clone();
if manager.get(guild.id).is_none() { if manager.get(guild_id).is_none() {
ctx.say("I'm not even in a voice channel!").await?; ctx.say("I'm not even in a voice channel!").await?;
return Ok(()); return Ok(());
} }
let _handler = manager.remove(guild.id).await; let _handler = manager.remove(guild_id).await;
ctx.say("Okay bye!").await?; ctx.say("Okay bye!").await?;
@ -61,7 +64,7 @@ pub async fn play(
return Ok(()); return Ok(());
}; };
let Some(guild) = ctx.guild() else { let Some(guild_id) = ctx.guild().map(|g| g.id) else {
ctx.say("You're not in a server, silly.").await?; ctx.say("You're not in a server, silly.").await?;
return Ok(()); return Ok(());
}; };
@ -71,53 +74,35 @@ pub async fn play(
.context("Expected a songbird manager")? .context("Expected a songbird manager")?
.clone(); .clone();
if manager.get(guild.id).is_none() { if manager.get(guild_id).is_none() {
let Some(Some(channel_id)) = guild.voice_states.get(&ctx.author().id).map(|vs| vs.channel_id) else { let Some((guild_id, Some(channel_id))) = ctx.guild().and_then(|g| {
ctx.say("Neither of us are in a voice channel, silly.").await?; g.voice_states
.get(&ctx.author().id)
.map(|vs| (g.id, vs.channel_id))
}) else {
ctx.say("You're not in a voice channel, silly.").await?;
return Ok(()); return Ok(());
}; };
let _handler = manager.join(guild.id, channel_id).await; let _handler = manager.join(guild_id, channel_id).await;
} }
if let Some(handler_lock) = manager.get(guild.id) { if let Some(handler_lock) = manager.get(guild_id) {
let mut msg = MessageBuilder::new(); let reply = poise::CreateReply::default()
msg.push_line(String::from(personality::get_random_loading_message())) .content(personality::get_random_loading_message())
.push_named_link( .embed(
"", CreateEmbed::new()
"https://media.giphy.com/media/H1dxi6xdh4NGQCZSvz/giphy.gif", .image("https://media.giphy.com/media/H1dxi6xdh4NGQCZSvz/giphy.gif"),
); );
let response = ctx.send(|r| r.content(msg.build())).await?; let response = ctx.send(reply).await?;
let mut handler = handler_lock.lock().await; let mut handler = handler_lock.lock().await;
debug!("Trying to play: {}", url); debug!("Trying to play: {}", url);
let source = songbird::ytdl(&url).await; let mut source: Input = YoutubeDl::new(ctx.data().http_client.clone(), url.clone()).into();
let metadata = source.aux_metadata().await?;
let source = match source { debug!("Playing: {:?}", metadata);
Ok(source) => source, let title = metadata.title.clone().unwrap_or(String::from("This video"));
Err(e) => {
match e {
songbird::input::error::Error::Json {
ref error,
ref parsed_text,
} => {
debug!("Failed to play: {}", error);
debug!("Parsed text: {}", parsed_text);
}
_ => {
debug!("Failed to play: {}", e);
}
}
return Err(Box::new(e));
}
};
debug!("Playing: {:?}", source.metadata);
let title = source
.metadata
.title
.clone()
.unwrap_or(String::from("This video"));
let mut msg = MessageBuilder::new(); let mut msg = MessageBuilder::new();
@ -133,13 +118,15 @@ pub async fn play(
msg.push_bold("Now playing: ").push_named_link(title, url); msg.push_bold("Now playing: ").push_named_link(title, url);
response.edit(ctx, |r| r.content(msg.build())).await?; response
.edit(ctx, CreateReply::default().content(msg.build()))
.await?;
let mut queue = ctx.data().queue.lock(); let mut queue = ctx.data().queue.lock();
if !queue.is_empty() { if !queue.is_empty() {
let _ = queue.stop(); let _ = queue.stop();
} }
queue.add_next(source, &mut handler); queue.add_next(source, &mut handler)?;
queue.resume()?; queue.resume()?;
} else { } else {
ctx.say("Neither of us are in a voice channel, silly.") ctx.say("Neither of us are in a voice channel, silly.")
@ -159,7 +146,7 @@ pub async fn queue(
return Ok(()); return Ok(());
}; };
let Some(guild) = ctx.guild() else { let Some(guild_id) = ctx.guild().map(|g| g.id) else {
ctx.say("You're not in a server, silly.").await?; ctx.say("You're not in a server, silly.").await?;
return Ok(()); return Ok(());
}; };
@ -169,33 +156,35 @@ pub async fn queue(
.context("Expected a songbird manager")? .context("Expected a songbird manager")?
.clone(); .clone();
if manager.get(guild.id).is_none() { if manager.get(guild_id).is_none() {
let Some(Some(channel_id)) = guild.voice_states.get(&ctx.author().id).map(|vs| vs.channel_id) else { let Some((guild_id, Some(channel_id))) = ctx.guild().and_then(|g| {
ctx.say("Neither of us are in a voice channel, silly.").await?; g.voice_states
.get(&ctx.author().id)
.map(|vs| (g.id, vs.channel_id))
}) else {
ctx.say("You're not in a voice channel, silly.").await?;
return Ok(()); return Ok(());
}; };
let _handler = manager.join(guild.id, channel_id).await; let _handler = manager.join(guild_id, channel_id).await;
} }
if let Some(handler_lock) = manager.get(guild.id) { if let Some(handler_lock) = manager.get(guild_id) {
let mut msg = MessageBuilder::new(); let reply = poise::CreateReply::default()
msg.push_line(String::from(personality::get_random_loading_message())) .content(personality::get_random_loading_message())
.push_named_link( .embed(
"", CreateEmbed::new()
"https://media.giphy.com/media/H1dxi6xdh4NGQCZSvz/giphy.gif", .image("https://media.giphy.com/media/H1dxi6xdh4NGQCZSvz/giphy.gif"),
); );
let response = ctx.send(|r| r.content(msg.build())).await?; let response = ctx.send(reply).await?;
let mut handler = handler_lock.lock().await; let mut handler = handler_lock.lock().await;
debug!("Trying to play: {}", url); debug!("Trying to play: {}", url);
let source = songbird::ytdl(&url).await?; let mut source: Input = YoutubeDl::new(ctx.data().http_client.clone(), url.clone()).into();
debug!("Playing: {:?}", source.metadata); let metadata = source.aux_metadata().await?;
let title = source
.metadata debug!("Playing: {:?}", metadata);
.title let title = metadata.title.clone().unwrap_or(String::from("This video"));
.clone()
.unwrap_or(String::from("This video"));
let mut msg = MessageBuilder::new(); let mut msg = MessageBuilder::new();
@ -211,10 +200,12 @@ pub async fn queue(
msg.push_bold("Queued: ").push_named_link(title, url); msg.push_bold("Queued: ").push_named_link(title, url);
response.edit(ctx, |r| r.content(msg.build())).await?; response
.edit(ctx, CreateReply::default().content(msg.build()))
.await?;
let mut queue = ctx.data().queue.lock(); let mut queue = ctx.data().queue.lock();
queue.add_to_end(source, &mut handler); queue.add_to_end(source, &mut handler)?;
} else { } else {
ctx.say("Neither of us are in a voice channel, silly.") ctx.say("Neither of us are in a voice channel, silly.")
.await?; .await?;
@ -225,7 +216,7 @@ pub async fn queue(
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> { pub async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> {
let Some(guild) = ctx.guild() else { let Some(guild_id) = ctx.guild().map(|g| g.id) else {
ctx.say("You're not in a server, silly.").await?; ctx.say("You're not in a server, silly.").await?;
return Ok(()); return Ok(());
}; };
@ -235,7 +226,7 @@ pub async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> {
.context("Expected a songbird manager")? .context("Expected a songbird manager")?
.clone(); .clone();
if manager.get(guild.id).is_some() { if manager.get(guild_id).is_some() {
{ {
let mut queue = ctx.data().queue.lock(); let mut queue = ctx.data().queue.lock();
queue.stop()?; queue.stop()?;
@ -251,7 +242,7 @@ pub async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> {
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn skip(ctx: CommandContext<'_>) -> Result<(), Error> { pub async fn skip(ctx: CommandContext<'_>) -> Result<(), Error> {
let Some(guild) = ctx.guild() else { let Some(guild_id) = ctx.guild().map(|g| g.id) else {
ctx.say("You're not in a server, silly.").await?; ctx.say("You're not in a server, silly.").await?;
return Ok(()); return Ok(());
}; };
@ -261,7 +252,7 @@ pub async fn skip(ctx: CommandContext<'_>) -> Result<(), Error> {
.context("Expected a songbird manager")? .context("Expected a songbird manager")?
.clone(); .clone();
if manager.get(guild.id).is_some() { if manager.get(guild_id).is_some() {
{ {
let mut queue = ctx.data().queue.lock(); let mut queue = ctx.data().queue.lock();
let _ = queue.stop(); let _ = queue.stop();
@ -278,7 +269,7 @@ pub async fn skip(ctx: CommandContext<'_>) -> Result<(), Error> {
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn pause(ctx: CommandContext<'_>) -> Result<(), Error> { pub async fn pause(ctx: CommandContext<'_>) -> Result<(), Error> {
let Some(guild) = ctx.guild() else { let Some(guild_id) = ctx.guild().map(|g| g.id) else {
ctx.say("You're not in a server, silly.").await?; ctx.say("You're not in a server, silly.").await?;
return Ok(()); return Ok(());
}; };
@ -288,7 +279,7 @@ pub async fn pause(ctx: CommandContext<'_>) -> Result<(), Error> {
.context("Expected a songbird manager")? .context("Expected a songbird manager")?
.clone(); .clone();
if manager.get(guild.id).is_some() { if manager.get(guild_id).is_some() {
{ {
let mut queue = ctx.data().queue.lock(); let mut queue = ctx.data().queue.lock();
queue.pause()?; queue.pause()?;
@ -304,7 +295,7 @@ pub async fn pause(ctx: CommandContext<'_>) -> Result<(), Error> {
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn resume(ctx: CommandContext<'_>) -> Result<(), Error> { pub async fn resume(ctx: CommandContext<'_>) -> Result<(), Error> {
let Some(guild) = ctx.guild() else { let Some(guild_id) = ctx.guild().map(|g| g.id) else {
ctx.say("You're not in a server, silly.").await?; ctx.say("You're not in a server, silly.").await?;
return Ok(()); return Ok(());
}; };
@ -314,7 +305,7 @@ pub async fn resume(ctx: CommandContext<'_>) -> Result<(), Error> {
.context("Expected a songbird manager")? .context("Expected a songbird manager")?
.clone(); .clone();
if manager.get(guild.id).is_some() { if manager.get(guild_id).is_some() {
{ {
let mut queue = ctx.data().queue.lock(); let mut queue = ctx.data().queue.lock();
queue.resume()?; queue.resume()?;
@ -330,7 +321,7 @@ pub async fn resume(ctx: CommandContext<'_>) -> Result<(), Error> {
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn clear(ctx: CommandContext<'_>) -> Result<(), Error> { pub async fn clear(ctx: CommandContext<'_>) -> Result<(), Error> {
let Some(guild) = ctx.guild() else { let Some(guild_id) = ctx.guild().map(|g| g.id) else {
ctx.say("You're not in a server, silly.").await?; ctx.say("You're not in a server, silly.").await?;
return Ok(()); return Ok(());
}; };
@ -340,7 +331,7 @@ pub async fn clear(ctx: CommandContext<'_>) -> Result<(), Error> {
.context("Expected a songbird manager")? .context("Expected a songbird manager")?
.clone(); .clone();
if manager.get(guild.id).is_some() { if manager.get(guild_id).is_some() {
{ {
let mut queue = ctx.data().queue.lock(); let mut queue = ctx.data().queue.lock();
queue.clear(); queue.clear();

View file

@ -5,17 +5,19 @@ mod personality;
mod queue; mod queue;
use commands::*; use commands::*;
use openai::set_key;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use anyhow::Result; use anyhow::Result;
use openai::set_key;
use parking_lot::Mutex; use parking_lot::Mutex;
use poise::serenity_prelude::{self as serenity}; use poise::serenity_prelude as serenity;
use songbird::serenity::SerenityInit; use reqwest::Client as HttpClient;
use songbird::SerenityInit;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
pub struct Data { pub struct Data {
http_client: HttpClient,
queue: Arc<Mutex<queue::Queue>>, queue: Arc<Mutex<queue::Queue>>,
} }
pub type Error = Box<dyn std::error::Error + Send + Sync>; pub type Error = Box<dyn std::error::Error + Send + Sync>;
@ -23,11 +25,11 @@ pub type CommandContext<'a> = poise::Context<'a, Data, Error>;
async fn event_event_handler( async fn event_event_handler(
_ctx: &serenity::Context, _ctx: &serenity::Context,
event: &poise::Event<'_>, event: &serenity::FullEvent,
_framework: poise::FrameworkContext<'_, Data, Error>, _framework: poise::FrameworkContext<'_, Data, Error>,
_user_data: &Data, _user_data: &Data,
) -> Result<(), Error> { ) -> Result<(), Error> {
if let poise::Event::Ready { data_about_bot } = event { if let serenity::FullEvent::Ready { data_about_bot } = event {
println!("{} is connected!", data_about_bot.user.name) println!("{} is connected!", data_about_bot.user.name)
} }
@ -49,6 +51,10 @@ async fn main() -> Result<()> {
.with(EnvFilter::from_default_env()) .with(EnvFilter::from_default_env())
.init(); .init();
env::var("OPENAI_API_KEY")
.expect("Expected an OpenAI API key in the environment: OPENAI_API_KEY");
// OLD
set_key(env::var("OPENAI_KEY").expect("Expected an OpenAI key in the environment: OPENAI_KEY")); set_key(env::var("OPENAI_KEY").expect("Expected an OpenAI key in the environment: OPENAI_KEY"));
let token = let token =
@ -75,19 +81,23 @@ async fn main() -> Result<()> {
let intents = serenity::GatewayIntents::non_privileged(); let intents = serenity::GatewayIntents::non_privileged();
poise::Framework::builder() let framework = poise::Framework::builder()
.token(token)
.options(options) .options(options)
.intents(intents)
.setup(|_ctx, _data, _framework| { .setup(|_ctx, _data, _framework| {
Box::pin(async move { Box::pin(async move {
Ok(Data { Ok(Data {
http_client: HttpClient::new(),
queue: Arc::new(Mutex::new(queue::Queue::new())), queue: Arc::new(Mutex::new(queue::Queue::new())),
}) })
}) })
}) })
.client_settings(|client_builder| client_builder.register_songbird()) .build();
.run()
serenity::ClientBuilder::new(token, intents)
.framework(framework)
.register_songbird()
.await?
.start()
.await?; .await?;
Ok(()) Ok(())

View file

@ -1,5 +1,9 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use openai::chat::{ChatCompletion, ChatCompletionMessage}; use async_openai::types::{
ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage,
ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs,
CreateChatCompletionRequestArgs,
};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
const LOADING_MESSAGES: [&str; 20] = [ const LOADING_MESSAGES: [&str; 20] = [
@ -53,48 +57,52 @@ pub async fn get_sassy_commentary(title: &str) -> Result<String> {
let prompt = format!("Play \"{title}\""); let prompt = format!("Play \"{title}\"");
let completion = ChatCompletion::builder( let client = async_openai::Client::new();
"gpt-4",
[
system
.into_iter()
.map(|s| ChatCompletionMessage {
role: openai::chat::ChatCompletionMessageRole::System,
content: String::from(s),
name: None,
})
.collect::<Vec<_>>(),
vec![
ChatCompletionMessage {
role: openai::chat::ChatCompletionMessageRole::User,
content: String::from(example_prompt),
name: None,
},
ChatCompletionMessage {
role: openai::chat::ChatCompletionMessageRole::Assistant,
content: String::from(example_response),
name: None,
},
ChatCompletionMessage {
role: openai::chat::ChatCompletionMessageRole::User,
content: prompt,
name: None,
},
],
]
.into_iter()
.flatten()
.collect::<Vec<_>>(),
)
.max_tokens(2048_u64)
.create()
.await??;
Ok(completion let request = CreateChatCompletionRequestArgs::default()
.model("gpt-4")
.messages(
[
system
.into_iter()
.map(|s| {
ChatCompletionRequestSystemMessageArgs::default()
.content(s)
.build()
.unwrap()
.into()
})
.collect::<Vec<ChatCompletionRequestMessage>>(),
vec![
ChatCompletionRequestUserMessageArgs::default()
.content(example_prompt)
.build()?
.into(),
ChatCompletionRequestAssistantMessageArgs::default()
.content(example_response)
.build()?
.into(),
ChatCompletionRequestUserMessageArgs::default()
.content(prompt)
.build()?
.into(),
],
]
.into_iter()
.flatten()
.collect::<Vec<_>>(),
)
.max_tokens(2048_u16)
.build()?;
let response = client.chat().create(request).await?;
response
.choices .choices
.first() .first()
.context("No choices")? .context("No choices")?
.message .message
.content .content
.clone()) .clone()
.context("No content")
} }

View file

@ -6,9 +6,8 @@ use anyhow::Result;
use parking_lot::Mutex; use parking_lot::Mutex;
use poise::async_trait; use poise::async_trait;
use songbird::{ use songbird::{
events::EventData,
input::Input, input::Input,
tracks::{self, Track, TrackHandle}, tracks::{Track, TrackHandle},
Driver, Event, EventContext, EventHandler, TrackEvent, Driver, Event, EventContext, EventHandler, TrackEvent,
}; };
use thiserror::Error; use thiserror::Error;
@ -31,16 +30,12 @@ struct QueueHandler {
impl QueueHandler { impl QueueHandler {
/// Event to remove the track from the queue when it ends /// Event to remove the track from the queue when it ends
pub fn register_track_end_event(track: &mut Track, remote_lock: Arc<Mutex<QueueCore>>) { pub fn register_track_end_event(
let position = track.position(); track: &mut TrackHandle,
track remote_lock: Arc<Mutex<QueueCore>>,
.events ) -> Result<()> {
.as_mut() track.add_event(Event::Track(TrackEvent::End), QueueHandler { remote_lock })?;
.expect("Why is this even an Option?") Ok(())
.add_event(
EventData::new(Event::Track(TrackEvent::End), QueueHandler { remote_lock }),
position,
);
} }
} }
@ -127,14 +122,13 @@ impl Queue {
} }
/// Adds a track to the end of the queue /// Adds a track to the end of the queue
pub fn add_to_end(&mut self, source: Input, handler: &mut Driver) -> TrackHandle { pub fn add_to_end(&mut self, source: Input, handler: &mut Driver) -> Result<TrackHandle> {
let mut inner = self.inner.lock(); let mut inner = self.inner.lock();
let (mut track, handle) = tracks::create_player(source); let track = Track::from(source).pause();
track.pause(); let mut handle = handler.play(track);
QueueHandler::register_track_end_event(&mut track, self.inner.clone()); QueueHandler::register_track_end_event(&mut handle, self.inner.clone())?;
inner.tracks.push_back(handle.clone()); inner.tracks.push_back(handle.clone());
handler.play(track); Ok(handle)
handle
} }
/// Adds multiple tracks to the end of the queue /// Adds multiple tracks to the end of the queue
@ -142,27 +136,26 @@ impl Queue {
&mut self, &mut self,
sources: Vec<Input>, sources: Vec<Input>,
handler: &mut Driver, handler: &mut Driver,
) -> Vec<TrackHandle> { ) -> Result<Vec<TrackHandle>> {
let mut handles = Vec::new(); let mut handles = Vec::new();
for source in sources { for source in sources {
handles.push(self.add_to_end(source, handler)); handles.push(self.add_to_end(source, handler)?);
} }
handles Ok(handles)
} }
/// Adds a track to play next /// Adds a track to play next
pub fn add_next(&mut self, source: Input, handler: &mut Driver) -> TrackHandle { pub fn add_next(&mut self, source: Input, handler: &mut Driver) -> Result<TrackHandle> {
let mut inner = self.inner.lock(); let mut inner = self.inner.lock();
let (mut track, handle) = tracks::create_player(source); let track = Track::from(source).pause();
track.pause(); let mut handle = handler.play(track);
QueueHandler::register_track_end_event(&mut track, self.inner.clone()); QueueHandler::register_track_end_event(&mut handle, self.inner.clone())?;
if inner.tracks.is_empty() { if inner.tracks.is_empty() {
inner.tracks.push_back(handle.clone()); inner.tracks.push_back(handle.clone());
} else { } else {
inner.tracks.insert(1, handle.clone()); inner.tracks.insert(1, handle.clone());
} }
handler.play(track); Ok(handle)
handle
} }
/// Clears the queue /// Clears the queue