From 0cb0a4c47bbf29a56df3a40d9b118f56b6c5d00c Mon Sep 17 00:00:00 2001 From: trivernis Date: Fri, 9 Apr 2021 12:48:44 +0200 Subject: [PATCH] Add command to manage playlists Signed-off-by: trivernis --- .../down.sql | 2 + .../up.sql | 7 +++ database/src/database.rs | 49 +++++++++++++++++ database/src/models.rs | 15 +++++ database/src/schema.rs | 13 +++++ src/commands/music/mod.rs | 55 +++++++++++++++---- src/commands/music/playlists.rs | 27 +++++++++ src/commands/music/save_playlist.rs | 34 ++++++++++++ 8 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 database/migrations/2021-04-09-095329_create_guild_playlists/down.sql create mode 100644 database/migrations/2021-04-09-095329_create_guild_playlists/up.sql create mode 100644 src/commands/music/playlists.rs create mode 100644 src/commands/music/save_playlist.rs diff --git a/database/migrations/2021-04-09-095329_create_guild_playlists/down.sql b/database/migrations/2021-04-09-095329_create_guild_playlists/down.sql new file mode 100644 index 0000000..2050c9d --- /dev/null +++ b/database/migrations/2021-04-09-095329_create_guild_playlists/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE guild_playlists; \ No newline at end of file diff --git a/database/migrations/2021-04-09-095329_create_guild_playlists/up.sql b/database/migrations/2021-04-09-095329_create_guild_playlists/up.sql new file mode 100644 index 0000000..ab1947a --- /dev/null +++ b/database/migrations/2021-04-09-095329_create_guild_playlists/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE guild_playlists ( + guild_id BIGINT NOT NULL, + name VARCHAR(255) NOT NULL, + url VARCHAR(1024) NOT NULL, + PRIMARY KEY (guild_id, name) +) \ No newline at end of file diff --git a/database/src/database.rs b/database/src/database.rs index 5f47dcd..16e7a38 100644 --- a/database/src/database.rs +++ b/database/src/database.rs @@ -78,4 +78,53 @@ impl Database { Ok(()) } + + /// Returns a list of all guild playlists + pub fn get_guild_playlists(&self, guild_id: u64) -> DatabaseResult> { + use guild_playlists::dsl; + log::debug!("Retrieving guild playlists for guild {}", guild_id); + let connection = self.pool.get()?; + let playlists: Vec = dsl::guild_playlists + .filter(dsl::guild_id.eq(guild_id as i64)) + .load::(&connection)?; + + Ok(playlists) + } + + /// Returns a guild playlist by name + pub fn get_guild_playlist( + &self, + guild_id: u64, + name: &str, + ) -> DatabaseResult> { + use guild_playlists::dsl; + log::debug!("Retriving guild playlist '{}' for guild {}", name, guild_id); + let connection = self.pool.get()?; + + let playlists: Vec = dsl::guild_playlists + .filter(dsl::guild_id.eq(guild_id as i64)) + .filter(dsl::name.eq(name)) + .load::(&connection)?; + + Ok(playlists.into_iter().next()) + } + + /// Adds a new playlist to the database overwriting the old one + pub fn add_guild_playlist(&self, guild_id: u64, name: &str, url: &str) -> DatabaseResult<()> { + use guild_playlists::dsl; + log::debug!("Inserting guild playlist '{}' for guild {}", name, guild_id); + let connection = self.pool.get()?; + insert_into(dsl::guild_playlists) + .values(GuildPlaylistInsert { + guild_id: guild_id as i64, + name: name.to_string(), + url: url.to_string(), + }) + .on_conflict((dsl::guild_id, dsl::name)) + .do_update() + .set(dsl::url.eq(url.to_string())) + .execute(&connection)?; + + Ok(()) + } } diff --git a/database/src/models.rs b/database/src/models.rs index aea5738..b734447 100644 --- a/database/src/models.rs +++ b/database/src/models.rs @@ -14,3 +14,18 @@ pub struct GuildSettingInsert { pub key: String, pub value: String, } + +#[derive(Queryable, Debug)] +pub struct GuildPlaylist { + pub guild_id: i64, + pub name: String, + pub url: String, +} + +#[derive(Insertable, Debug)] +#[table_name = "guild_playlists"] +pub struct GuildPlaylistInsert { + pub guild_id: i64, + pub name: String, + pub url: String, +} diff --git a/database/src/schema.rs b/database/src/schema.rs index d896ee3..672a7df 100644 --- a/database/src/schema.rs +++ b/database/src/schema.rs @@ -1,3 +1,11 @@ +table! { + guild_playlists (guild_id, name) { + guild_id -> Int8, + name -> Varchar, + url -> Varchar, + } +} + table! { guild_settings (guild_id, key) { guild_id -> Int8, @@ -5,3 +13,8 @@ table! { value -> Nullable, } } + +allow_tables_to_appear_in_same_query!( + guild_playlists, + guild_settings, +); diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs index 1bc142d..8a6d081 100644 --- a/src/commands/music/mod.rs +++ b/src/commands/music/mod.rs @@ -19,7 +19,9 @@ use leave::LEAVE_COMMAND; use pause::PAUSE_COMMAND; use play::PLAY_COMMAND; use play_next::PLAY_NEXT_COMMAND; +use playlists::PLAYLISTS_COMMAND; use queue::QUEUE_COMMAND; +use save_playlist::SAVE_PLAYLIST_COMMAND; use shuffle::SHUFFLE_COMMAND; use skip::SKIP_COMMAND; @@ -27,7 +29,7 @@ use crate::providers::music::queue::{MusicQueue, Song}; use crate::providers::music::{ get_video_information, get_videos_for_playlist, search_video_information, }; -use crate::utils::context_data::Store; +use crate::utils::context_data::{DatabaseContainer, Store}; use crate::utils::error::{BotError, BotResult}; use regex::Regex; use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering}; @@ -40,13 +42,26 @@ mod leave; mod pause; mod play; mod play_next; +mod playlists; mod queue; +mod save_playlist; mod shuffle; mod skip; #[group] #[commands( - join, leave, play, queue, skip, shuffle, current, play_next, clear, pause + join, + leave, + play, + queue, + skip, + shuffle, + current, + play_next, + clear, + pause, + save_playlist, + playlists )] #[prefixes("m", "music")] pub struct Music; @@ -240,8 +255,11 @@ 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> { + let guild_id = msg.guild_id.unwrap(); + let mut query = query.to_string(); lazy_static::lazy_static! { // expressions to determine the type of url + static ref PLAYLIST_NAME_REGEX: Regex = Regex::new(r"^pl:(\S+)$").unwrap(); static ref YOUTUBE_URL_REGEX: Regex = Regex::new(r"^(https?(://))?(www\.)?(youtube\.com/watch\?.*v=.*)|(/youtu.be/.*)|(youtube\.com/playlist\?.*list=.*)$").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(); @@ -250,12 +268,25 @@ async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotRe let mut songs = Vec::new(); let data = ctx.data.read().await; let store = data.get::().unwrap(); + let database = data.get::().unwrap(); log::debug!("Querying play input {}", query); - if YOUTUBE_URL_REGEX.is_match(query) { + if let Some(captures) = PLAYLIST_NAME_REGEX.captures(&query) { + log::debug!("Query is a saved playlist"); + let pl_name: &str = captures.get(1).unwrap().as_str(); + log::trace!("Playlist name is {}", pl_name); + let playlist_opt = database.get_guild_playlist(guild_id.0, pl_name)?; + log::trace!("Playlist is {:?}", playlist_opt); + + if let Some(playlist) = playlist_opt { + log::debug!("Assigning url for saved playlist to query"); + query = playlist.url; + } + } + if YOUTUBE_URL_REGEX.is_match(&query) { log::debug!("Query is youtube video or playlist"); // try fetching the url as a playlist - songs = get_videos_for_playlist(query) + songs = get_videos_for_playlist(&query) .await? .into_iter() .map(Song::from) @@ -264,32 +295,32 @@ 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 { log::debug!("Query is youtube video"); - let mut song: Song = get_video_information(query).await?.into(); + let mut song: Song = get_video_information(&query).await?.into(); added_one_msg(&ctx, msg, &mut song).await?; songs.push(song); } else { log::debug!("Query is playlist with {} songs", songs.len()); added_multiple_msg(&ctx, msg, &mut songs).await?; } - } else if SPOTIFY_PLAYLIST_REGEX.is_match(query) { + } 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?; + 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) { + } 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?; + 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) { + } 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 mut song = store.spotify_api.get_song_name(&query).await?; added_one_msg(ctx, msg, &mut song).await?; songs.push(song); } else { log::debug!("Query is a youtube search"); - let mut song: Song = search_video_information(query.to_string()) + let mut song: Song = search_video_information(query.clone()) .await? .ok_or(BotError::Msg(format!("Noting found for {}", query)))? .into(); diff --git a/src/commands/music/playlists.rs b/src/commands/music/playlists.rs new file mode 100644 index 0000000..c032147 --- /dev/null +++ b/src/commands/music/playlists.rs @@ -0,0 +1,27 @@ +use crate::utils::context_data::get_database_from_context; +use serenity::client::Context; +use serenity::framework::standard::macros::command; +use serenity::framework::standard::CommandResult; +use serenity::model::channel::Message; + +#[command] +#[only_in(guilds)] +#[description("Displays a list of all saved playlists")] +#[usage("")] +async fn playlists(ctx: &Context, msg: &Message) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.unwrap(); + log::debug!("Displaying playlists for guild {}", guild.id); + let database = get_database_from_context(ctx).await; + + let playlists = database.get_guild_playlists(guild.id.0)?; + msg.channel_id + .send_message(ctx, |m| { + m.embed(|e| { + e.title("Saved Playlists") + .fields(playlists.into_iter().map(|p| (p.name, p.url, true))) + }) + }) + .await?; + + Ok(()) +} diff --git a/src/commands/music/save_playlist.rs b/src/commands/music/save_playlist.rs new file mode 100644 index 0000000..5ffa6cb --- /dev/null +++ b/src/commands/music/save_playlist.rs @@ -0,0 +1,34 @@ +use crate::utils::context_data::get_database_from_context; +use serenity::client::Context; +use serenity::framework::standard::macros::command; +use serenity::framework::standard::{Args, CommandResult}; +use serenity::model::channel::Message; + +#[command] +#[only_in(guilds)] +#[description("Adds a playlist to the guilds saved playlists")] +#[usage(" ")] +#[example("anime https://www.youtube.com/playlist?list=PLqaM77H_o5hykROCe3uluvZEaPo6bZj-C")] +#[min_args(2)] +#[aliases("add-playlist", "save-playlist")] +#[allowed_roles("DJ")] +async fn save_playlist(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.unwrap(); + let name: String = args.single().unwrap(); + let url: &str = args.remains().unwrap(); + log::debug!( + "Adding playlist '{}' with url '{}' to guild {}", + name, + url, + guild.id + ); + let database = get_database_from_context(ctx).await; + + database.add_guild_playlist(guild.id.0, &*name, url)?; + + msg.channel_id + .say(ctx, format!("Playlist **{}** saved", name)) + .await?; + + Ok(()) +}