Reimplement queue and add guild settings
Signed-off-by: trivernis <trivernis@protonmail.com>pull/2/head
parent
1fad2950b5
commit
b6dd726c17
@ -0,0 +1,10 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<Languages>
|
||||||
|
<language minSize="60" name="Rust" />
|
||||||
|
</Languages>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
@ -0,0 +1,28 @@
|
|||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::CommandResult;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
use crate::commands::music::get_queue_for_guild;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Clears the queue")]
|
||||||
|
#[usage("clear")]
|
||||||
|
#[aliases("cl")]
|
||||||
|
#[allowed_roles("DJ")]
|
||||||
|
async fn clear(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let queue = get_queue_for_guild(ctx, &guild.id).await?;
|
||||||
|
{
|
||||||
|
let mut queue_lock = queue.lock().await;
|
||||||
|
queue_lock.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.channel_id
|
||||||
|
.say(ctx, "The queue has been cleared")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,23 +1,181 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use serenity::async_trait;
|
||||||
|
use serenity::client::Context;
|
||||||
use serenity::framework::standard::macros::group;
|
use serenity::framework::standard::macros::group;
|
||||||
|
use serenity::http::Http;
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
use serenity::model::guild::Guild;
|
||||||
|
use serenity::model::id::{ChannelId, GuildId, UserId};
|
||||||
|
use songbird::{
|
||||||
|
Call, Event, EventContext, EventHandler as VoiceEventHandler, Songbird, TrackEvent,
|
||||||
|
};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use clear::CLEAR_COMMAND;
|
||||||
use current::CURRENT_COMMAND;
|
use current::CURRENT_COMMAND;
|
||||||
use join::JOIN_COMMAND;
|
use join::JOIN_COMMAND;
|
||||||
use leave::LEAVE_COMMAND;
|
use leave::LEAVE_COMMAND;
|
||||||
use play::PLAY_COMMAND;
|
use play::PLAY_COMMAND;
|
||||||
|
use play_next::PLAY_NEXT_COMMAND;
|
||||||
use queue::QUEUE_COMMAND;
|
use queue::QUEUE_COMMAND;
|
||||||
use shuffle::SHUFFLE_COMMAND;
|
use shuffle::SHUFFLE_COMMAND;
|
||||||
use skip::SKIP_COMMAND;
|
use skip::SKIP_COMMAND;
|
||||||
|
|
||||||
|
use crate::providers::music::{get_video_information, get_videos_for_playlist};
|
||||||
|
use crate::providers::music::queue::{MusicQueue, Song};
|
||||||
|
use crate::utils::error::{BotError, BotResult};
|
||||||
|
use crate::utils::store::Store;
|
||||||
|
|
||||||
|
mod clear;
|
||||||
mod current;
|
mod current;
|
||||||
mod join;
|
mod join;
|
||||||
mod leave;
|
mod leave;
|
||||||
mod play;
|
mod play;
|
||||||
|
mod play_next;
|
||||||
mod queue;
|
mod queue;
|
||||||
mod shuffle;
|
mod shuffle;
|
||||||
mod skip;
|
mod skip;
|
||||||
mod utils;
|
|
||||||
|
|
||||||
#[group]
|
#[group]
|
||||||
#[commands(join, leave, play, queue, skip, shuffle, current)]
|
#[commands(join, leave, play, queue, skip, shuffle, current, play_next, clear)]
|
||||||
#[prefix("m")]
|
#[prefix("m")]
|
||||||
pub struct Music;
|
pub struct Music;
|
||||||
|
|
||||||
|
/// Joins a voice channel
|
||||||
|
async fn join_channel(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) -> Arc<Mutex<Call>> {
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let (handler, _) = manager.join(guild_id, channel_id).await;
|
||||||
|
let mut data = ctx.data.write().await;
|
||||||
|
let store = data.get_mut::<Store>().unwrap();
|
||||||
|
let queue = Arc::new(Mutex::new(MusicQueue::new()));
|
||||||
|
|
||||||
|
store.music_queues.insert(guild_id, queue.clone());
|
||||||
|
{
|
||||||
|
let mut handler_lock = handler.lock().await;
|
||||||
|
|
||||||
|
handler_lock.add_global_event(
|
||||||
|
Event::Track(TrackEvent::End),
|
||||||
|
SongEndNotifier {
|
||||||
|
channel_id,
|
||||||
|
http: ctx.http.clone(),
|
||||||
|
queue: Arc::clone(&queue),
|
||||||
|
handler: handler.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handler
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the voice channel the author is in
|
||||||
|
fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult<ChannelId> {
|
||||||
|
guild
|
||||||
|
.voice_states
|
||||||
|
.get(author_id)
|
||||||
|
.and_then(|voice_state| voice_state.channel_id)
|
||||||
|
.ok_or(BotError::from("Not in a voice channel."))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the voice manager from the context
|
||||||
|
async fn get_voice_manager(ctx: &Context) -> Arc<Songbird> {
|
||||||
|
songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to a guilds music queue
|
||||||
|
async fn get_queue_for_guild(
|
||||||
|
ctx: &Context,
|
||||||
|
guild_id: &GuildId,
|
||||||
|
) -> BotResult<Arc<Mutex<MusicQueue>>> {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
let store = data.get::<Store>().unwrap();
|
||||||
|
|
||||||
|
let queue = store
|
||||||
|
.music_queues
|
||||||
|
.get(guild_id)
|
||||||
|
.ok_or(BotError::from("No queue for server"))?
|
||||||
|
.clone();
|
||||||
|
Ok(queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SongEndNotifier {
|
||||||
|
channel_id: ChannelId,
|
||||||
|
http: Arc<Http>,
|
||||||
|
queue: Arc<Mutex<MusicQueue>>,
|
||||||
|
handler: Arc<Mutex<Call>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl VoiceEventHandler for SongEndNotifier {
|
||||||
|
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
|
||||||
|
play_next_in_queue(&self.http, &self.channel_id, &self.queue, &self.handler).await;
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays the next song in the queue
|
||||||
|
async fn play_next_in_queue(
|
||||||
|
http: &Arc<Http>,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
queue: &Arc<Mutex<MusicQueue>>,
|
||||||
|
handler: &Arc<Mutex<Call>>,
|
||||||
|
) {
|
||||||
|
let mut queue_lock = queue.lock().await;
|
||||||
|
|
||||||
|
if let Some(next) = queue_lock.next() {
|
||||||
|
let source = match songbird::ytdl(&next.url).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = channel_id
|
||||||
|
.say(&http, format!("Failed to enqueue {}: {:?}", next.title, e))
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut handler_lock = handler.lock().await;
|
||||||
|
let track = handler_lock.play_only_source(source);
|
||||||
|
queue_lock.set_current(track);
|
||||||
|
} else {
|
||||||
|
queue_lock.clear_current();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the list of songs for a given url
|
||||||
|
async fn get_songs_for_url(ctx: &&Context, msg: &Message, url: &str) -> BotResult<Vec<Song>> {
|
||||||
|
let mut songs: Vec<Song> = get_videos_for_playlist(url)?
|
||||||
|
.into_iter()
|
||||||
|
.map(Song::from)
|
||||||
|
.collect();
|
||||||
|
if songs.len() == 0 {
|
||||||
|
let song: Song = get_video_information(url)?.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
e
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.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?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(songs)
|
||||||
|
}
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::{Args, CommandError, CommandResult};
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
use crate::commands::music::{
|
||||||
|
get_channel_for_author, get_queue_for_guild, get_songs_for_url, get_voice_manager,
|
||||||
|
join_channel, play_next_in_queue,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Puts a song as the next to play in the queue")]
|
||||||
|
#[usage("play_next <song-url>")]
|
||||||
|
#[min_args(1)]
|
||||||
|
#[max_args(2)]
|
||||||
|
#[aliases("pn")]
|
||||||
|
#[allowed_roles("DJ")]
|
||||||
|
async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
|
let url = args.message();
|
||||||
|
|
||||||
|
if !url.starts_with("http") {
|
||||||
|
return Err(CommandError::from("The provided url is not valid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let manager = get_voice_manager(ctx).await;
|
||||||
|
let mut handler = manager.get(guild.id);
|
||||||
|
|
||||||
|
if handler.is_none() {
|
||||||
|
msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
let channel_id = get_channel_for_author(&msg.author.id, &guild)?;
|
||||||
|
handler = Some(join_channel(ctx, channel_id, guild.id).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler = handler.ok_or(CommandError::from("Not in a voice channel"))?;
|
||||||
|
|
||||||
|
let mut songs = get_songs_for_url(&ctx, msg, url).await?;
|
||||||
|
|
||||||
|
let queue = get_queue_for_guild(ctx, &guild.id).await?;
|
||||||
|
let play_first = {
|
||||||
|
let mut queue_lock = queue.lock().await;
|
||||||
|
songs.reverse();
|
||||||
|
|
||||||
|
for song in songs {
|
||||||
|
queue_lock.add_next(song);
|
||||||
|
}
|
||||||
|
queue_lock.current().is_none()
|
||||||
|
};
|
||||||
|
|
||||||
|
if play_first {
|
||||||
|
play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,33 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use serenity::client::Context;
|
|
||||||
use serenity::model::guild::Guild;
|
|
||||||
use serenity::model::id::{ChannelId, GuildId, UserId};
|
|
||||||
use songbird::Call;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use crate::utils::error::{BotError, BotResult};
|
|
||||||
|
|
||||||
/// Joins a voice channel
|
|
||||||
pub(crate) async fn join_channel(
|
|
||||||
ctx: &Context,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
guild_id: GuildId,
|
|
||||||
) -> Arc<Mutex<Call>> {
|
|
||||||
let manager = songbird::get(ctx)
|
|
||||||
.await
|
|
||||||
.expect("Songbird Voice client placed in at initialisation.")
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
let (handler, _) = manager.join(guild_id, channel_id).await;
|
|
||||||
handler
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the voice channel the author is in
|
|
||||||
pub(crate) fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult<ChannelId> {
|
|
||||||
guild
|
|
||||||
.voice_states
|
|
||||||
.get(author_id)
|
|
||||||
.and_then(|voice_state| voice_state.channel_id)
|
|
||||||
.ok_or(BotError::from("Not in a voice channel."))
|
|
||||||
}
|
|
@ -0,0 +1,60 @@
|
|||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::{Args, CommandResult};
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
use crate::database::get_database_from_context;
|
||||||
|
use crate::database::guild::GUILD_SETTINGS;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Get a guild setting")]
|
||||||
|
#[usage("get (<setting>)")]
|
||||||
|
#[example("get music.autoshuffle")]
|
||||||
|
#[min_args(0)]
|
||||||
|
#[max_args(1)]
|
||||||
|
#[required_permissions("MANAGE_GUILD")]
|
||||||
|
async fn get(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
|
let database = get_database_from_context(ctx).await;
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
if let Some(key) = args.single::<String>().ok() {
|
||||||
|
let database_lock = database.lock().await;
|
||||||
|
let setting = database_lock.get_guild_setting::<String>(&guild.id, &key);
|
||||||
|
|
||||||
|
match setting {
|
||||||
|
Ok(value) => {
|
||||||
|
msg.channel_id
|
||||||
|
.say(ctx, format!("`{}` is set to to `{}`", key, value))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to get setting: {:?}", e);
|
||||||
|
msg.channel_id
|
||||||
|
.say(ctx, format!("`{}` is not set", key))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for key in GUILD_SETTINGS {
|
||||||
|
let mut kv_pairs = Vec::new();
|
||||||
|
{
|
||||||
|
let database_lock = database.lock().await;
|
||||||
|
match database_lock.get_guild_setting::<String>(&guild.id, &key) {
|
||||||
|
Ok(value) => kv_pairs.push(format!("`{}` = `{}`", key, value)),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to get setting: {:?}", e);
|
||||||
|
kv_pairs.push(format!("`{}` not set", key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg.channel_id
|
||||||
|
.send_message(ctx, |m| {
|
||||||
|
m.embed(|e| e.title("Guild Settings").description(kv_pairs.join("\n")))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
use serenity::framework::standard::macros::group;
|
||||||
|
|
||||||
|
use get::GET_COMMAND;
|
||||||
|
use set::SET_COMMAND;
|
||||||
|
|
||||||
|
mod get;
|
||||||
|
mod set;
|
||||||
|
|
||||||
|
#[group]
|
||||||
|
#[commands(set, get)]
|
||||||
|
#[prefix("settings")]
|
||||||
|
pub struct Settings;
|
@ -0,0 +1,28 @@
|
|||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::framework::standard::{Args, CommandResult};
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
use crate::database::get_database_from_context;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Set a guild setting")]
|
||||||
|
#[usage("set <setting> <value>")]
|
||||||
|
#[example("set music.autoshuffle true")]
|
||||||
|
#[min_args(2)]
|
||||||
|
#[max_args(2)]
|
||||||
|
#[required_permissions("MANAGE_GUILD")]
|
||||||
|
async fn set(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
|
let key = args.single::<String>().unwrap();
|
||||||
|
let value = args.single::<String>().unwrap();
|
||||||
|
let database = get_database_from_context(ctx).await;
|
||||||
|
let database_lock = database.lock().await;
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
database_lock.set_guild_setting(&guild.id, &key, value.clone())?;
|
||||||
|
msg.channel_id
|
||||||
|
.say(ctx, format!("Set `{}` to `{}`", key, value))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,26 +1,6 @@
|
|||||||
CREATE TABLE IF NOT EXISTS guilds
|
|
||||||
(
|
|
||||||
guild_id INTEGER PRIMARY KEY
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS guild_settings
|
CREATE TABLE IF NOT EXISTS guild_settings
|
||||||
(
|
(
|
||||||
guild_id
|
guild_id TEXT NOT NULL,
|
||||||
INTEGER
|
setting_key TEXT NOT NULL,
|
||||||
NOT
|
setting_value TEXT NOT NULL
|
||||||
NULL,
|
);
|
||||||
setting_key
|
|
||||||
TEXT
|
|
||||||
NOT
|
|
||||||
NULL,
|
|
||||||
setting_value
|
|
||||||
TEXT,
|
|
||||||
FOREIGN
|
|
||||||
KEY
|
|
||||||
(
|
|
||||||
guild_id
|
|
||||||
) REFERENCES guilds
|
|
||||||
(
|
|
||||||
guild_id
|
|
||||||
)
|
|
||||||
);
|
|
@ -1,2 +1,2 @@
|
|||||||
SELECT NULL
|
PRAGMA foreign_keys = false;
|
||||||
FROM guilds;
|
DROP TABLE IF EXISTS guilds;
|
@ -1 +1 @@
|
|||||||
pub(crate) mod ytdl;
|
pub(crate) mod music;
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
use crate::providers::music::responses::{PlaylistEntry, VideoInformation};
|
||||||
|
use crate::utils::error::BotResult;
|
||||||
|
|
||||||
|
pub(crate) mod queue;
|
||||||
|
pub(crate) mod responses;
|
||||||
|
|
||||||
|
/// 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)?;
|
||||||
|
|
||||||
|
let videos = output
|
||||||
|
.lines()
|
||||||
|
.filter_map(|l| serde_json::from_str::<PlaylistEntry>(l).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(videos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()?;
|
||||||
|
|
||||||
|
let information = serde_json::from_reader(ytdl.stdout.unwrap())?;
|
||||||
|
|
||||||
|
Ok(information)
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use songbird::tracks::TrackHandle;
|
||||||
|
|
||||||
|
use crate::providers::music::responses::{PlaylistEntry, VideoInformation};
|
||||||
|
use crate::utils::shuffle_vec_deque;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MusicQueue {
|
||||||
|
inner: VecDeque<Song>,
|
||||||
|
current: Option<TrackHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicQueue {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: VecDeque::new(),
|
||||||
|
current: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a song to the queue
|
||||||
|
pub fn add(&mut self, song: Song) {
|
||||||
|
self.inner.push_back(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a song to be played next in the queue
|
||||||
|
pub fn add_next(&mut self, song: Song) {
|
||||||
|
self.inner.push_front(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shuffles the queue
|
||||||
|
pub fn shuffle(&mut self) {
|
||||||
|
shuffle_vec_deque(&mut self.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the inner deque
|
||||||
|
pub fn entries(&self) -> &VecDeque<Song> {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the next song from the queue
|
||||||
|
pub fn next(&mut self) -> Option<Song> {
|
||||||
|
self.inner.pop_front()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the currently playing song
|
||||||
|
pub fn set_current(&mut self, handle: TrackHandle) {
|
||||||
|
self.current = Some(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the currently playing song
|
||||||
|
pub fn clear_current(&mut self) {
|
||||||
|
self.current = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the reference to the currently playing song
|
||||||
|
pub fn current(&self) -> &Option<TrackHandle> {
|
||||||
|
&self.current
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the queue
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.inner.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Song {
|
||||||
|
pub url: String,
|
||||||
|
pub title: String,
|
||||||
|
pub author: String,
|
||||||
|
pub thumbnail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VideoInformation> for Song {
|
||||||
|
fn from(info: VideoInformation) -> Self {
|
||||||
|
Self {
|
||||||
|
url: info.webpage_url,
|
||||||
|
title: info.title,
|
||||||
|
author: info.uploader,
|
||||||
|
thumbnail: info.thumbnail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlaylistEntry> for Song {
|
||||||
|
fn from(entry: PlaylistEntry) -> Self {
|
||||||
|
Self {
|
||||||
|
url: format!("https://www.youtube.com/watch?v={}", entry.url),
|
||||||
|
title: entry.title,
|
||||||
|
author: entry.uploader,
|
||||||
|
thumbnail: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
use serde_derive::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
pub(crate) struct PlaylistEntry {
|
||||||
|
ie_key: String,
|
||||||
|
id: String,
|
||||||
|
pub url: String,
|
||||||
|
pub title: String,
|
||||||
|
pub uploader: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
pub(crate) struct VideoInformation {
|
||||||
|
id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub thumbnail: Option<String>,
|
||||||
|
pub webpage_url: String,
|
||||||
|
pub uploader: String,
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
use std::io::Read;
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
|
||||||
use crate::providers::ytdl::playlist_entry::PlaylistEntry;
|
|
||||||
use crate::utils::error::BotResult;
|
|
||||||
|
|
||||||
mod playlist_entry;
|
|
||||||
|
|
||||||
/// Returns a list of youtube videos for a given url
|
|
||||||
pub(crate) fn get_videos_for_url(url: &str) -> BotResult<Vec<PlaylistEntry>> {
|
|
||||||
let ytdl = Command::new("youtube-dl")
|
|
||||||
.args(&[
|
|
||||||
"-f",
|
|
||||||
"--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)?;
|
|
||||||
|
|
||||||
let videos = output
|
|
||||||
.lines()
|
|
||||||
.map(|l| serde_json::from_str::<PlaylistEntry>(l).unwrap())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(videos)
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
use serde_derive::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
|
||||||
pub(crate) struct PlaylistEntry {
|
|
||||||
ie_key: String,
|
|
||||||
id: String,
|
|
||||||
pub url: String,
|
|
||||||
pub title: String,
|
|
||||||
}
|
|
@ -1,2 +1,16 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
|
/// Fisher-Yates shuffle for VecDeque
|
||||||
|
pub fn shuffle_vec_deque<T>(deque: &mut VecDeque<T>) {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let mut i = deque.len();
|
||||||
|
while i >= 2 {
|
||||||
|
i -= 1;
|
||||||
|
deque.swap(i, rng.gen_range(0..i + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue