Add spotify url support for music

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/2/head
trivernis 3 years ago
parent 8a94518747
commit ca1f9e7ecf
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

217
Cargo.lock generated

@ -36,6 +36,27 @@ version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
[[package]]
name = "aspotify"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45f408f0fda4701d158664e1f25d36316808a7a2f920d7052593a28833cee649"
dependencies = [
"base64 0.13.0",
"chrono",
"futures-util",
"isocountry",
"isolanguage-1",
"itertools",
"rand 0.8.3",
"reqwest",
"serde",
"serde_json",
"serde_millis",
"tokio",
"url",
]
[[package]]
name = "async-trait"
version = "0.1.48"
@ -213,6 +234,22 @@ dependencies = [
"syn",
]
[[package]]
name = "core-foundation"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
[[package]]
name = "cpuid-bool"
version = "0.1.2"
@ -348,6 +385,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
@ -622,6 +674,19 @@ dependencies = [
"webpki",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes 1.0.1",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "idna"
version = "0.2.2"
@ -700,6 +765,25 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135"
[[package]]
name = "isocountry"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ea1dc4bf0fb4904ba83ffdb98af3d9c325274e92e6e295e4151e86c96363e04"
dependencies = [
"serde",
"thiserror",
]
[[package]]
name = "isolanguage-1"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8e2c8b6a22c1151c2e5ea3ac2da0fbe4f226bc046e43955395e19be4271b3d3"
dependencies = [
"serde",
]
[[package]]
name = "itertools"
version = "0.10.0"
@ -880,6 +964,24 @@ dependencies = [
"getrandom 0.2.2",
]
[[package]]
name = "native-tls"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
dependencies = [
"lazy_static",
"libc",
"log 0.4.14",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ntapi"
version = "0.3.6"
@ -939,6 +1041,39 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577"
dependencies = [
"bitflags 1.2.1",
"cfg-if 1.0.0",
"foreign-types",
"libc",
"once_cell",
"openssl-sys",
]
[[package]]
name = "openssl-probe"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
[[package]]
name = "openssl-sys"
version = "0.9.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "parking_lot"
version = "0.11.1"
@ -1187,6 +1322,15 @@ version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "reqwest"
version = "0.11.2"
@ -1202,12 +1346,14 @@ dependencies = [
"http-body",
"hyper",
"hyper-rustls",
"hyper-tls",
"ipnet",
"js-sys",
"lazy_static",
"log 0.4.14",
"mime",
"mime_guess",
"native-tls",
"percent-encoding",
"pin-project-lite",
"rustls",
@ -1215,6 +1361,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"url",
"wasm-bindgen",
@ -1295,6 +1442,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
"winapi 0.3.9",
]
[[package]]
name = "scoped-tls"
version = "1.0.0"
@ -1317,6 +1474,29 @@ dependencies = [
"untrusted",
]
[[package]]
name = "security-framework"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84"
dependencies = [
"bitflags 1.2.1",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.125"
@ -1348,6 +1528,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_millis"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e2dc780ca5ee2c369d1d01d100270203c4ff923d2a4264812d723766434d00"
dependencies = [
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.6"
@ -1604,6 +1793,20 @@ dependencies = [
"unicode-xid 0.0.3",
]
[[package]]
name = "tempfile"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if 1.0.0",
"libc",
"rand 0.8.3",
"redox_syscall",
"remove_dir_all",
"winapi 0.3.9",
]
[[package]]
name = "term"
version = "0.4.6"
@ -1664,9 +1867,13 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
name = "tobi-rs"
version = "0.1.0"
dependencies = [
"aspotify",
"dotenv",
"futures",
"lazy_static",
"minecraft-data-rs",
"rand 0.8.3",
"regex",
"rusqlite",
"serde",
"serde_derive",
@ -1708,6 +1915,16 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.22.0"

@ -18,4 +18,8 @@ thiserror = "1.0.24"
minecraft-data-rs = "0.2.0"
songbird = "0.1.5"
serde_json = "1.0.64"
rand = "0.8.3"
rand = "0.8.3"
regex = "1.4.5"
aspotify = "0.7.0"
lazy_static = "1.4.0"
futures = "0.3.13"

@ -23,11 +23,15 @@ 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;
mod clear;
mod current;
@ -151,40 +155,97 @@ async fn play_next_in_queue(
}
/// Returns the list of songs for a given url
async fn get_songs_for_query(ctx: &&Context, msg: &Message, query: &str) -> BotResult<Vec<Song>> {
let mut songs: Vec<Song> = get_videos_for_playlist(query)?
.into_iter()
.map(Song::from)
.collect();
if songs.len() == 0 {
let song: Song = if !query.starts_with("http") {
search_video_information(query)?
.ok_or(BotError::Msg(format!("Noting found for {}", query)))?
.into()
} else {
get_video_information(query)?.into()
};
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.thumbnail(thumb);
}
async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotResult<Vec<Song>> {
lazy_static::lazy_static! {
static ref YOUTUBE_URL_REGEX: Regex = Regex::new(r"^(https?(://))?(www\.)?(youtube\.com/watch\?.*v=.*)|(/youtu.be/.*)$").unwrap();
static ref SPOTIFY_PLAYLIST_REGEX: Regex = Regex::new(r"^(https?(://))?(www\.|open\.)?spotify\.com/playlist/.*").unwrap();
static ref SPOTIFY_ALBUM_REGEX: Regex = Regex::new(r"^(https?(://))?(www\.|open\.)?spotify\.com/album/.*").unwrap();
static ref SPOTIFY_SONG_REGEX: Regex = Regex::new(r"^(https?(://))?(www\.|open\.)?spotify\.com/track/.*").unwrap();
}
let mut songs = Vec::new();
let data = ctx.data.read().await;
let store = data.get::<Store>().unwrap();
e
})
})
.await?;
if YOUTUBE_URL_REGEX.is_match(query) {
songs = get_videos_for_playlist(query)
.await?
.into_iter()
.map(Song::from)
.collect();
if songs.len() == 0 {
let song: Song = get_video_information(query).await?.into();
added_one_msg(&ctx, msg, &song).await?;
songs.push(song);
} else {
added_multiple_msg(&ctx, msg, &mut songs).await?;
}
} else if SPOTIFY_PLAYLIST_REGEX.is_match(query) {
let song_names = store.spotify_api.get_songs_in_playlist(query).await?;
songs = parallel_search_youtube(song_names).await;
added_multiple_msg(&ctx, msg, &mut songs).await?;
} else if SPOTIFY_ALBUM_REGEX.is_match(query) {
let song_names = store.spotify_api.get_songs_in_album(query).await?;
songs = parallel_search_youtube(song_names).await;
added_multiple_msg(&ctx, msg, &mut songs).await?;
} else if SPOTIFY_SONG_REGEX.is_match(query) {
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?;
songs.push(song);
} else {
msg.channel_id
.send_message(&ctx.http, |m| {
m.embed(|e| e.description(format!("Added {} songs to the queue", songs.len())))
})
.await?;
let 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?;
songs.push(song);
}
Ok(songs)
}
/// Searches songs on youtube in parallel
async fn parallel_search_youtube(song_names: Vec<String>) -> Vec<Song> {
let search_futures: Vec<BoxFuture<BotResult<Option<VideoInformation>>>> = song_names
.into_iter()
.map(|s| search_video_information(s).boxed())
.collect();
let information: Vec<BotResult<Option<VideoInformation>>> =
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<()> {
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.thumbnail(thumb);
}
e
})
})
.await?;
Ok(())
}
/// Message when multiple songs were added to the queue
async fn added_multiple_msg(ctx: &Context, msg: &Message, songs: &mut Vec<Song>) -> BotResult<()> {
msg.channel_id
.send_message(&ctx.http, |m| {
m.embed(|e| e.description(format!("Added {} songs to the queue", songs.len())))
})
.await?;
Ok(())
}

@ -1,21 +1,17 @@
use std::io::Read;
use std::process::{Command, Stdio};
use crate::providers::music::responses::{PlaylistEntry, VideoInformation};
use crate::utils::error::BotResult;
use std::process::Stdio;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
pub(crate) mod queue;
pub(crate) mod responses;
pub(crate) mod spotify;
/// Returns a list of youtube videos for a given url
pub(crate) fn get_videos_for_playlist(url: &str) -> BotResult<Vec<PlaylistEntry>> {
let ytdl = Command::new("youtube-dl")
.args(&["--no-warnings", "--flat-playlist", "--dump-json", "-i", url])
.stdout(Stdio::piped())
.spawn()?;
let mut output = String::new();
ytdl.stdout.unwrap().read_to_string(&mut output)?;
pub(crate) async fn get_videos_for_playlist(url: &str) -> BotResult<Vec<PlaylistEntry>> {
let output =
youtube_dl(&["--no-warnings", "--flat-playlist", "--dump-json", "-i", url]).await?;
let videos = output
.lines()
@ -26,30 +22,36 @@ pub(crate) fn get_videos_for_playlist(url: &str) -> BotResult<Vec<PlaylistEntry>
}
/// Returns information for a single video by using youtube-dl
pub(crate) fn get_video_information(url: &str) -> BotResult<VideoInformation> {
let ytdl = Command::new("youtube-dl")
.args(&["--no-warnings", "--dump-json", "-i", url])
.stdout(Stdio::piped())
.spawn()?;
pub(crate) async fn get_video_information(url: &str) -> BotResult<VideoInformation> {
let output = youtube_dl(&["--no-warnings", "--dump-json", "-i", url]).await?;
let information = serde_json::from_reader(ytdl.stdout.unwrap())?;
let information = serde_json::from_str(&*output)?;
Ok(information)
}
/// Searches for a video
pub(crate) fn search_video_information(query: &str) -> BotResult<Option<VideoInformation>> {
pub(crate) async fn search_video_information(query: String) -> BotResult<Option<VideoInformation>> {
let output = youtube_dl(&[
"--no-warnings",
"--dump-json",
"-i",
format!("ytsearch:\"{}\"", query).as_str(),
])
.await?;
let information = serde_json::from_str(&*output)?;
Ok(information)
}
/// Executes youtube-dl asynchronously
async fn youtube_dl(args: &[&str]) -> BotResult<String> {
let ytdl = Command::new("youtube-dl")
.args(&[
"--no-warnings",
"--dump-json",
"-i",
format!("ytsearch:\"{}\"", query).as_str(),
])
.args(args)
.stdout(Stdio::piped())
.spawn()?;
let mut output = String::new();
ytdl.stdout.unwrap().read_to_string(&mut output).await?;
let information = serde_json::from_reader(ytdl.stdout.unwrap())?;
Ok(information)
Ok(output)
}

@ -0,0 +1,100 @@
use crate::utils::error::{BotError, BotResult};
use aspotify::{Client, ClientCredentials, PlaylistItemType};
pub struct SpotifyApi {
client: Client,
}
impl SpotifyApi {
/// Creates a new spotify api wrapper with the credentials stored
/// in the .env files
pub fn new() -> Self {
let credentials = ClientCredentials {
id: dotenv::var("SPOTIFY_CLIENT_ID").expect("Missing Spotify Credentials"),
secret: dotenv::var("SPOTIFY_CLIENT_SECRET").expect("Missing Spotify Credentials"),
};
let client = Client::new(credentials);
Self { client }
}
/// Returns the song names for a playlist
pub async fn get_songs_in_playlist(&self, url: &str) -> BotResult<Vec<String>> {
let id = self.get_id_for_url(url)?;
let playlist = self.client.playlists().get_playlist(&*id, None).await?.data;
let song_names = playlist
.tracks
.items
.into_iter()
.filter_map(|item| item.item)
.map(|t| match t {
PlaylistItemType::Track(t) => format!(
"{} - {}",
t.artists
.into_iter()
.map(|a| a.name)
.collect::<Vec<String>>()
.join(","),
t.name
),
PlaylistItemType::Episode(e) => e.name,
})
.collect();
Ok(song_names)
}
/// Returns all song names for a given album
pub async fn get_songs_in_album(&self, url: &str) -> BotResult<Vec<String>> {
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::<Vec<String>>()
.join(","),
item.name
)
})
.collect();
Ok(song_names)
}
/// Returns the name for a spotify song url
pub async fn get_song_name(&self, url: &str) -> BotResult<String> {
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::<Vec<String>>()
.join(","),
track.name
))
}
/// Returns the id for a given spotify URL
fn get_id_for_url(&self, url: &str) -> BotResult<String> {
url.split('/')
.last()
.ok_or(BotError::from("Invalid Spotify URL"))
.and_then(|s| {
s.split('?')
.next()
.ok_or(BotError::from("Invalid Spotify URL"))
})
.map(|s| s.to_string())
}
}

@ -22,6 +22,9 @@ pub enum BotError {
#[error("JSON Error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Spotify API Error: {0}")]
SpotifyError(#[from] aspotify::Error),
#[error("{0}")]
Msg(String),
}

@ -7,6 +7,7 @@ use tokio::sync::Mutex;
use crate::database::Database;
use crate::providers::music::queue::MusicQueue;
use crate::providers::music::spotify::SpotifyApi;
pub struct Store;
@ -14,6 +15,7 @@ pub struct StoreData {
pub database: Arc<Mutex<Database>>,
pub minecraft_data_api: minecraft_data_rs::api::Api,
pub music_queues: HashMap<GuildId, Arc<Mutex<MusicQueue>>>,
pub spotify_api: SpotifyApi,
}
impl StoreData {
@ -24,6 +26,7 @@ impl StoreData {
minecraft_data_rs::api::versions::latest_stable().unwrap(),
),
music_queues: HashMap::new(),
spotify_api: SpotifyApi::new(),
}
}
}

Loading…
Cancel
Save