Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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

[dependencies.songbird]
version = "0.3.2"
Expand Down
184 changes: 133 additions & 51 deletions src/commands/play.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ use crate::{
errors::{verify, ParrotError},
guild::settings::{GuildSettings, GuildSettingsMap},
handlers::track_end::update_queue_messages,
messaging::message::ParrotMessage,
messaging::messages::{
PLAY_QUEUE, PLAY_TOP, SPOTIFY_AUTH_FAILED, TRACK_DURATION, TRACK_TIME_TO_PLAY,
},
messaging::{
message::ParrotMessage,
messages::{QUEUE_NO_SRC, QUEUE_NO_TITLE},
},
sources::{
file::{FileRestartable, FileSource},
spotify::{Spotify, SPOTIFY},
youtube::{YouTube, YouTubeRestartable},
},
Expand All @@ -17,8 +21,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,58 +43,29 @@ pub enum Mode {
Jump,
}

#[derive(Clone)]
#[derive(Clone, Debug)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we will change this once it is "ready"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually usually leave the Debug derive in because it provides a certain level of detail in logs, but I'll remove it if it's not idiomatic to have in production code.

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("cdn.discordapp.com") => Some(QueryType::extract()),
Some(other) => {
let mut data = ctx.data.write().await;
let settings = data.get_mut::<GuildSettingsMap>().unwrap();
Expand All @@ -112,12 +91,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,18 +117,86 @@ 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),
};

println!("options: {:?}", interaction.data.options);

let arg = interaction
.data
.options
.get(idx)
.expect("Expected attachment or query option")
.clone();

let option = arg
.resolved
.as_ref()
.expect("Expected attachment or query object");

let (url, attachment) = match option {
CommandDataOptionValue::Attachment(attachment) => {
println!(
"Attachment name: {}, attachment size: {}",
attachment.filename, attachment.size
);
(None, Some(attachment))
}
CommandDataOptionValue::String(url) => {
println!("UrlOrQuery: {}", 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 => FileSource::extract(attachment.unwrap().clone()),
};

let query_type = verify(
query_type,
ParrotError::Other("Something went wrong while parsing your query!"),
)?;

println!("query_type: {:?}", query_type);

// reply with a temporary message while we fetch the source
// needed because interactions must be replied within 3s and queueing takes longer
create_response(&ctx.http, interaction, ParrotMessage::Search).await?;
Expand All @@ -169,7 +217,9 @@ pub async fn play(
.ok_or(ParrotError::Other("failed to fetch playlist"))?;

for url in urls.iter() {
let Ok(queue) = enqueue_track(&call, &QueryType::VideoLink(url.to_string())).await else {
let Ok(queue) =
enqueue_track(&call, &QueryType::VideoLink(url.to_string())).await
else {
continue;
};
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
Expand All @@ -182,6 +232,13 @@ pub async fn play(
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
}
QueryType::File(attachment) => {
println!("attachment: {:?}", attachment);
let query_type = FileSource::extract(attachment)
.ok_or(ParrotError::Other("failed to load 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 @@ -194,7 +251,8 @@ pub async fn play(
.ok_or(ParrotError::Other("failed to fetch playlist"))?;

for (idx, url) in urls.into_iter().enumerate() {
let Ok(queue) = insert_track(&call, &QueryType::VideoLink(url), idx + 1).await else {
let Ok(queue) = insert_track(&call, &QueryType::VideoLink(url), idx + 1).await
else {
continue;
};
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
Expand All @@ -207,6 +265,12 @@ pub async fn play(
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
}
QueryType::File(attachment) => {
let query_type = FileSource::extract(attachment)
.ok_or(ParrotError::Other("failed to load 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 All @@ -227,7 +291,9 @@ pub async fn play(
let mut insert_idx = 1;

for (i, url) in urls.into_iter().enumerate() {
let Ok(mut queue) = insert_track(&call, &QueryType::VideoLink(url), insert_idx).await else {
let Ok(mut queue) =
insert_track(&call, &QueryType::VideoLink(url), insert_idx).await
else {
continue;
};

Expand Down Expand Up @@ -256,6 +322,12 @@ pub async fn play(
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
}
QueryType::File(attachment) => {
let query_type = FileSource::extract(attachment)
.ok_or(ParrotError::Other("failed to load 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 @@ -276,6 +348,12 @@ pub async fn play(
update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await;
}
}
QueryType::File(attachment) => {
let query_type = FileSource::extract(attachment)
.ok_or(ParrotError::Other("failed to load 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 @@ -366,14 +444,14 @@ async fn create_queued_embed(
let mut embed = CreateEmbed::default();
let metadata = track.metadata().clone();

embed.thumbnail(&metadata.thumbnail.unwrap());
embed.thumbnail(&metadata.thumbnail.unwrap_or_default());

embed.field(
title,
&format!(
"[**{}**]({})",
metadata.title.unwrap(),
metadata.source_url.unwrap()
metadata.title.unwrap_or(QUEUE_NO_TITLE.to_string()),
metadata.source_url.unwrap_or(QUEUE_NO_SRC.to_string())
),
false,
);
Expand All @@ -400,6 +478,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
18 changes: 13 additions & 5 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,20 @@ 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.as_ref().unwrap());
if metadata.thumbnail.is_some() {
embed.thumbnail(metadata.thumbnail.as_ref().unwrap());
}

format!(
"[{}]({}) • `{}`",
metadata.title.as_ref().unwrap(),
metadata.source_url.as_ref().unwrap(),
metadata
.title
.as_ref()
.unwrap_or(&String::from(QUEUE_NO_TITLE)),
metadata
.source_url
.as_ref()
.unwrap_or(&String::from(QUEUE_NO_SRC)),
get_human_readable_timestamp(metadata.duration)
)
} else {
Expand Down
Loading