diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs index 199c7ae..04b8bb7 100644 --- a/src/commands/music/mod.rs +++ b/src/commands/music/mod.rs @@ -23,15 +23,13 @@ use shuffle::SHUFFLE_COMMAND; use skip::SKIP_COMMAND; use crate::providers::music::queue::{MusicQueue, Song}; -use crate::providers::music::responses::VideoInformation; use crate::providers::music::{ get_video_information, get_videos_for_playlist, search_video_information, }; use crate::utils::error::{BotError, BotResult}; use crate::utils::store::Store; -use futures::future::BoxFuture; -use futures::FutureExt; use regex::Regex; +use std::time::Duration; mod clear; mod current; @@ -121,7 +119,9 @@ struct SongEndNotifier { #[async_trait] impl VoiceEventHandler for SongEndNotifier { async fn act(&self, _ctx: &EventContext<'_>) -> Option { - play_next_in_queue(&self.http, &self.channel_id, &self.queue, &self.handler).await; + while !play_next_in_queue(&self.http, &self.channel_id, &self.queue, &self.handler).await { + tokio::time::sleep(Duration::from_millis(100)).await; + } None } @@ -133,17 +133,29 @@ async fn play_next_in_queue( channel_id: &ChannelId, queue: &Arc>, handler: &Arc>, -) { +) -> bool { let mut queue_lock = queue.lock().await; - if let Some(next) = queue_lock.next() { - let source = match songbird::ytdl(&next.url).await { + if let Some(mut next) = queue_lock.next() { + let url = match next.url().await { + Some(url) => url, + None => { + let _ = channel_id + .say(&http, format!("'{}' not found", next.title())) + .await; + return false; + } + }; + let source = match songbird::ytdl(&url).await { Ok(s) => s, Err(e) => { let _ = channel_id - .say(&http, format!("Failed to enqueue {}: {:?}", next.title, e)) + .say( + &http, + format!("Failed to enqueue {}: {:?}", next.title(), e), + ) .await; - return; + return false; } }; let mut handler_lock = handler.lock().await; @@ -152,6 +164,7 @@ async fn play_next_in_queue( } else { queue_lock.clear_current(); } + true } /// Returns the list of songs for a given url @@ -177,65 +190,46 @@ async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotRe // if no songs were found fetch the song as a video if songs.len() == 0 { - let song: Song = get_video_information(query).await?.into(); - added_one_msg(&ctx, msg, &song).await?; + let mut song: Song = get_video_information(query).await?.into(); + added_one_msg(&ctx, msg, &mut song).await?; songs.push(song); } else { added_multiple_msg(&ctx, msg, &mut songs).await?; } } else if SPOTIFY_PLAYLIST_REGEX.is_match(query) { // search for all songs in the playlist and search for them on youtube - let song_names = store.spotify_api.get_songs_in_playlist(query).await?; - songs = parallel_search_youtube(song_names).await; + songs = store.spotify_api.get_songs_in_playlist(query).await?; added_multiple_msg(&ctx, msg, &mut songs).await?; } else if SPOTIFY_ALBUM_REGEX.is_match(query) { // fetch all songs in the album and search for them on youtube - let song_names = store.spotify_api.get_songs_in_album(query).await?; - songs = parallel_search_youtube(song_names).await; + songs = store.spotify_api.get_songs_in_album(query).await?; added_multiple_msg(&ctx, msg, &mut songs).await?; } else if SPOTIFY_SONG_REGEX.is_match(query) { // fetch the song name and search it on youtube - let name = store.spotify_api.get_song_name(query).await?; - let song: Song = search_video_information(name.clone()) - .await? - .ok_or(BotError::Msg(format!("Noting found for {}", name)))? - .into(); - added_one_msg(ctx, msg, &song).await?; + let mut song = store.spotify_api.get_song_name(query).await?; + added_one_msg(ctx, msg, &mut song).await?; songs.push(song); } else { - let song: Song = search_video_information(query.to_string()) + let mut song: Song = search_video_information(query.to_string()) .await? .ok_or(BotError::Msg(format!("Noting found for {}", query)))? .into(); - added_one_msg(&ctx, msg, &song).await?; + added_one_msg(&ctx, msg, &mut song).await?; songs.push(song); } Ok(songs) } -/// Searches songs on youtube in parallel -async fn parallel_search_youtube(song_names: Vec) -> Vec { - let search_futures: Vec>>> = song_names - .into_iter() - .map(|s| search_video_information(s).boxed()) - .collect(); - let information: Vec>> = - futures::future::join_all(search_futures).await; - information - .into_iter() - .filter_map(|i| i.ok().and_then(|s| s).map(Song::from)) - .collect() -} - /// Message when one song was added to the queue -async fn added_one_msg(ctx: &Context, msg: &Message, song: &Song) -> BotResult<()> { +async fn added_one_msg(ctx: &Context, msg: &Message, song: &mut Song) -> BotResult<()> { + let url = song.url().await.ok_or(BotError::from("Song not found"))?; msg.channel_id .send_message(&ctx.http, |m| { m.embed(|mut e| { - e = e.description(format!("Added [{}]({}) to the queue", song.title, song.url)); - if let Some(thumb) = &song.thumbnail { + e = e.description(format!("Added [{}]({}) to the queue", song.title(), url)); + if let Some(thumb) = &song.thumbnail() { e = e.thumbnail(thumb); } diff --git a/src/commands/music/queue.rs b/src/commands/music/queue.rs index fc355da..6a04c6d 100644 --- a/src/commands/music/queue.rs +++ b/src/commands/music/queue.rs @@ -1,8 +1,8 @@ use std::cmp::min; use serenity::client::Context; -use serenity::framework::standard::CommandResult; use serenity::framework::standard::macros::command; +use serenity::framework::standard::CommandResult; use serenity::model::channel::Message; use crate::commands::music::get_queue_for_guild; @@ -20,7 +20,7 @@ async fn queue(ctx: &Context, msg: &Message) -> CommandResult { let songs: Vec<(usize, String)> = queue_lock .entries() .into_iter() - .map(|s| s.title.clone()) + .map(|s| s.title().clone()) .enumerate() .collect(); diff --git a/src/providers/music/mod.rs b/src/providers/music/mod.rs index 8705a51..97a3d87 100644 --- a/src/providers/music/mod.rs +++ b/src/providers/music/mod.rs @@ -1,5 +1,8 @@ +use crate::providers::music::queue::Song; use crate::providers::music::responses::{PlaylistEntry, VideoInformation}; use crate::utils::error::BotResult; +use futures::future::BoxFuture; +use futures::FutureExt; use std::process::Stdio; use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::Arc; @@ -49,6 +52,21 @@ pub(crate) async fn search_video_information(query: String) -> BotResult) -> Vec { + let search_futures: Vec>>> = song_names + .into_iter() + .map(|s| search_video_information(s).boxed()) + .collect(); + let information: Vec>> = + futures::future::join_all(search_futures).await; + information + .into_iter() + .filter_map(|i| i.ok().and_then(|s| s).map(Song::from)) + .collect() +} + /// Executes youtube-dl asynchronously /// An atomic U8 is used to control the number of parallel processes /// to avoid using too much memory diff --git a/src/providers/music/queue.rs b/src/providers/music/queue.rs index 7435ed6..5044d02 100644 --- a/src/providers/music/queue.rs +++ b/src/providers/music/queue.rs @@ -3,7 +3,9 @@ use std::collections::VecDeque; use songbird::tracks::TrackHandle; use crate::providers::music::responses::{PlaylistEntry, VideoInformation}; +use crate::providers::music::search_video_information; use crate::utils::shuffle_vec_deque; +use aspotify::{Track, TrackSimplified}; #[derive(Clone, Debug)] pub struct MusicQueue { @@ -67,16 +69,52 @@ impl MusicQueue { #[derive(Clone, Debug)] pub struct Song { - pub url: String, - pub title: String, - pub author: String, - pub thumbnail: Option, + url: Option, + title: String, + author: String, + thumbnail: Option, +} + +impl Song { + /// The url of the song + /// fetched when not available + pub async fn url(&mut self) -> Option { + if let Some(url) = self.url.clone() { + Some(url) + } else { + let information = search_video_information(format!("{} - {}", self.author, self.title)) + .await + .ok() + .and_then(|i| i)?; + self.url = Some(information.webpage_url.clone()); + self.thumbnail = information.thumbnail; + self.author = information.uploader; + + Some(information.webpage_url) + } + } + + /// The title of the song + pub fn title(&self) -> &String { + &self.title + } + + #[allow(dead_code)] + /// the author of the song + pub fn author(&self) -> &String { + &self.author + } + + /// The thumbnail of the song + pub fn thumbnail(&self) -> &Option { + &self.thumbnail + } } impl From for Song { fn from(info: VideoInformation) -> Self { Self { - url: info.webpage_url, + url: Some(info.webpage_url), title: info.title, author: info.uploader, thumbnail: info.thumbnail, @@ -87,10 +125,42 @@ impl From for Song { impl From for Song { fn from(entry: PlaylistEntry) -> Self { Self { - url: format!("https://www.youtube.com/watch?v={}", entry.url), + url: Some(format!("https://www.youtube.com/watch?v={}", entry.url)), title: entry.title, author: entry.uploader, thumbnail: None, } } } + +impl From for Song { + fn from(track: Track) -> Self { + Self { + title: track.name, + author: track + .artists + .into_iter() + .map(|a| a.name) + .collect::>() + .join(" & "), + url: None, + thumbnail: None, + } + } +} + +impl From for Song { + fn from(track: TrackSimplified) -> Self { + Self { + title: track.name, + author: track + .artists + .into_iter() + .map(|a| a.name) + .collect::>() + .join(" & "), + url: None, + thumbnail: None, + } + } +} diff --git a/src/providers/music/spotify.rs b/src/providers/music/spotify.rs index 4a6d922..46a83c4 100644 --- a/src/providers/music/spotify.rs +++ b/src/providers/music/spotify.rs @@ -1,3 +1,4 @@ +use crate::providers::music::queue::Song; use crate::utils::error::{BotError, BotResult}; use aspotify::{Client, ClientCredentials, PlaylistItem, PlaylistItemType}; @@ -19,7 +20,7 @@ impl SpotifyApi { } /// Returns the song names for a playlist - pub async fn get_songs_in_playlist(&self, url: &str) -> BotResult> { + pub async fn get_songs_in_playlist(&self, url: &str) -> BotResult> { let id = self.get_id_for_url(url)?; let mut playlist_tracks = Vec::new(); let mut offset = 0; @@ -36,17 +37,9 @@ impl SpotifyApi { let song_names = playlist_tracks .into_iter() .filter_map(|item| item.item) - .map(|t| match t { - PlaylistItemType::Track(t) => format!( - "{} - {}", - t.artists - .into_iter() - .map(|a| a.name) - .collect::>() - .join(" & "), - t.name - ), - PlaylistItemType::Episode(e) => e.name, + .filter_map(|t| match t { + PlaylistItemType::Track(t) => Some(Song::from(t)), + PlaylistItemType::Episode(_) => None, }) .collect(); @@ -71,44 +64,20 @@ impl SpotifyApi { } /// Returns all song names for a given album - pub async fn get_songs_in_album(&self, url: &str) -> BotResult> { + pub async fn get_songs_in_album(&self, url: &str) -> BotResult> { let id = self.get_id_for_url(url)?; let album = self.client.albums().get_album(&*id, None).await?.data; - let song_names = album - .tracks - .items - .into_iter() - .map(|item| { - format!( - "{} - {}", - item.artists - .into_iter() - .map(|a| a.name) - .collect::>() - .join(" & "), - item.name - ) - }) - .collect(); + let song_names = album.tracks.items.into_iter().map(Song::from).collect(); Ok(song_names) } /// Returns the name for a spotify song url - pub async fn get_song_name(&self, url: &str) -> BotResult { + pub async fn get_song_name(&self, url: &str) -> BotResult { let id = self.get_id_for_url(url)?; let track = self.client.tracks().get_track(&*id, None).await?.data; - Ok(format!( - "{} - {}", - track - .artists - .into_iter() - .map(|a| a.name) - .collect::>() - .join(" & "), - track.name - )) + Ok(track.into()) } /// Returns the id for a given spotify URL