Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,744 changes: 1,197 additions & 547 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ rspotify = "0.12.0"
serde_json = "1.0.79"
url = "2.3.1"
serde = "1.0.152"
ffprobe = "0.3.3"
reqwest = "0.11.6"
tempfile = "3.3"

[dependencies.songbird]
version = "0.3.2"
Expand Down
152 changes: 104 additions & 48 deletions src/commands/play.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use crate::{
handlers::track_end::update_queue_messages,
messaging::message::ParrotMessage,
messaging::messages::{
PLAY_QUEUE, PLAY_TOP, SPOTIFY_AUTH_FAILED, TRACK_DURATION, TRACK_TIME_TO_PLAY,
PLAY_QUEUE, PLAY_TOP, QUEUE_NO_SRC, QUEUE_NO_TITLE, SPOTIFY_AUTH_FAILED, TRACK_DURATION,
TRACK_TIME_TO_PLAY,
},
sources::{
file::{extract_query_type, FileRestartable},
spotify::{Spotify, SPOTIFY},
youtube::{YouTube, YouTubeRestartable},
},
Expand All @@ -17,8 +19,12 @@ use crate::{
},
};
use serenity::{
builder::CreateEmbed, client::Context,
model::application::interaction::application_command::ApplicationCommandInteraction,
builder::CreateEmbed,
client::Context,
model::{
application::interaction::application_command::ApplicationCommandInteraction,
prelude::{interaction::application_command::CommandDataOptionValue, Attachment},
},
prelude::Mutex,
};
use songbird::{input::Restartable, tracks::TrackHandle, Call};
Expand All @@ -35,57 +41,27 @@ pub enum Mode {
Jump,
}

#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum QueryType {
Keywords(String),
KeywordList(Vec<String>),
VideoLink(String),
PlaylistLink(String),
File(Attachment),
}

pub async fn play(
async fn match_url(
ctx: &Context,
interaction: &mut ApplicationCommandInteraction,
) -> Result<(), ParrotError> {
let args = interaction.data.options.clone();
let first_arg = args.first().unwrap();

let mode = match first_arg.name.as_str() {
"next" => Mode::Next,
"all" => Mode::All,
"reverse" => Mode::Reverse,
"shuffle" => Mode::Shuffle,
"jump" => Mode::Jump,
_ => Mode::End,
};

let url = match mode {
Mode::End => first_arg.value.as_ref().unwrap().as_str().unwrap(),
_ => first_arg
.options
.first()
.unwrap()
.value
.as_ref()
.unwrap()
.as_str()
.unwrap(),
};

url: &str,
) -> Result<Option<QueryType>, ParrotError> {
let guild_id = interaction.guild_id.unwrap();
let manager = songbird::get(ctx).await.unwrap();

// try to join a voice channel if not in one just yet
summon(ctx, interaction, false).await?;
let call = manager.get(guild_id).unwrap();

// determine whether this is a link or a query string
let query_type = match Url::parse(url) {
match Url::parse(url) {
Ok(url_data) => match url_data.host_str() {
Some("open.spotify.com") => {
let spotify = SPOTIFY.lock().await;
let spotify = verify(spotify.as_ref(), ParrotError::Other(SPOTIFY_AUTH_FAILED))?;
Some(Spotify::extract(spotify, url).await?)
Spotify::extract(spotify, url).await.map(Some)
}
Some(other) => {
let mut data = ctx.data.write().await;
Expand All @@ -112,12 +88,13 @@ pub async fn play(
domain: other.to_string(),
},
)
.await;
.await
.map(|_| None);
}

YouTube::extract(url)
Ok(YouTube::extract(url))
}
None => None,
None => Ok(None),
},
Err(_) => {
let mut data = ctx.data.write().await;
Expand All @@ -137,13 +114,68 @@ pub async fn play(
domain: "youtube.com".to_string(),
},
)
.await;
.await
.map(|_| None);
}

Some(QueryType::Keywords(url.to_string()))
Ok(Some(QueryType::Keywords(url.to_string())))
}
}
}

pub async fn play(
ctx: &Context,
interaction: &mut ApplicationCommandInteraction,
) -> Result<(), ParrotError> {
let first_arg = interaction.data.options.first().ok_or(ParrotError::Other(
"Expected at least one argument for play command",
))?;
let mode_str = first_arg.name.as_str();

let (mode, idx) = match mode_str {
"next" => (Mode::Next, 1),
"all" => (Mode::All, 1),
"reverse" => (Mode::Reverse, 1),
"shuffle" => (Mode::Shuffle, 1),
"jump" => (Mode::Jump, 1),
_ => (Mode::End, 0),
};

let arg = interaction
.data
.options
.get(idx)
.ok_or(ParrotError::Other("Expected attachment or query option"))?
.clone();

let option = arg
.resolved
.as_ref()
.ok_or(ParrotError::Other("Expected attachment or query object"))?;

let (url, attachment) = match option {
CommandDataOptionValue::Attachment(attachment) => (None, Some(attachment)),
CommandDataOptionValue::String(url) => (Some(url), None),
_ => {
return Err(ParrotError::Other(
"Something went wrong while parsing your query!",
));
}
};

let guild_id = interaction.guild_id.unwrap();
let manager = songbird::get(ctx).await.unwrap();

// try to join a voice channel if not in one just yet
summon(ctx, interaction, false).await?;
let call = manager.get(guild_id).unwrap();

// determine whether this is a link or a query string
let query_type = match url {
Some(url) => match_url(ctx, interaction, url).await?,
None => Some(extract_query_type(attachment.unwrap().clone())?),
};

let query_type = verify(
query_type,
ParrotError::Other("Something went wrong while parsing your query!"),
Expand Down Expand Up @@ -184,6 +216,10 @@ pub async fn play(
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
}
QueryType::File(_) => {
let queue = enqueue_track(&call, &query_type).await?;
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
},
Mode::Next => match query_type.clone() {
QueryType::Keywords(_) | QueryType::VideoLink(_) => {
Expand All @@ -210,6 +246,10 @@ pub async fn play(
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
}
QueryType::File(_) => {
let queue = insert_track(&call, &query_type, 1).await?;
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
},
Mode::Jump => match query_type.clone() {
QueryType::Keywords(_) | QueryType::VideoLink(_) => {
Expand Down Expand Up @@ -261,6 +301,10 @@ pub async fn play(
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
}
QueryType::File(_) => {
let queue = insert_track(&call, &query_type, 1).await?;
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
},
Mode::All | Mode::Reverse | Mode::Shuffle => match query_type.clone() {
QueryType::VideoLink(url) | QueryType::PlaylistLink(url) => {
Expand All @@ -281,6 +325,10 @@ pub async fn play(
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
}
QueryType::File(_) => {
let queue = enqueue_track(&call, &query_type).await?;
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
_ => {
edit_response(&ctx.http, interaction, ParrotMessage::PlayAllFailed).await?;
return Ok(());
Expand Down Expand Up @@ -371,14 +419,18 @@ async fn create_queued_embed(
let mut embed = CreateEmbed::default();
let metadata = track.metadata().clone();

embed.thumbnail(metadata.thumbnail.unwrap());
if let Some(thumbnail) = &metadata.thumbnail {
embed.thumbnail(thumbnail);
}

embed.field(
title,
format!(
"[**{}**]({})",
metadata.title.unwrap(),
metadata.source_url.unwrap()
metadata.title.unwrap_or_else(|| QUEUE_NO_TITLE.to_string()),
metadata
.source_url
.unwrap_or_else(|| QUEUE_NO_SRC.to_string())
),
false,
);
Expand All @@ -405,6 +457,10 @@ async fn get_track_source(query_type: QueryType) -> Result<Restartable, ParrotEr
.await
.map_err(ParrotError::TrackFail),

QueryType::File(attachment) => FileRestartable::download(attachment.url, true)
.await
.map_err(ParrotError::TrackFail),

_ => unreachable!(),
}
}
Expand Down
19 changes: 12 additions & 7 deletions src/commands/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crate::{
guild::cache::GuildCacheMap,
handlers::track_end::ModifyQueueHandler,
messaging::messages::{
QUEUE_EXPIRED, QUEUE_NOTHING_IS_PLAYING, QUEUE_NOW_PLAYING, QUEUE_NO_SONGS, QUEUE_PAGE,
QUEUE_PAGE_OF, QUEUE_UP_NEXT,
QUEUE_EXPIRED, QUEUE_NOTHING_IS_PLAYING, QUEUE_NOW_PLAYING, QUEUE_NO_SONGS, QUEUE_NO_SRC,
QUEUE_NO_TITLE, QUEUE_PAGE, QUEUE_PAGE_OF, QUEUE_UP_NEXT,
},
utils::get_human_readable_timestamp,
};
Expand Down Expand Up @@ -142,12 +142,17 @@ pub fn create_queue_embed(tracks: &[TrackHandle], page: usize) -> CreateEmbed {

let description = if !tracks.is_empty() {
let metadata = tracks[0].metadata();
embed.thumbnail(tracks[0].metadata().thumbnail.clone().unwrap());
if let Some(thumbnail) = metadata.thumbnail.as_ref() {
embed.thumbnail(thumbnail);
}

let title = metadata.title.as_deref().unwrap_or(QUEUE_NO_TITLE);
let source_url = metadata.source_url.as_deref().unwrap_or(QUEUE_NO_SRC);

format!(
"[{}]({}) • `{}`",
metadata.title.as_ref().unwrap(),
metadata.source_url.as_ref().unwrap(),
title,
source_url,
get_human_readable_timestamp(metadata.duration)
)
} else {
Expand Down Expand Up @@ -210,8 +215,8 @@ fn build_queue_page(tracks: &[TrackHandle], page: usize) -> String {
let mut description = String::new();

for (i, t) in queue.iter().enumerate() {
let title = t.metadata().title.as_ref().unwrap();
let url = t.metadata().source_url.as_ref().unwrap();
let title = t.metadata().title.as_deref().unwrap_or(QUEUE_NO_TITLE);
let url = t.metadata().source_url.as_deref().unwrap_or(QUEUE_NO_SRC);
let duration = get_human_readable_timestamp(t.metadata().duration);

let _ = writeln!(
Expand Down
8 changes: 6 additions & 2 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::messaging::messages::{
FAIL_ANOTHER_CHANNEL, FAIL_AUTHOR_DISCONNECTED, FAIL_AUTHOR_NOT_FOUND,
FAIL_NO_VOICE_CONNECTION, FAIL_WRONG_CHANNEL, NOTHING_IS_PLAYING, QUEUE_IS_EMPTY,
TRACK_INAPPROPRIATE, TRACK_NOT_FOUND,
FAIL_NO_VOICE_CONNECTION, FAIL_WRONG_CHANNEL, FILE_TOO_LARGE, NOTHING_IS_PLAYING,
QUEUE_IS_EMPTY, TRACK_INAPPROPRIATE, TRACK_NOT_FOUND, UNSUPPORTED_FILE_TYPE,
};
use rspotify::ClientError as RSpotifyClientError;
use serenity::{model::mention::Mention, prelude::SerenityError};
Expand All @@ -26,6 +26,8 @@ pub enum ParrotError {
RSpotify(RSpotifyClientError),
IO(std::io::Error),
Serde(serde_json::Error),
FileTooLarge,
UnsupportedFileType,
}

/// `ParrotError` implements the [`Debug`] and [`Display`] traits
Expand Down Expand Up @@ -70,6 +72,8 @@ impl Display for ParrotError {
Self::RSpotify(err) => f.write_str(&format!("{err}")),
Self::IO(err) => f.write_str(&format!("{err}")),
Self::Serde(err) => f.write_str(&format!("{err}")),
Self::FileTooLarge => f.write_str(FILE_TOO_LARGE),
Self::UnsupportedFileType => f.write_str(UNSUPPORTED_FILE_TYPE),
}
}
}
Expand Down
17 changes: 12 additions & 5 deletions src/handlers/serenity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,18 @@ impl SerenityHandler {
.name("play")
.description("Add a track to the queue")
.create_option(|option| {
option
.name("query")
.description("The media to play")
.kind(CommandOptionType::String)
.required(true)
option
.name("query")
.description("The media to play")
.kind(CommandOptionType::String)
.required(false)
})
.create_option(|option| {
option
.name("attachment")
.description("An audio file to play")
.kind(CommandOptionType::Attachment)
.required(false)
})
})
.create_application_command(|command| {
Expand Down
5 changes: 5 additions & 0 deletions src/messaging/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub const PLAY_TOP: &str = "📃 Added to top!";
pub const QUEUE_EXPIRED: &str =
"In order to save resources, this command has expired.\nPlease feel free to reinvoke it!";
pub const QUEUE_IS_EMPTY: &str = "Queue is empty!";
pub const QUEUE_NO_TITLE: &str = "Unknown title";
pub const QUEUE_NO_SRC: &str = "Unknown source url";
pub const QUEUE_NO_SONGS: &str = "There's no songs up next!";
pub const QUEUE_NOTHING_IS_PLAYING: &str = "Nothing is playing!";
pub const QUEUE_NOW_PLAYING: &str = "🔊 Now playing";
Expand Down Expand Up @@ -68,3 +70,6 @@ pub const TRACK_INAPPROPRIATE: &str = "⚠️ **Could not play track!**\nThe v
pub const TRACK_TIME_TO_PLAY: &str = "Estimated time until play: ";
pub const VERSION_LATEST: &str = "Find the latest version [here]";
pub const VERSION: &str = "Version";

pub const FILE_TOO_LARGE: &str = "⚠️ File is too large! Maximum size is 50 MB.";
pub const UNSUPPORTED_FILE_TYPE: &str = "⚠️ Unsupported file type! Only audio files are allowed.";
Loading
Loading