commit
e2f9027fb9
@ -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)
|
||||
)
|
@ -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())
|
||||
}
|
||||
}
|
@ -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("&")
|
||||
}
|
||||
|
Loading…
Reference in New Issue