Compare commits
2 commits
bbf65e6458
...
0ff20483c9
Author | SHA1 | Date | |
---|---|---|---|
0ff20483c9 | |||
67f1b4176b |
13 changed files with 376 additions and 124 deletions
|
@ -4,6 +4,7 @@ mod voice_utils;
|
||||||
pub mod player_common;
|
pub mod player_common;
|
||||||
pub mod radio;
|
pub mod radio;
|
||||||
pub mod general_player;
|
pub mod general_player;
|
||||||
|
pub mod voice_types;
|
||||||
|
|
||||||
// ! not working
|
// ! not working
|
||||||
// pub mod yt;
|
// pub mod yt;
|
||||||
|
|
|
@ -2,8 +2,7 @@ use std::vec;
|
||||||
|
|
||||||
use songbird::input::YoutubeDl;
|
use songbird::input::YoutubeDl;
|
||||||
|
|
||||||
use crate::utils::poise_context_extension::ContextExt;
|
use crate::types::{Context, ContextExt, Error};
|
||||||
use crate::types::{Context, Error};
|
|
||||||
use crate::commands::voice::voice_utils::autocomplete_channel;
|
use crate::commands::voice::voice_utils::autocomplete_channel;
|
||||||
|
|
||||||
use super::connect;
|
use super::connect;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
|
use poise::CreateReply;
|
||||||
use songbird::input::{File, Input};
|
use songbird::input::{File, Input};
|
||||||
|
|
||||||
use crate::utils::debug::send_error;
|
use crate::utils::debug::send_error;
|
||||||
use crate::utils::poise_context_extension::ContextExt;
|
use crate::types::{Context, ContextExt, Error};
|
||||||
use crate::utils::utilities::get_local_songs;
|
use crate::utils::utilities::get_local_songs;
|
||||||
use crate::types::{Context, Error};
|
|
||||||
|
|
||||||
use super::voice_utils::{connect, autocomplete_channel};
|
use super::voice_utils::{connect, autocomplete_channel};
|
||||||
|
|
||||||
|
@ -114,3 +114,48 @@ pub async fn play_local(ctx: Context<'_>,
|
||||||
ctx.reply_ephemeral("Done!").await?;
|
ctx.reply_ephemeral("Done!").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
description_localized("en-US", "Display currently playing info")
|
||||||
|
)]
|
||||||
|
pub async fn playing(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let Some(guild_id) = ctx.guild_id() else {
|
||||||
|
ctx.reply_ephemeral("Guild id not found").await?;
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx.serenity_context())
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let Some(_) = manager.get(guild_id) else {
|
||||||
|
ctx.reply_ephemeral("I am not connected to any voice channel right now").await?;
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
// println!("here");
|
||||||
|
|
||||||
|
let embed = {
|
||||||
|
let mutex_hashmap = ctx.data().playing_info.lock().await;
|
||||||
|
let Some(playing_info) = mutex_hashmap.get(&guild_id) else {
|
||||||
|
ctx.reply_ephemeral("Entry not found, try to reconnect me").await?;
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
playing_info.generate_embed().await
|
||||||
|
};
|
||||||
|
|
||||||
|
dbg!(&embed);
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.embed(embed)
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
use radiobrowser::RadioBrowserAPI;
|
use poise::ChoiceParameter;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
||||||
use songbird::input::Input;
|
use songbird::input::Input;
|
||||||
use songbird::input::HttpRequest;
|
use songbird::input::HttpRequest;
|
||||||
|
|
||||||
use super::super::connect;
|
use super::super::connect;
|
||||||
use crate::utils::poise_context_extension::ContextExt;
|
use super::link_or_string;
|
||||||
|
use super::parse_radio_autocomplete;
|
||||||
|
use super::LinkString;
|
||||||
|
use crate::commands::voice_types::NumberOfEntries;
|
||||||
|
use crate::commands::voice_types::PlayingInfoType;
|
||||||
|
use crate::types::ContextExt;
|
||||||
use crate::types::{Context, Error};
|
use crate::types::{Context, Error};
|
||||||
use crate::commands::voice::voice_utils::autocomplete_channel;
|
use crate::commands::voice::voice_utils::autocomplete_channel;
|
||||||
|
|
||||||
use super::radio_utils::paginate_search;
|
use super::radio_utils::paginate_search_stations;
|
||||||
|
|
||||||
use super::super::voice_utils::MAX_ENTRIES;
|
use super::super::voice_utils::MAX_ENTRIES;
|
||||||
|
|
||||||
|
@ -24,6 +29,8 @@ pub async fn radio(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: deduplicate you use a lot of the same code
|
||||||
|
|
||||||
// TODO: autocomplete radio stream URLs
|
// TODO: autocomplete radio stream URLs
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
slash_command,
|
slash_command,
|
||||||
|
@ -43,6 +50,47 @@ pub async fn play(ctx: Context<'_>,
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let api = &ctx.data().radio_browser;
|
||||||
|
|
||||||
|
let stations_result = match link_or_string(&name) {
|
||||||
|
LinkString::Link => {
|
||||||
|
if name.ends_with(".m3u") {
|
||||||
|
ctx.reply_ephemeral("Sorry, currently I don't support m3u files").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
api.get_stations().byurl(name).await
|
||||||
|
},
|
||||||
|
LinkString::String => {
|
||||||
|
let (name, country, language) = match parse_radio_autocomplete(&name) {
|
||||||
|
Some(tuple) => tuple,
|
||||||
|
None => {
|
||||||
|
ctx.reply_ephemeral("Please use either direct URL or provided autocomplete").await?;
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
api.get_stations()
|
||||||
|
.name_exact(true)
|
||||||
|
.name(name)
|
||||||
|
.country_exact(true)
|
||||||
|
.country(country)
|
||||||
|
.language_exact(true)
|
||||||
|
.language(language)
|
||||||
|
.send().await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(stations) = stations_result else {
|
||||||
|
ctx.reply_ephemeral("There was an error with radio API!").await?;
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(station) = stations.first() else {
|
||||||
|
ctx.reply_ephemeral("Radio station not found!").await?;
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
let manager = songbird::get(ctx.serenity_context())
|
||||||
.await
|
.await
|
||||||
.expect("Songbird Voice client placed in at initialisation.")
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
@ -54,8 +102,9 @@ pub async fn play(ctx: Context<'_>,
|
||||||
};
|
};
|
||||||
|
|
||||||
if manager.get(guild_id).is_none() {
|
if manager.get(guild_id).is_none() {
|
||||||
if let Err(e) = connect(&ctx, guild_id, channel).await {
|
if let Err(e) = connect(&ctx, guild_id, channel).await {
|
||||||
ctx.reply_ephemeral(&e.to_string()).await?;
|
ctx.reply_ephemeral(&e.to_string()).await?;
|
||||||
|
println!("SONGBIRD MANAGER ERROR: {}", e.to_string());
|
||||||
return Ok(())
|
return Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +113,7 @@ pub async fn play(ctx: Context<'_>,
|
||||||
let mut handler = handler_lock.lock().await;
|
let mut handler = handler_lock.lock().await;
|
||||||
|
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let request = HttpRequest::new(client, name);
|
let request = HttpRequest::new(client, station.url.clone());
|
||||||
let input = Input::from(request);
|
let input = Input::from(request);
|
||||||
handler.play_only_input(input);
|
handler.play_only_input(input);
|
||||||
}
|
}
|
||||||
|
@ -73,6 +122,11 @@ pub async fn play(ctx: Context<'_>,
|
||||||
return Ok(())
|
return Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut hash_map = ctx.data().playing_info.lock().await;
|
||||||
|
hash_map.insert(guild_id, PlayingInfoType::RadioInfo(station.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
ctx.reply_ephemeral("Done!").await?;
|
ctx.reply_ephemeral("Done!").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -82,17 +136,7 @@ async fn autocomplete_radio(
|
||||||
partial: &str
|
partial: &str
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
|
|
||||||
let api = {
|
let api = &ctx.data().radio_browser;
|
||||||
match &ctx.data().radio_browser {
|
|
||||||
Some(v) => v.clone(),
|
|
||||||
None => {
|
|
||||||
let Ok(radio_browser) = RadioBrowserAPI::new().await else {
|
|
||||||
return vec!["There was an error with radio API!".to_owned()]
|
|
||||||
};
|
|
||||||
radio_browser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let stations: Vec<String> = match api.get_stations()
|
let stations: Vec<String> = match api.get_stations()
|
||||||
.limit(MAX_ENTRIES)
|
.limit(MAX_ENTRIES)
|
||||||
|
@ -100,14 +144,14 @@ async fn autocomplete_radio(
|
||||||
.hidebroken(true)
|
.hidebroken(true)
|
||||||
.send().await {
|
.send().await {
|
||||||
Ok(stations) => {
|
Ok(stations) => {
|
||||||
stations.iter().map(|station|
|
stations.iter().map(|station: &radiobrowser::ApiStation|
|
||||||
format!("Name: {} Language: {} Bitrate: {}", station.name, station.countrycode, station.bitrate)
|
format!("Name: {} Country: {} Language: {}", station.name, station.country, station.language)
|
||||||
).collect()
|
).collect()
|
||||||
},
|
},
|
||||||
Err(_) => return vec!["".to_owned()]
|
Err(_) => return vec!["".to_owned()]
|
||||||
};
|
};
|
||||||
|
|
||||||
dbg!(&stations);
|
// dbg!(&stations);
|
||||||
|
|
||||||
return stations
|
return stations
|
||||||
// return vec![];
|
// return vec![];
|
||||||
|
@ -118,7 +162,11 @@ async fn autocomplete_radio(
|
||||||
// search with buttons and list in embed with direct links to stream
|
// search with buttons and list in embed with direct links to stream
|
||||||
// embed showing currently playing song
|
// embed showing currently playing song
|
||||||
|
|
||||||
|
// use trace::trace;
|
||||||
|
// trace::init_depth_var!();
|
||||||
|
|
||||||
// TODO: autocomplete radio stream URLs
|
// TODO: autocomplete radio stream URLs
|
||||||
|
// #[trace]
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
slash_command,
|
slash_command,
|
||||||
description_localized("en-US", "Search for a radio")
|
description_localized("en-US", "Search for a radio")
|
||||||
|
@ -129,27 +177,23 @@ pub async fn search(ctx: Context<'_>,
|
||||||
#[description = "Country: "]
|
#[description = "Country: "]
|
||||||
country: Option<String>,
|
country: Option<String>,
|
||||||
#[description = "Tag: "]
|
#[description = "Tag: "]
|
||||||
tag: Option<String>
|
tag: Option<String>,
|
||||||
|
#[description = "Number of entries on page: "]
|
||||||
|
entries: Option<NumberOfEntries>
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
let api = match &ctx.data().radio_browser {
|
let api = &ctx.data().radio_browser;
|
||||||
Some(v) => v.clone(),
|
|
||||||
None => {
|
|
||||||
let Ok(radio_browser) = RadioBrowserAPI::new().await else {
|
|
||||||
let _ = ctx.reply_ephemeral("There was an error with radio API!").await;
|
|
||||||
return Ok(())
|
|
||||||
};
|
|
||||||
radio_browser
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let station_search_builder = api.get_stations()
|
let entries = entries.unwrap_or(NumberOfEntries::Ten);
|
||||||
|
|
||||||
|
let search_builder = api.get_stations()
|
||||||
|
.limit(entries.name())
|
||||||
.name(name)
|
.name(name)
|
||||||
.tag(tag.unwrap_or("".to_owned()))
|
.tag(tag.unwrap_or("".to_owned()))
|
||||||
.country(country.unwrap_or("".to_owned()))
|
.country(country.unwrap_or("".to_owned()))
|
||||||
.hidebroken(true);
|
.hidebroken(true);
|
||||||
|
|
||||||
paginate_search(ctx, station_search_builder);
|
paginate_search_stations(&ctx, &search_builder, entries).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,83 +1,144 @@
|
||||||
use poise::{ChoiceParameter, CreateReply};
|
use poise::CreateReply;
|
||||||
use radiobrowser::StationSearchBuilder;
|
use radiobrowser::{ApiStation, StationSearchBuilder};
|
||||||
use serenity::{all::{CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse, CreateInteractionResponseMessage}, futures::SinkExt};
|
use regex::Regex;
|
||||||
|
use serenity::all::{CreateActionRow, CreateButton, CreateEmbed, CreateEmbedFooter, CreateInteractionResponse, CreateInteractionResponseMessage};
|
||||||
|
|
||||||
use crate::types::Context;
|
use crate::{commands::voice_types::NumberOfEntries, types::Context};
|
||||||
|
|
||||||
#[derive(ChoiceParameter)]
|
pub async fn paginate_search_stations(
|
||||||
pub enum WelcomeChoice {
|
ctx: &Context<'_>,
|
||||||
#[name = "5"]
|
search_builder: &StationSearchBuilder,
|
||||||
A,
|
limit: NumberOfEntries
|
||||||
#[name = "10"]
|
|
||||||
B,
|
|
||||||
#[name = "15"]
|
|
||||||
C
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn paginate_search(
|
|
||||||
ctx: Context<'_>,
|
|
||||||
search_builder: StationSearchBuilder,
|
|
||||||
) -> Result<(), serenity::Error> {
|
) -> Result<(), serenity::Error> {
|
||||||
// Define some unique identifiers for the navigation buttons
|
// Define some unique identifiers for the navigation buttons
|
||||||
let ctx_id = ctx.id();
|
let ctx_id = ctx.id();
|
||||||
let prev_button_id = format!("{}prev", ctx_id);
|
let prev_button_id = format!("{}prev", ctx_id);
|
||||||
let next_button_id = format!("{}next", ctx_id);
|
let next_button_id = format!("{}next", ctx_id);
|
||||||
|
|
||||||
let Ok(stations) = search_builder.send().await else {
|
let search_builder = search_builder;
|
||||||
let _ = ctx.reply("Something went wrong, try searching again").await;
|
|
||||||
|
let Ok(stations) = search_builder.clone().send().await else {
|
||||||
|
ctx.reply("Something went wrong, try searching again").await?;
|
||||||
return Ok(())
|
return Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut page = 0;
|
||||||
|
|
||||||
|
let embed = create_station_list_embed(&stations, page);
|
||||||
|
|
||||||
// Send the embed with the first page as content
|
// Send the embed with the first page as content
|
||||||
let reply = {
|
let reply = {
|
||||||
let components = CreateActionRow::Buttons(vec![
|
let components = CreateActionRow::Buttons(vec![
|
||||||
CreateButton::new(&prev_button_id).emoji('◀'),
|
CreateButton::new(&prev_button_id).emoji('◀'),
|
||||||
CreateButton::new(&next_button_id).emoji('▶'),
|
CreateButton::new(&next_button_id).emoji('▶'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CreateReply::default()
|
CreateReply::default()
|
||||||
.embed(CreateEmbed::new())
|
.embed(embed)
|
||||||
.components(vec![components])
|
.components(vec![components])
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.send(reply).await?;
|
ctx.send(reply).await?;
|
||||||
|
|
||||||
// Loop through incoming interactions with the navigation buttons
|
// Loop through incoming interactions with the navigation buttons
|
||||||
let mut current_page = 0;
|
let mut offset = 0;
|
||||||
while let Some(press) = serenity::collector::ComponentInteractionCollector::new(ctx)
|
let limit_int = limit as u32;
|
||||||
|
while let Some(press) = serenity::collector::ComponentInteractionCollector::new(ctx)
|
||||||
// We defined our button IDs to start with `ctx_id`. If they don't, some other command's
|
// We defined our button IDs to start with `ctx_id`. If they don't, some other command's
|
||||||
// button was pressed
|
// button was pressed
|
||||||
.filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string()))
|
.filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string()))
|
||||||
// Timeout when no navigation button has been pressed for 24 hours
|
// Timeout when no navigation button has been pressed for 24 hours
|
||||||
.timeout(std::time::Duration::from_secs(3600 * 24))
|
.timeout(std::time::Duration::from_secs(3600 * 24))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
// Depending on which button was pressed, go to next or previous page
|
// Depending on which button was pressed, go to next or previous page
|
||||||
if press.data.custom_id == next_button_id {
|
if press.data.custom_id == next_button_id {
|
||||||
current_page += 1;
|
offset += limit_int;
|
||||||
// TODO find a way to check end
|
page += 1;
|
||||||
// if current_page >= pages.len() {
|
|
||||||
// current_page = 0;
|
|
||||||
// }
|
|
||||||
} else if press.data.custom_id == prev_button_id {
|
} else if press.data.custom_id == prev_button_id {
|
||||||
// TODO find a way to get pages len
|
offset = if offset < limit_int { offset } else { offset - limit_int };
|
||||||
// current_page = current_page.checked_sub(1).unwrap_or(pages.len() - 1);
|
page = if page == 0 { 0 } else { page - 1 };
|
||||||
} else {
|
} else {
|
||||||
// This is an unrelated button interaction
|
// This is an unrelated button interaction
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Ok(mut stations) = search_builder.clone().offset(offset.to_string()).send().await else {
|
||||||
|
ctx.reply("Something went wrong, try searching again").await?;
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
if stations.is_empty() {
|
||||||
|
offset = 0;
|
||||||
|
page = 0;
|
||||||
|
|
||||||
|
let Ok(new_stations) = search_builder.clone().offset(offset.to_string()).send().await else {
|
||||||
|
ctx.reply("Something went wrong, try searching again").await?;
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
stations = new_stations;
|
||||||
|
}
|
||||||
|
|
||||||
|
let embed = create_station_list_embed(&stations, page);
|
||||||
|
|
||||||
// Update the message with the new page contents
|
// Update the message with the new page contents
|
||||||
press
|
press
|
||||||
.create_response(
|
.create_response(
|
||||||
ctx.serenity_context(),
|
ctx.serenity_context(),
|
||||||
CreateInteractionResponse::UpdateMessage(
|
CreateInteractionResponse::UpdateMessage(
|
||||||
CreateInteractionResponseMessage::new()
|
CreateInteractionResponseMessage::new()
|
||||||
.embed(CreateEmbed::new()),
|
.embed(embed),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_station_list_embed(stations: &Vec<ApiStation>, page: u32) -> CreateEmbed {
|
||||||
|
let result = CreateEmbed::new()
|
||||||
|
.fields(
|
||||||
|
stations.iter().map(|station| {
|
||||||
|
(
|
||||||
|
station.name.clone(),
|
||||||
|
format!("Country: {} Stream: {}",
|
||||||
|
station.country, station.url),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.footer(CreateEmbedFooter::new(format!("Page: {}", page + 1)));
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum LinkString {
|
||||||
|
Link,
|
||||||
|
String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn link_or_string(haystack: &str) -> LinkString {
|
||||||
|
let Ok(re) = Regex::new(r"^https?://([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$") else {
|
||||||
|
panic!("Wrong regex expression!");
|
||||||
|
};
|
||||||
|
|
||||||
|
return if re.is_match(haystack) { LinkString::Link } else { LinkString::String }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_radio_autocomplete(haystack: &str) -> Option<(String, String, String)> {
|
||||||
|
let Ok(re) = Regex::new(r"^Name: (.*) Country: (.*) Language: (.*)") else {
|
||||||
|
panic!("Wrong regex expression!");
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(captures) = re.captures(haystack) else {
|
||||||
|
return None
|
||||||
|
};
|
||||||
|
|
||||||
|
return Some((
|
||||||
|
String::from(&captures[1]),
|
||||||
|
String::from(&captures[2]),
|
||||||
|
String::from(&captures[3])
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
91
src/commands/voice/voice_types.rs
Normal file
91
src/commands/voice/voice_types.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use poise::ChoiceParameter;
|
||||||
|
use radiobrowser::ApiStation;
|
||||||
|
use serenity::{all::CreateEmbed, async_trait};
|
||||||
|
|
||||||
|
use crate::utils::utilities::hash_to_u32;
|
||||||
|
|
||||||
|
#[derive(ChoiceParameter, Debug)]
|
||||||
|
pub enum NumberOfEntries {
|
||||||
|
#[name = "5"]
|
||||||
|
Five = 5,
|
||||||
|
#[name = "10"]
|
||||||
|
Ten = 10,
|
||||||
|
#[name = "15"]
|
||||||
|
Fifteen = 15
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait GenerateEmbed {
|
||||||
|
async fn generate_embed(&self) -> CreateEmbed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct SpotifyInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub interpret: String,
|
||||||
|
// client
|
||||||
|
pub duration: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct GeneralInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub site: String,
|
||||||
|
// client
|
||||||
|
pub duration: Option<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl GenerateEmbed for GeneralInfo {
|
||||||
|
async fn generate_embed(&self) -> CreateEmbed {
|
||||||
|
CreateEmbed::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl GenerateEmbed for SpotifyInfo {
|
||||||
|
async fn generate_embed(&self) -> CreateEmbed {
|
||||||
|
CreateEmbed::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl GenerateEmbed for ApiStation {
|
||||||
|
async fn generate_embed(&self) -> CreateEmbed {
|
||||||
|
let embed= CreateEmbed::new()
|
||||||
|
.title("Current station:")
|
||||||
|
.thumbnail(&self.favicon)
|
||||||
|
.field("Name:", &self.name, true)
|
||||||
|
.field("Country:", &self.country, true)
|
||||||
|
.field("Language:", &self.language, true)
|
||||||
|
.field("Website:", &self.homepage, true)
|
||||||
|
.field("Stream:", &self.url, true)
|
||||||
|
.field("Tags:", &self.tags, false)
|
||||||
|
.color(hash_to_u32(&self.stationuuid));
|
||||||
|
|
||||||
|
embed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub struct PlayingInfo<T> {
|
||||||
|
// pub data: T,
|
||||||
|
// pub create_embed: Box<dyn Fn(T) -> CreateEmbed + Send + Sync>
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum PlayingInfoType {
|
||||||
|
SpotifyInfo(SpotifyInfo),
|
||||||
|
RadioInfo(ApiStation),
|
||||||
|
GeneralInfo(GeneralInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayingInfoType {
|
||||||
|
pub async fn generate_embed(&self) -> CreateEmbed {
|
||||||
|
match self {
|
||||||
|
PlayingInfoType::SpotifyInfo(v) => v.generate_embed().await,
|
||||||
|
PlayingInfoType::RadioInfo(v) => v.generate_embed().await,
|
||||||
|
PlayingInfoType::GeneralInfo(v) => v.generate_embed().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
use poise::{CreateReply};
|
use serenity::all::{ChannelId, GuildId};
|
||||||
use serenity::all::{ChannelId, CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse, CreateInteractionResponseMessage, Embed, GuildId};
|
|
||||||
use serenity::async_trait;
|
use serenity::async_trait;
|
||||||
|
|
||||||
use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler};
|
use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -6,6 +7,7 @@ use std::error;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use serenity::async_trait;
|
use serenity::async_trait;
|
||||||
|
use serenity::futures::lock::Mutex;
|
||||||
use serenity::prelude::GatewayIntents;
|
use serenity::prelude::GatewayIntents;
|
||||||
use serenity::client::Context;
|
use serenity::client::Context;
|
||||||
use serenity::model::gateway::Ready;
|
use serenity::model::gateway::Ready;
|
||||||
|
@ -19,6 +21,8 @@ use dotenv::dotenv;
|
||||||
use songbird::SerenityInit;
|
use songbird::SerenityInit;
|
||||||
use tokio_cron_scheduler::{JobScheduler, Job};
|
use tokio_cron_scheduler::{JobScheduler, Job};
|
||||||
|
|
||||||
|
use radiobrowser::RadioBrowserAPI;
|
||||||
|
|
||||||
mod message_handler;
|
mod message_handler;
|
||||||
use message_handler::handle;
|
use message_handler::handle;
|
||||||
|
|
||||||
|
@ -90,6 +94,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
commands::radio::radio(),
|
commands::radio::radio(),
|
||||||
commands::general_player::play(),
|
commands::general_player::play(),
|
||||||
commands::player_common::disconnect(),
|
commands::player_common::disconnect(),
|
||||||
|
commands::player_common::playing()
|
||||||
],
|
],
|
||||||
prefix_options: poise::PrefixFrameworkOptions {
|
prefix_options: poise::PrefixFrameworkOptions {
|
||||||
prefix: Some("/".into()),
|
prefix: Some("/".into()),
|
||||||
|
@ -131,7 +136,8 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
Ok(Data {
|
Ok(Data {
|
||||||
http_client: HttpClient::new(),
|
http_client: HttpClient::new(),
|
||||||
radio_browser: RadioBrowserAPI::new().await.ok()
|
radio_browser: RadioBrowserAPI::new().await?,
|
||||||
|
playing_info: Mutex::new(HashMap::new())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
30
src/types.rs
30
src/types.rs
|
@ -1,23 +1,37 @@
|
||||||
// pub struct Data {}
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use std::{ffi::OsString, path::PathBuf};
|
|
||||||
|
|
||||||
|
use poise::CreateReply;
|
||||||
use radiobrowser::RadioBrowserAPI;
|
use radiobrowser::RadioBrowserAPI;
|
||||||
use reqwest::Client as HttpClient;
|
use reqwest::Client as HttpClient;
|
||||||
|
use serenity::{all::{futures::lock::Mutex, GuildId}, async_trait};
|
||||||
|
|
||||||
|
use crate::commands::voice_types::PlayingInfoType;
|
||||||
|
|
||||||
// TODO: this should be probably expanded also to be used by multiple servers and commands
|
// TODO: this should be probably expanded also to be used by multiple servers and commands
|
||||||
// radio and spotify commands always create new HttpClient
|
// radio and spotify commands always create new HttpClient
|
||||||
|
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
pub http_client: HttpClient,
|
pub http_client: HttpClient,
|
||||||
pub radio_browser: Option<RadioBrowserAPI>,
|
pub radio_browser: RadioBrowserAPI,
|
||||||
|
pub playing_info: Mutex<HashMap<GuildId, PlayingInfoType>>
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
// replace () with Data if you ever need to store some additional data
|
// replace () with Data if you ever need to store some additional data
|
||||||
pub type Context<'a> = poise::Context<'a, Data, Error>;
|
pub type Context<'a> = poise::Context<'a, Data, Error>;
|
||||||
|
|
||||||
pub struct Track {
|
/// Trait to extend `Context` with additional methods.
|
||||||
pub path: PathBuf,
|
#[async_trait]
|
||||||
pub name: OsString,
|
pub trait ContextExt {
|
||||||
|
async fn reply_ephemeral(&self, content: &str) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ContextExt for Context<'_> {
|
||||||
|
async fn reply_ephemeral(&self, content: &str) -> anyhow::Result<()> {
|
||||||
|
self.send(CreateReply::default()
|
||||||
|
.ephemeral(true)
|
||||||
|
.content(content)
|
||||||
|
).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
pub use utilities::*;
|
||||||
|
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod utilities;
|
pub mod utilities;
|
||||||
pub mod gifs;
|
pub mod gifs;
|
||||||
pub mod poise_context_extension;
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
use poise::CreateReply;
|
|
||||||
use serenity::async_trait;
|
|
||||||
|
|
||||||
use crate::types::Context;
|
|
||||||
|
|
||||||
|
|
||||||
/// Trait to extend `Context` with additional methods.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait ContextExt {
|
|
||||||
async fn reply_ephemeral(&self, content: &str) -> anyhow::Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ContextExt for Context<'_> {
|
|
||||||
async fn reply_ephemeral(&self, content: &str) -> anyhow::Result<()> {
|
|
||||||
self.send(CreateReply::default()
|
|
||||||
.ephemeral(true)
|
|
||||||
.content(content)
|
|
||||||
).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,13 @@
|
||||||
use std::{fs, io, path::Path, sync::Arc, vec};
|
use std::{fs, hash::{DefaultHasher, Hash, Hasher}, io, path::Path, sync::Arc, vec};
|
||||||
|
|
||||||
use serenity::{all::{ChannelId, ChannelType, CreateMessage, GuildId, GuildRef, Message}, http::Http};
|
use serenity::{all::{ChannelId, ChannelType, CreateMessage, GuildId, GuildRef, Message}, http::Http};
|
||||||
|
|
||||||
|
use poise::CreateReply;
|
||||||
|
use serenity::async_trait;
|
||||||
|
|
||||||
|
use crate::types::Context;
|
||||||
|
|
||||||
|
|
||||||
pub async fn get_system_channel(guild_id: GuildId, http: &Http) -> anyhow::Result<ChannelId> {
|
pub async fn get_system_channel(guild_id: GuildId, http: &Http) -> anyhow::Result<ChannelId> {
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
return http.get_guild(guild_id).await?.system_channel_id
|
return http.get_guild(guild_id).await?.system_channel_id
|
||||||
|
@ -58,3 +64,10 @@ pub fn get_local_songs(partial: &str) -> io::Result<Vec<String>> {
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hash_to_u32<T: Hash>(from: &T) -> u32 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
from.hash(&mut hasher);
|
||||||
|
let hash_val = hasher.finish();
|
||||||
|
(hash_val & 0xFFFFFF) as u32
|
||||||
|
}
|
||||||
|
|
2
tenor-v2
2
tenor-v2
|
@ -1 +1 @@
|
||||||
Subproject commit fd8c8bde1be41116fe398776c45625a19ec10281
|
Subproject commit 66cf5cc388af947846caf5b7118257df01d60725
|
Loading…
Reference in a new issue