Merge pull request #15 from Trivernis/develop

Develop
pull/16/head
Trivernis 3 years ago committed by GitHub
commit e2f9027fb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -39,6 +39,10 @@ jobs:
with:
name: tobi-rs-linux-x86_64
path: target/tobi-rs-linux-x86_64
- name: Upload to ipfs
uses: aquiladev/ipfs-action@v1
with:
path: target/tobi-rs-linux-x86_64
- name: publish release
uses: "marvinpinto/action-automatic-releases@latest"
with:

15
Cargo.lock generated

@ -193,7 +193,7 @@ dependencies = [
[[package]]
name = "bot-database"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"chrono",
"diesel",
@ -2318,7 +2318,7 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tobi-rs"
version = "0.5.3"
version = "0.6.0"
dependencies = [
"aspotify",
"bot-coreutils",
@ -2346,6 +2346,7 @@ dependencies = [
"sysinfo",
"thiserror",
"tokio",
"trigram",
]
[[package]]
@ -2474,6 +2475,16 @@ dependencies = [
"tracing",
]
[[package]]
name = "trigram"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "324350103581dfc6869f2c9399c019f394ac49a7ab16af23206add07851c5347"
dependencies = [
"lazy_static",
"regex",
]
[[package]]
name = "try-lock"
version = "0.2.3"

@ -1,6 +1,6 @@
[package]
name = "tobi-rs"
version = "0.5.3"
version = "0.6.0"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"
@ -35,4 +35,5 @@ sysinfo = "0.16.5"
reqwest = "0.11.2"
chrono-tz = "0.5.3"
sauce-api = "0.7.1"
rustc_version_runtime = "0.2.0"
rustc_version_runtime = "0.2.0"
trigram = "0.4.4"

@ -23,6 +23,20 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bot-database"
version = "0.4.0"
dependencies = [
"chrono",
"diesel",
"diesel_migrations",
"dotenv",
"log",
"r2d2",
"thiserror",
"tokio-diesel",
]
[[package]]
name = "byteorder"
version = "1.4.3"
@ -48,20 +62,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "database"
version = "0.1.0"
dependencies = [
"chrono",
"diesel",
"diesel_migrations",
"dotenv",
"log",
"r2d2",
"thiserror",
"tokio-diesel",
]
[[package]]
name = "diesel"
version = "1.4.6"

@ -1,6 +1,6 @@
[package]
name = "bot-database"
version = "0.3.0"
version = "0.4.0"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE youtube_songs;

@ -0,0 +1,11 @@
-- Your SQL goes here
CREATE TABLE youtube_songs (
id BIGSERIAL PRIMARY KEY,
spotify_id VARCHAR(255) NOT NULL,
artist VARCHAR(128) NOT NULL,
title VARCHAR(255) NOT NULL,
album VARCHAR(255) NOT NULL,
url VARCHAR(128) NOT NULL,
score INTEGER DEFAULT 0 NOT NULL,
UNIQUE (spotify_id, url)
)

@ -2,6 +2,7 @@ pub use gifs::*;
pub use guild_playlists::*;
pub use guild_playlists::*;
pub use statistics::*;
pub use youtube_songs::*;
use crate::PoolConnection;
@ -9,6 +10,7 @@ mod gifs;
mod guild_playlists;
mod guild_settings;
mod statistics;
mod youtube_songs;
#[derive(Clone)]
pub struct Database {

@ -0,0 +1,60 @@
use diesel::insert_into;
use diesel::prelude::*;
use tokio_diesel::*;
use crate::error::DatabaseResult;
use crate::models::*;
use crate::schema::*;
use crate::Database;
impl Database {
/// Adds a song to the database or increments the score when it
/// already exists
pub async fn add_song(
&self,
spotify_id: &str,
artist: &str,
title: &str,
album: &str,
url: &str,
) -> DatabaseResult<()> {
use youtube_songs::dsl;
log::debug!(
"Inserting/Updating song in database spotify_id: '{}' artist: '{}', title: '{}', album: '{}', url: '{}'",
spotify_id,
artist,
title,
album,
url,
);
insert_into(dsl::youtube_songs)
.values(YoutubeSongInsert {
spotify_id: spotify_id.to_string(),
artist: artist.to_string(),
title: title.to_string(),
album: album.to_string(),
url: url.to_string(),
})
.on_conflict((dsl::spotify_id, dsl::url))
.do_update()
.set(dsl::score.eq(dsl::score + 1))
.execute_async(&self.pool)
.await?;
Ok(())
}
/// Returns the song with the best score for the given query
pub async fn get_song(&self, spotify_id: &str) -> DatabaseResult<Option<YoutubeSong>> {
use youtube_songs::dsl;
let songs: Vec<YoutubeSong> = dsl::youtube_songs
.filter(dsl::spotify_id.eq(spotify_id))
.order(dsl::score.desc())
.limit(1)
.load_async::<YoutubeSong>(&self.pool)
.await?;
Ok(songs.into_iter().next())
}
}

@ -56,3 +56,24 @@ pub struct StatisticInsert {
pub success: bool,
pub error_msg: Option<String>,
}
#[derive(Queryable, Debug, Clone)]
pub struct YoutubeSong {
pub id: i64,
pub spotify_id: String,
pub artist: String,
pub title: String,
pub album: String,
pub url: String,
pub score: i32,
}
#[derive(Insertable, Debug)]
#[table_name = "youtube_songs"]
pub struct YoutubeSongInsert {
pub spotify_id: String,
pub artist: String,
pub title: String,
pub album: String,
pub url: String,
}

@ -34,9 +34,22 @@ table! {
}
}
table! {
youtube_songs (id) {
id -> Int8,
spotify_id -> Varchar,
artist -> Varchar,
title -> Varchar,
album -> Varchar,
url -> Varchar,
score -> Int4,
}
}
allow_tables_to_appear_in_same_query!(
gifs,
guild_playlists,
guild_settings,
statistics,
youtube_songs,
);

@ -30,7 +30,7 @@ async fn current(ctx: &Context, msg: &Message) -> CommandResult {
queue_lock.current().clone()
};
if let Some(current) = current {
if let Some((current, _)) = current {
let metadata = current.metadata().clone();
log::trace!("Metadata is {:?}", metadata);
let np_msg = create_now_playing_msg(ctx, queue.clone(), msg.channel_id).await?;

@ -41,7 +41,7 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
}
if manager.get(guild.id).is_some() {
if let Some(current) = queue_lock.current() {
if let Some((current, _)) = queue_lock.current() {
current.stop()?;
}
manager.remove(guild.id).await?;

@ -23,7 +23,7 @@ async fn lyrics(ctx: &Context, msg: &Message) -> CommandResult {
);
let queue_lock = queue.lock().await;
if let Some(current) = queue_lock.current() {
if let Some((current, _)) = queue_lock.current() {
log::debug!("Playing music. Fetching lyrics for currently playing song...");
let metadata = current.metadata();
let title = metadata.title.clone().unwrap();

@ -35,10 +35,12 @@ use skip::SKIP_COMMAND;
use crate::messages::music::now_playing::update_now_playing_msg;
use crate::providers::music::queue::{MusicQueue, Song};
use crate::providers::music::youtube_dl;
use crate::providers::music::{add_youtube_song_to_database, youtube_dl};
use crate::providers::settings::{get_setting, Setting};
use crate::utils::context_data::{DatabaseContainer, Store};
use crate::utils::error::{BotError, BotResult};
use aspotify::Track;
use bot_database::Database;
mod clear_queue;
mod current;
@ -123,7 +125,7 @@ impl VoiceEventHandler for ChannelDurationNotifier {
let mut handler_lock = self.handler.lock().await;
handler_lock.remove_all_global_events();
}
if let Some(current) = queue_lock.current() {
if let Some((current, _)) = queue_lock.current() {
let _ = current.stop();
}
let _ = self.manager.remove(self.guild_id).await;
@ -268,7 +270,7 @@ async fn play_next_in_queue(
log::error!("Failed to update now playing message: {:?}", e);
}
}
queue_lock.set_current(track);
queue_lock.set_current(track, next);
} else {
if let Some(np) = mem::take(&mut queue_lock.now_playing_msg) {
let np = np.read().await;
@ -327,6 +329,7 @@ async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotRe
log::debug!("Query is youtube video");
let mut song: Song = youtube_dl::get_video_information(&query).await?.into();
added_one_msg(&ctx, msg, &mut song).await?;
add_youtube_song_to_database(&store, &database, &mut song).await?;
songs.push(song);
} else {
log::debug!("Query is playlist with {} songs", songs.len());
@ -335,17 +338,38 @@ async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotRe
} else if SPOTIFY_PLAYLIST_REGEX.is_match(&query) {
// search for all songs in the playlist and search for them on youtube
log::debug!("Query is spotify playlist");
songs = store.spotify_api.get_songs_in_playlist(&query).await?;
let tracks = store.spotify_api.get_songs_in_playlist(&query).await?;
for track in tracks {
songs.push(
get_youtube_song_for_track(&database, track.clone())
.await?
.unwrap_or(track.into()),
)
}
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
log::debug!("Query is spotify album");
songs = store.spotify_api.get_songs_in_album(&query).await?;
let tracks = store.spotify_api.get_songs_in_album(&query).await?;
for track in tracks {
songs.push(
get_youtube_song_for_track(&database, track.clone())
.await?
.unwrap_or(track.into()),
)
}
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
log::debug!("Query is a spotify song");
let mut song = store.spotify_api.get_song_name(&query).await?;
let track = store.spotify_api.get_track_for_url(&query).await?;
let mut song = get_youtube_song_for_track(&database, track.clone())
.await?
.unwrap_or(track.into());
added_one_msg(ctx, msg, &mut song).await?;
songs.push(song);
} else {
@ -409,3 +433,16 @@ pub async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult<bool
Ok(true)
}
}
/// Searches for a matching youtube song for the given track in the local database
async fn get_youtube_song_for_track(database: &Database, track: Track) -> BotResult<Option<Song>> {
log::debug!("Trying to find track in database.");
if let Some(id) = track.id {
let entry = database.get_song(&id).await?;
log::trace!("Found entry is {:?}", entry);
Ok(entry.map(Song::from))
} else {
log::debug!("Track has no ID");
Ok(None)
}
}

@ -37,7 +37,8 @@ async fn pause(ctx: &Context, msg: &Message) -> CommandResult {
m.content("⏸️ Paused playback")
})
.await?;
if let (Some(menu), Some(current)) = (&queue_lock.now_playing_msg, queue_lock.current())
if let (Some(menu), Some((current, _))) =
(&queue_lock.now_playing_msg, queue_lock.current())
{
update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?;
}
@ -47,7 +48,8 @@ async fn pause(ctx: &Context, msg: &Message) -> CommandResult {
m.content("▶ Resumed playback")
})
.await?;
if let (Some(menu), Some(current)) = (&queue_lock.now_playing_msg, queue_lock.current())
if let (Some(menu), Some((current, _))) =
(&queue_lock.now_playing_msg, queue_lock.current())
{
update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?;
}

@ -28,7 +28,7 @@ async fn skip(ctx: &Context, msg: &Message) -> CommandResult {
);
let queue_lock = queue.lock().await;
if let Some(current) = queue_lock.current() {
if let Some((current, _)) = queue_lock.current() {
current.stop()?;
}

@ -4,12 +4,12 @@ use crate::utils::logging::init_logger;
#[macro_use]
extern crate bot_serenityutils;
pub mod client;
mod client;
mod commands;
pub mod handler;
mod handler;
mod messages;
mod providers;
pub mod utils;
mod utils;
pub static VERSION: &str = env!("CARGO_PKG_VERSION");

@ -6,7 +6,9 @@ use serenity::model::prelude::ChannelId;
use songbird::input::Metadata;
use crate::commands::music::{get_queue_for_guild, get_voice_manager, is_dj};
use crate::providers::music::add_youtube_song_to_database;
use crate::providers::music::queue::MusicQueue;
use crate::utils::context_data::{DatabaseContainer, Store};
use crate::utils::error::*;
use bot_serenityutils::core::MessageHandle;
use bot_serenityutils::error::SerenityUtilsResult;
@ -21,6 +23,7 @@ use tokio::sync::{Mutex, RwLock};
static PAUSE_BUTTON: &str = "⏯️";
static SKIP_BUTTON: &str = "⏭️";
static STOP_BUTTON: &str = "⏹️";
static GOOD_PICK_BUTTON: &str = "👍";
/// Creates a new now playing message and returns the embed for that message
pub async fn create_now_playing_msg(
@ -39,6 +42,9 @@ pub async fn create_now_playing_msg(
.add_control(2, SKIP_BUTTON, |c, m, r| {
Box::pin(skip_button_action(c, m, r))
})
.add_control(3, GOOD_PICK_BUTTON, |c, m, r| {
Box::pin(good_pick_action(c, m, r))
})
.add_page(Page::new_builder(move || {
let queue = Arc::clone(&queue);
Box::pin(async move {
@ -47,7 +53,7 @@ pub async fn create_now_playing_msg(
log::debug!("Queue locked");
let mut page = CreateMessage::default();
if let Some(current) = queue.current() {
if let Some((current, _)) = queue.current() {
page.embed(|e| create_now_playing_embed(current.metadata(), e, queue.paused()));
} else {
page.embed(|e| e.description("Queue is empty"));
@ -142,7 +148,7 @@ async fn play_pause_button_action(
};
log::debug!("Queue is unlocked");
if let Some(current) = current {
if let Some((current, _)) = current {
update_now_playing_msg(&ctx.http, &message, current.metadata(), paused).await?;
}
}
@ -169,7 +175,7 @@ async fn skip_button_action(
queue.current().clone()
};
if let Some(current) = current {
if let Some((current, _)) = current {
let _ = current.stop();
}
}
@ -201,7 +207,7 @@ async fn stop_button_action(
handler_lock.remove_all_global_events();
}
if let Some(current) = queue.current() {
current.stop().map_err(BotError::from)?;
current.0.stop().map_err(BotError::from)?;
}
if manager.get(guild_id).is_some() {
@ -221,3 +227,22 @@ async fn stop_button_action(
Ok(())
}
async fn good_pick_action(
ctx: &Context,
_menu: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
let guild_id = reaction.guild_id.unwrap();
let queue = get_queue_for_guild(ctx, &guild_id).await?;
let queue = queue.lock().await;
if let Some((_, song)) = queue.current() {
let data = ctx.data.read().await;
let store = data.get::<Store>().unwrap();
let database = data.get::<DatabaseContainer>().unwrap();
add_youtube_song_to_database(store, database, &mut song.clone()).await?;
}
Ok(())
}

@ -1,5 +1,121 @@
use crate::providers::music::queue::{Song, SongSource};
use crate::utils::context_data::StoreData;
use crate::utils::error::BotResult;
use aspotify::{ArtistSimplified, Track};
use bot_database::Database;
use regex::Regex;
use responses::VideoInformation;
use youtube_dl::search_video_information;
pub(crate) mod lyrics;
pub(crate) mod queue;
pub(crate) mod responses;
pub(crate) mod spotify;
pub(crate) mod youtube_dl;
/// Searches for a youtube video for the specified song
pub(crate) async fn song_to_youtube_video(song: &Song) -> BotResult<Option<VideoInformation>> {
let artist = song.author().clone();
let title = song.title().clone();
let match_query = format!("{} - {}", artist, title);
let queries = vec![
format! {"{} - {} topic", artist, title},
format!("{} - {} lyrics", artist, title),
format!("{} - {} audio only", artist, title),
format!("{} by {}", title, artist),
match_query.clone(),
];
let mut last_result = None;
for query in queries {
let result = search_video_information(query).await?;
if let Some(video) = result {
if trigram::similarity(&video.title, &match_query) >= 0.4
|| (trigram::similarity(&video.title, &title) >= 0.3
&& trigram::similarity(&video.uploader, &artist) >= 0.3)
{
return Ok(Some(video));
}
last_result = Some(video);
}
}
Ok(last_result)
}
/// Adds a youtube song to the database of songs
pub async fn add_youtube_song_to_database(
store: &StoreData,
database: &Database,
song: &mut Song,
) -> BotResult<()> {
let track = match song.source() {
SongSource::Spotify(track) => track.clone(),
SongSource::YouTube(_) => match search_for_song_variations(store, song).await {
Ok(Some(track)) => track,
Err(e) => {
log::error!("Failed to search for song on spotify {:?}", e);
return Ok(());
}
_ => return Ok(()),
},
};
log::debug!("Song found on spotify. Inserting metadata");
let artists = artists_to_string(track.artists);
let url = song.url().await.unwrap();
if let Some(id) = track.id {
database
.add_song(&id, &artists, &track.name, &track.album.name, &url)
.await?;
}
Ok(())
}
/// Searches for multiple queries on spotify
async fn search_for_song_variations(
store: &StoreData,
song: &mut Song,
) -> BotResult<Option<Track>> {
static COMMON_AFFIXES: &str =
r"feat\.(\s\w+)|official(\svideo)?|remastered|revisited|(with\s)?lyrics";
lazy_static::lazy_static! {
static ref COMMON_ADDITIONS: Regex = Regex::new(format!(r"(?i)\[|\]|\(?[^\w\s]*\s?({})[^\w\s]*\s?\)?", COMMON_AFFIXES).as_str()).unwrap();
}
let mut query = song
.title()
.replace(|c| c != ' ' && !char::is_alphanumeric(c), "");
query = COMMON_ADDITIONS.replace_all(&query, " ").to_string();
log::debug!("Searching for youtube song");
if let Some(track) = store.spotify_api.search_for_song(&query).await? {
let similarity = trigram::similarity(
&format!(
"{} {}",
artists_to_string(track.artists.clone()),
track.name
),
&query,
);
if similarity > 0.3 {
log::debug!("Result is similar enough ({}). Returning track", similarity);
return Ok(Some(track));
}
log::debug!("Result is not similar enough");
}
log::debug!("No result found");
Ok(None)
}
/// Creates a string from a vector of artists
pub fn artists_to_string(artists: Vec<ArtistSimplified>) -> String {
artists
.into_iter()
.map(|a| a.name)
.collect::<Vec<String>>()
.join("&")
}

@ -1,12 +1,13 @@
use std::collections::VecDeque;
use aspotify::{Track, TrackSimplified};
use aspotify::Track;
use songbird::tracks::TrackHandle;
use bot_coreutils::shuffle::Shuffle;
use crate::providers::music::responses::{PlaylistEntry, VideoInformation};
use crate::providers::music::youtube_dl;
use crate::providers::music::song_to_youtube_video;
use bot_database::models::YoutubeSong;
use bot_serenityutils::core::MessageHandle;
use std::sync::Arc;
use tokio::sync::RwLock;
@ -14,7 +15,7 @@ use tokio::sync::RwLock;
#[derive(Clone)]
pub struct MusicQueue {
inner: VecDeque<Song>,
current: Option<TrackHandle>,
current: Option<(TrackHandle, Song)>,
paused: bool,
pub now_playing_msg: Option<Arc<RwLock<MessageHandle>>>,
pub leave_flag: bool,
@ -57,8 +58,8 @@ impl MusicQueue {
}
/// Sets the currently playing song
pub fn set_current(&mut self, handle: TrackHandle) {
self.current = Some(handle)
pub fn set_current(&mut self, handle: TrackHandle, song: Song) {
self.current = Some((handle, song))
}
/// Clears the currently playing song
@ -67,7 +68,7 @@ impl MusicQueue {
}
/// Returns the reference to the currently playing song
pub fn current(&self) -> &Option<TrackHandle> {
pub fn current(&self) -> &Option<(TrackHandle, Song)> {
&self.current
}
@ -92,9 +93,9 @@ impl MusicQueue {
pub fn pause(&mut self) {
if let Some(current) = &self.current {
if self.paused {
let _ = current.play();
let _ = current.0.play();
} else {
let _ = current.pause();
let _ = current.0.pause();
}
self.paused = !self.paused;
@ -109,12 +110,19 @@ impl MusicQueue {
}
}
#[derive(Clone, Debug)]
pub enum SongSource {
Spotify(Track),
YouTube(String),
}
#[derive(Clone, Debug)]
pub struct Song {
url: Option<String>,
title: String,
author: String,
thumbnail: Option<String>,
source: SongSource,
}
impl Song {
@ -125,11 +133,7 @@ impl Song {
Some(url)
} else {
log::debug!("Lazy fetching video for title");
let information =
youtube_dl::search_video_information(format!("{} - {}", self.author, self.title))
.await
.ok()
.and_then(|i| i)?;
let information = song_to_youtube_video(&self).await.ok()??;
self.url = Some(information.webpage_url.clone());
self.thumbnail = information.thumbnail;
self.author = information.uploader;
@ -153,15 +157,21 @@ impl Song {
pub fn thumbnail(&self) -> &Option<String> {
&self.thumbnail
}
/// The source of the song
pub fn source(&self) -> &SongSource {
&self.source
}
}
impl From<VideoInformation> for Song {
fn from(info: VideoInformation) -> Self {
Self {
url: Some(info.webpage_url),
url: Some(info.webpage_url.clone()),
title: info.title,
author: info.uploader,
thumbnail: info.thumbnail,
source: SongSource::YouTube(info.webpage_url),
}
}
}
@ -173,6 +183,7 @@ impl From<PlaylistEntry> for Song {
title: entry.title,
author: entry.uploader,
thumbnail: None,
source: SongSource::YouTube(format!("https://www.youtube.com/watch?v={}", entry.url)),
}
}
}
@ -180,31 +191,29 @@ impl From<PlaylistEntry> for Song {
impl From<Track> for Song {
fn from(track: Track) -> Self {
Self {
title: track.name,
title: track.name.clone(),
author: track
.clone()
.artists
.into_iter()
.map(|a| a.name)
.map(|a| a.name.clone())
.collect::<Vec<String>>()
.join(" & "),
url: None,
thumbnail: None,
source: SongSource::Spotify(track),
}
}
}
impl From<TrackSimplified> for Song {
fn from(track: TrackSimplified) -> Self {
impl From<YoutubeSong> for Song {
fn from(song: YoutubeSong) -> Self {
Self {
title: track.name,
author: track
.artists
.into_iter()
.map(|a| a.name)
.collect::<Vec<String>>()
.join(" & "),
url: None,
title: song.title,
author: song.artist,
url: Some(song.url.clone()),
thumbnail: None,
source: SongSource::YouTube(song.url),
}
}
}

@ -1,6 +1,5 @@
use aspotify::{Client, ClientCredentials, PlaylistItem, PlaylistItemType};
use aspotify::{Client, ClientCredentials, ItemType, PlaylistItemType, Track};
use crate::providers::music::queue::Song;
use crate::utils::error::{BotError, BotResult};
pub struct SpotifyApi {
@ -21,8 +20,26 @@ impl SpotifyApi {
Self { client }
}
/// Searches for a song on spotify
pub async fn search_for_song(&self, query: &str) -> BotResult<Option<Track>> {
log::debug!("Searching for song '{}' on spotify", query);
let types = vec![ItemType::Track];
let result = self
.client
.search()
.search(query, types, false, 1, 0, None)
.await?;
log::trace!("Result is {:?}", result);
let tracks = result
.data
.tracks
.ok_or(BotError::from("Failed to get search spotify results"))?;
Ok(tracks.items.into_iter().next())
}
/// Returns the songs for a playlist
pub async fn get_songs_in_playlist(&self, url: &str) -> BotResult<Vec<Song>> {
pub async fn get_songs_in_playlist(&self, url: &str) -> BotResult<Vec<Track>> {
log::debug!("Fetching spotify songs from playlist '{}'", url);
let id = self.get_id_for_url(url)?;
let mut playlist_tracks = Vec::new();
@ -42,17 +59,9 @@ impl SpotifyApi {
url
);
let songs = playlist_tracks
.into_iter()
.filter_map(|item| item.item)
.filter_map(|t| match t {
PlaylistItemType::Track(t) => Some(Song::from(t)),
PlaylistItemType::Episode(_) => None,
})
.collect();
log::trace!("Songs are {:?}", songs);
log::trace!("Songs are {:?}", playlist_tracks);
Ok(songs)
Ok(playlist_tracks)
}
/// Returns the tracks of a playlist with pagination
@ -61,43 +70,66 @@ impl SpotifyApi {
id: &str,
limit: usize,
offset: usize,
) -> BotResult<Vec<PlaylistItem>> {
) -> BotResult<Vec<Track>> {
log::trace!(
"Fetching songs from spotify playlist: limit {}, offset {}",
limit,
offset
);
let tracks = self
let page = self
.client
.playlists()
.get_playlists_items(id, limit, offset, None)
.await?
.data;
let tracks: Vec<Track> = page
.items
.into_iter()
.filter_map(|item| item.item)
.filter_map(|t| match t {
PlaylistItemType::Track(t) => Some(t),
PlaylistItemType::Episode(_) => None,
})
.collect();
log::trace!("Tracks are {:?}", tracks);
Ok(tracks.items)
Ok(tracks)
}
/// Returns all songs for a given album
pub async fn get_songs_in_album(&self, url: &str) -> BotResult<Vec<Song>> {
pub async fn get_songs_in_album(&self, url: &str) -> BotResult<Vec<Track>> {
log::debug!("Fetching songs for spotify album '{}'", url);
let id = self.get_id_for_url(url)?;
let album = self.client.albums().get_album(&*id, None).await?.data;
log::trace!("Album is {:?}", album);
let song_names: Vec<Song> = album.tracks.items.into_iter().map(Song::from).collect();
log::debug!("{} songs found in album '{}'", song_names.len(), url);
Ok(song_names)
let simple_tracks: Vec<String> = album
.tracks
.items
.into_iter()
.filter_map(|t| t.id)
.collect();
let tracks = self
.client
.tracks()
.get_tracks(simple_tracks, None)
.await?
.data;
log::trace!("Tracks are {:?}", tracks);
Ok(tracks)
}
/// Returns song entity for a given spotify url
pub async fn get_song_name(&self, url: &str) -> BotResult<Song> {
pub async fn get_track_for_url(&self, url: &str) -> BotResult<Track> {
log::debug!("Getting song for {}", url);
let id = self.get_id_for_url(url)?;
let track = self.client.tracks().get_track(&*id, None).await?.data;
log::trace!("Track info is {:?}", track);
Ok(track.into())
Ok(track)
}
/// Returns the id for a given spotify URL

Loading…
Cancel
Save