use poise to register slash commands, separate files more
This commit is contained in:
71
Cargo.lock
generated
71
Cargo.lock
generated
@@ -380,6 +380,41 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.14.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.14.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.14.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.4.0"
|
version = "5.4.0"
|
||||||
@@ -429,6 +464,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"log",
|
"log",
|
||||||
|
"poise",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serenity",
|
"serenity",
|
||||||
"simple_logger",
|
"simple_logger",
|
||||||
@@ -814,6 +850,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -1216,6 +1258,35 @@ dependencies = [
|
|||||||
"pnet_base",
|
"pnet_base",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "poise"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "git+https://github.com/serenity-rs/poise?branch=serenity-next#50ddbbb85a66068e3de33a9ba3c6ff72e62596b6"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"derivative",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"poise_macros",
|
||||||
|
"regex",
|
||||||
|
"serenity",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "poise_macros"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "git+https://github.com/serenity-rs/poise?branch=serenity-next#50ddbbb85a66068e3de33a9ba3c6ff72e62596b6"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "poly1305"
|
name = "poly1305"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ features = [ "rustls-tls" ]
|
|||||||
version = "0.11"
|
version = "0.11"
|
||||||
git = "https://github.com/serenity-rs/serenity"
|
git = "https://github.com/serenity-rs/serenity"
|
||||||
branch = "next"
|
branch = "next"
|
||||||
features = [ "client", "standard_framework", "voice", "rustls_backend" ]
|
features = [ "client", "voice", "rustls_backend" ]
|
||||||
|
|
||||||
[dependencies.songbird]
|
[dependencies.songbird]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
@@ -36,6 +36,11 @@ git = "https://github.com/serenity-rs/songbird"
|
|||||||
branch = "next"
|
branch = "next"
|
||||||
features = [ "builtin-queue" ]
|
features = [ "builtin-queue" ]
|
||||||
|
|
||||||
|
[dependencies.poise]
|
||||||
|
version = "0.4"
|
||||||
|
git = "https://github.com/serenity-rs/poise"
|
||||||
|
branch = "serenity-next"
|
||||||
|
|
||||||
[dependencies.symphonia]
|
[dependencies.symphonia]
|
||||||
version = "0.5"
|
version = "0.5"
|
||||||
features = ["aac", "mp3"]
|
features = ["aac", "mp3"]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ in rustPlatform.buildRustPackage {
|
|||||||
"sunk-0.1.2" = "sha256-gxDinyzKHxph2D5OZ9TevVEWtsEnnFpu4BfkkBnRMx4=";
|
"sunk-0.1.2" = "sha256-gxDinyzKHxph2D5OZ9TevVEWtsEnnFpu4BfkkBnRMx4=";
|
||||||
"serenity-0.11.5" = "sha256-10s0kflNYEMwUXAgrh6d1IUk3ZRSCkAilz9m1lVhXhA=";
|
"serenity-0.11.5" = "sha256-10s0kflNYEMwUXAgrh6d1IUk3ZRSCkAilz9m1lVhXhA=";
|
||||||
"songbird-0.3.2" = "sha256-8wzCcV9W6K0MHqZ8yhTIMjh165NV8OQ9zlgrRrIhlOI=";
|
"songbird-0.3.2" = "sha256-8wzCcV9W6K0MHqZ8yhTIMjh165NV8OQ9zlgrRrIhlOI=";
|
||||||
|
"poise-0.4.1" = "sha256-MPfg3miu9tesGKChaR8vz8RZA45I5uJs60MtgxDJryw=";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
use crate::handles::SubsonicClientHandle;
|
use crate::discord;
|
||||||
use anyhow::{Context as ErrContext, Result};
|
use anyhow::{Context as ErrContext, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use serenity::{
|
use serenity::client::Client as DiscordClient;
|
||||||
client::Client as DiscordClient, framework::standard::StandardFramework,
|
|
||||||
prelude::GatewayIntents,
|
|
||||||
};
|
|
||||||
use simple_logger::SimpleLogger;
|
use simple_logger::SimpleLogger;
|
||||||
use songbird::SerenityInit;
|
|
||||||
use sunk::Client as SubsonicClient;
|
use sunk::Client as SubsonicClient;
|
||||||
|
|
||||||
use crate::discord::{after_hook, Handler, GENERAL_GROUP};
|
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
#[derive(Parser, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[clap(long, env = "DISCONIC_SUBSONIC_URL")]
|
#[clap(long, env = "DISCONIC_SUBSONIC_URL")]
|
||||||
@@ -20,6 +14,8 @@ pub struct Config {
|
|||||||
subsonic_user: String,
|
subsonic_user: String,
|
||||||
#[clap(long, env = "DISCONIC_SUBSONIC_PASSWORD")]
|
#[clap(long, env = "DISCONIC_SUBSONIC_PASSWORD")]
|
||||||
subsonic_password: String,
|
subsonic_password: String,
|
||||||
|
#[clap(long, env = "DISCONIC_DISCORD_GUILD")]
|
||||||
|
discord_guild: Option<u64>,
|
||||||
#[clap(long, env = "DISCONIC_DISCORD_TOKEN")]
|
#[clap(long, env = "DISCONIC_DISCORD_TOKEN")]
|
||||||
discord_token: String,
|
discord_token: String,
|
||||||
#[clap(long, env = "DISCONIC_LOG_LEVEL", default_value = "warn")]
|
#[clap(long, env = "DISCONIC_LOG_LEVEL", default_value = "warn")]
|
||||||
@@ -27,22 +23,8 @@ pub struct Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub async fn discord(&self, ss: SubsonicClient) -> Result<DiscordClient> {
|
pub async fn discord(&self, subsonic_client: SubsonicClient) -> Result<DiscordClient> {
|
||||||
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
|
discord::create_client(&self.discord_token, self.discord_guild, subsonic_client).await
|
||||||
let framework = StandardFramework::new()
|
|
||||||
.group(&GENERAL_GROUP)
|
|
||||||
.after(after_hook);
|
|
||||||
|
|
||||||
framework.configure(|c| c.prefix("~"));
|
|
||||||
|
|
||||||
let client = DiscordClient::builder(&self.discord_token, intents)
|
|
||||||
.event_handler(Handler)
|
|
||||||
.framework(framework)
|
|
||||||
.type_map_insert::<SubsonicClientHandle>(ss)
|
|
||||||
.register_songbird()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(client)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn subsonic(&self) -> Result<SubsonicClient> {
|
pub async fn subsonic(&self) -> Result<SubsonicClient> {
|
||||||
|
|||||||
409
src/discord.rs
409
src/discord.rs
@@ -1,409 +0,0 @@
|
|||||||
use anyhow::{anyhow, Context as ErrContext, Result};
|
|
||||||
use serenity::{
|
|
||||||
all::{ChannelId, GuildId},
|
|
||||||
async_trait,
|
|
||||||
client::{Context, EventHandler},
|
|
||||||
framework::standard::{
|
|
||||||
macros::{command, group, hook},
|
|
||||||
Args, CommandResult,
|
|
||||||
},
|
|
||||||
model::{channel::Message, gateway::Ready},
|
|
||||||
utils::MessageBuilder,
|
|
||||||
};
|
|
||||||
use songbird::{
|
|
||||||
input::HttpRequest,
|
|
||||||
tracks::{Track, TrackHandle},
|
|
||||||
Songbird,
|
|
||||||
};
|
|
||||||
use sunk::{
|
|
||||||
search::{self, SearchPage},
|
|
||||||
song::Song,
|
|
||||||
Streamable,
|
|
||||||
};
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::handles::{SubsonicClientHandle, SubsonicSongHandle};
|
|
||||||
|
|
||||||
#[group]
|
|
||||||
#[commands(
|
|
||||||
song, random, skip, stop, pause, resume, queue, nowplaying, remove, album, join, leave
|
|
||||||
)]
|
|
||||||
pub struct General;
|
|
||||||
|
|
||||||
pub struct Handler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl EventHandler for Handler {
|
|
||||||
async fn ready(&self, _: Context, ready: Ready) {
|
|
||||||
log::info!("{} is connected!", ready.user.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[hook]
|
|
||||||
pub async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: CommandResult) {
|
|
||||||
if let Err(why) = error {
|
|
||||||
msg.reply(&ctx.http, format!("{:?}", why)).await.ok();
|
|
||||||
eprintln!("{}: {:?}", cmd_name, why,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
|
|
||||||
let guild = get_guild(ctx, msg)?;
|
|
||||||
let manager = get_manager(ctx).await?;
|
|
||||||
manager.remove(guild).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
|
|
||||||
let guild = get_guild(ctx, msg)?;
|
|
||||||
let channel = get_channel(ctx, msg)?;
|
|
||||||
let manager = get_manager(ctx).await?;
|
|
||||||
let _handler = manager.join(guild, channel).await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
#[aliases(s, p, play)]
|
|
||||||
/// Play a named song
|
|
||||||
async fn song(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
||||||
let data = ctx.data.read().await;
|
|
||||||
let music_client = data
|
|
||||||
.get::<SubsonicClientHandle>()
|
|
||||||
.expect("Couldn't retrieve music client");
|
|
||||||
|
|
||||||
let search_size = SearchPage::new().with_size(1);
|
|
||||||
let ignore = search::NONE;
|
|
||||||
|
|
||||||
let result = music_client
|
|
||||||
.search(args.rest(), ignore, ignore, search_size)
|
|
||||||
.await
|
|
||||||
.with_context(|| anyhow!("Could not search for song"))?
|
|
||||||
.songs;
|
|
||||||
|
|
||||||
let song = result
|
|
||||||
.first()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.ok_or_else(|| anyhow!("No song matching search found"))?;
|
|
||||||
|
|
||||||
log::info!("Found song {song:?}");
|
|
||||||
queue_song(ctx, msg, &song, music_client).await?;
|
|
||||||
|
|
||||||
let message = format!(
|
|
||||||
"Added **{} - {}** to the queue",
|
|
||||||
song.title,
|
|
||||||
song.artist.as_deref().unwrap_or_default(),
|
|
||||||
);
|
|
||||||
msg.reply(&ctx.http, &MessageBuilder::new().push(&message).build())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
#[aliases(a, album)]
|
|
||||||
/// Play a named album
|
|
||||||
async fn album(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
||||||
let data = ctx.data.read().await;
|
|
||||||
let music_client = data
|
|
||||||
.get::<SubsonicClientHandle>()
|
|
||||||
.expect("Couldn't retrieve music client");
|
|
||||||
|
|
||||||
let search_size = SearchPage::new().with_size(1);
|
|
||||||
let ignore = search::NONE;
|
|
||||||
|
|
||||||
let result = music_client
|
|
||||||
.search(args.rest(), ignore, search_size, ignore)
|
|
||||||
.await?
|
|
||||||
.albums;
|
|
||||||
|
|
||||||
let album = result
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| anyhow!("No albums matching search found"))?;
|
|
||||||
|
|
||||||
let songs = album.songs(music_client).await?;
|
|
||||||
for song in songs.iter() {
|
|
||||||
queue_song(ctx, msg, &song, music_client).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = format!(
|
|
||||||
"Added album **{} - {}** ({} songs) to the queue",
|
|
||||||
album.name,
|
|
||||||
album.artist.as_deref().unwrap_or_default(),
|
|
||||||
songs.len(),
|
|
||||||
);
|
|
||||||
msg.reply(&ctx.http, &MessageBuilder::new().push(&message).build())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
#[aliases(r, rand)]
|
|
||||||
/// Play a random song
|
|
||||||
async fn random(ctx: &Context, msg: &Message) -> CommandResult {
|
|
||||||
let data = ctx.data.read().await;
|
|
||||||
let music_client = data
|
|
||||||
.get::<SubsonicClientHandle>()
|
|
||||||
.expect("Couldn't retrieve music client");
|
|
||||||
|
|
||||||
let result = Song::random(music_client, 1).await?;
|
|
||||||
|
|
||||||
let song: Song = result
|
|
||||||
.first()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.ok_or_else(|| anyhow!("No song matching search found"))?;
|
|
||||||
queue_song(ctx, msg, &song, music_client).await?;
|
|
||||||
|
|
||||||
let message = format!(
|
|
||||||
"Added **{} - {}** to the queue",
|
|
||||||
song.title,
|
|
||||||
song.artist.as_deref().unwrap_or_default(),
|
|
||||||
);
|
|
||||||
msg.reply(&ctx.http, &MessageBuilder::new().push(&message).build())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
/// Skip song(s)
|
|
||||||
async fn skip(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
||||||
let call = get_call(ctx, msg).await?;
|
|
||||||
let handler = call.lock().await;
|
|
||||||
let n = args.rest().parse().unwrap_or(1);
|
|
||||||
|
|
||||||
let queue = handler.queue();
|
|
||||||
for _ in 0..(n - 1) {
|
|
||||||
queue.dequeue(1).ok_or_else(|| anyhow!("Song not found"))?;
|
|
||||||
}
|
|
||||||
queue.skip()?;
|
|
||||||
|
|
||||||
msg.reply(&ctx.http, &format!("{n} song(s) skipped"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
/// Clear queue and stop playing
|
|
||||||
async fn stop(ctx: &Context, msg: &Message) -> CommandResult {
|
|
||||||
let call = get_call(ctx, msg).await?;
|
|
||||||
let handler = call.lock().await;
|
|
||||||
|
|
||||||
let queue = handler.queue();
|
|
||||||
queue.stop();
|
|
||||||
|
|
||||||
msg.reply(&ctx.http, "Stopped playing").await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
/// Pause playing current song
|
|
||||||
async fn pause(ctx: &Context, msg: &Message) -> CommandResult {
|
|
||||||
let call = get_call(ctx, msg).await?;
|
|
||||||
let handler = call.lock().await;
|
|
||||||
|
|
||||||
let queue = handler.queue();
|
|
||||||
let current = queue
|
|
||||||
.current()
|
|
||||||
.ok_or_else(|| anyhow!("Not currently playing"))?;
|
|
||||||
current.pause()?;
|
|
||||||
|
|
||||||
msg.reply(&ctx.http, "Paused playing").await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
#[aliases(resume)]
|
|
||||||
/// Resume playing current song
|
|
||||||
async fn resume(ctx: &Context, msg: &Message) -> CommandResult {
|
|
||||||
let call = get_call(ctx, msg).await?;
|
|
||||||
let handler = call.lock().await;
|
|
||||||
|
|
||||||
let queue = handler.queue();
|
|
||||||
queue.resume()?;
|
|
||||||
|
|
||||||
msg.reply(&ctx.http, "Resumed playing").await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
#[aliases(nowplaying, now, np, playing)]
|
|
||||||
/// Show currently playing song
|
|
||||||
async fn nowplaying(ctx: &Context, msg: &Message) -> CommandResult {
|
|
||||||
let call = get_call(ctx, msg).await?;
|
|
||||||
let handler = call.lock().await;
|
|
||||||
|
|
||||||
let queue = handler.queue();
|
|
||||||
let current = queue
|
|
||||||
.current()
|
|
||||||
.ok_or_else(|| anyhow!("Not currently playing"))?;
|
|
||||||
|
|
||||||
let song = get_song(¤t).await?;
|
|
||||||
let text = format!(
|
|
||||||
"**Currently playing**: {} - {}",
|
|
||||||
song.title,
|
|
||||||
song.artist.as_deref().unwrap_or_default()
|
|
||||||
);
|
|
||||||
msg.reply(&ctx.http, text).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
#[aliases(q)]
|
|
||||||
/// Show song queue
|
|
||||||
async fn queue(ctx: &Context, msg: &Message) -> CommandResult {
|
|
||||||
let call = get_call(ctx, msg).await?;
|
|
||||||
let handler = call.lock().await;
|
|
||||||
|
|
||||||
let current_queue = handler.queue().current_queue();
|
|
||||||
|
|
||||||
let text = if current_queue.is_empty() {
|
|
||||||
"No songs queued".into()
|
|
||||||
} else {
|
|
||||||
let mut text = String::new();
|
|
||||||
let mut queue = current_queue.iter().enumerate();
|
|
||||||
|
|
||||||
let (_, track) = queue.next().unwrap();
|
|
||||||
let song = get_song(track).await?;
|
|
||||||
text.push_str(&format!(
|
|
||||||
"**Currently playing**: {} - {}\n\n",
|
|
||||||
song.title,
|
|
||||||
song.artist.as_deref().unwrap_or_default()
|
|
||||||
));
|
|
||||||
|
|
||||||
text.push_str("**Next songs in queue**:\n");
|
|
||||||
for (i, track) in queue {
|
|
||||||
let song = get_song(track).await?;
|
|
||||||
let song_text = format!(
|
|
||||||
"**{}.** {} - {}",
|
|
||||||
i,
|
|
||||||
song.title,
|
|
||||||
song.artist.as_deref().unwrap_or_default()
|
|
||||||
);
|
|
||||||
text.push_str(&song_text);
|
|
||||||
text.push('\n');
|
|
||||||
}
|
|
||||||
text
|
|
||||||
};
|
|
||||||
|
|
||||||
msg.reply(&ctx.http, text).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
#[only_in(guilds)]
|
|
||||||
/// Remove song from queue, given id
|
|
||||||
async fn remove(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
||||||
let call = get_call(ctx, msg).await?;
|
|
||||||
let handler = call.lock().await;
|
|
||||||
|
|
||||||
let index: usize = args.single()?;
|
|
||||||
|
|
||||||
let queue = handler.queue();
|
|
||||||
let track = queue
|
|
||||||
.dequeue(index - 1)
|
|
||||||
.ok_or_else(|| anyhow!("Song not found"))?;
|
|
||||||
let song = get_song(&track).await?;
|
|
||||||
let text = format!("Removed track: {}", song.title);
|
|
||||||
msg.reply(&ctx.http, text).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================
|
|
||||||
// ==========================
|
|
||||||
// ==========================
|
|
||||||
|
|
||||||
async fn queue_song(
|
|
||||||
ctx: &Context,
|
|
||||||
msg: &Message,
|
|
||||||
song: &Song,
|
|
||||||
client: &sunk::Client,
|
|
||||||
) -> Result<()> {
|
|
||||||
let call = get_call(ctx, msg).await?;
|
|
||||||
let mut handler = call.lock().await;
|
|
||||||
|
|
||||||
let track = load_song(song, client).await?;
|
|
||||||
let track_handle = handler.enqueue(track).await;
|
|
||||||
let mut type_map = track_handle.typemap().write().await;
|
|
||||||
type_map.insert::<SubsonicSongHandle>(song.clone());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_guild(ctx: &Context, msg: &Message) -> Result<GuildId> {
|
|
||||||
let guild = msg.guild(&ctx.cache).unwrap();
|
|
||||||
Ok(guild.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_channel(ctx: &Context, msg: &Message) -> Result<ChannelId> {
|
|
||||||
let guild = msg.guild(&ctx.cache).unwrap();
|
|
||||||
let channel_id = guild
|
|
||||||
.voice_states
|
|
||||||
.get(&msg.author.id)
|
|
||||||
.and_then(|voice_state| voice_state.channel_id)
|
|
||||||
.ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?;
|
|
||||||
|
|
||||||
Ok(channel_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_call(ctx: &Context, msg: &Message) -> Result<Arc<Mutex<songbird::Call>>> {
|
|
||||||
let manager = get_manager(ctx).await?;
|
|
||||||
let guild = get_guild(ctx, msg)?;
|
|
||||||
let call = manager.get(guild);
|
|
||||||
|
|
||||||
if let Some(c) = call {
|
|
||||||
Ok(c)
|
|
||||||
} else {
|
|
||||||
let channel = get_channel(ctx, msg)?;
|
|
||||||
log::warn!("Not in a voice channel, trying to join");
|
|
||||||
let _handler = manager.join(guild, channel).await;
|
|
||||||
manager
|
|
||||||
.get(guild)
|
|
||||||
.ok_or_else(|| anyhow!("Not in a voice channel, try running 'join'"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_song(track: &TrackHandle) -> Result<Song> {
|
|
||||||
let song = track
|
|
||||||
.typemap()
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.get::<SubsonicSongHandle>()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.ok_or_else(|| anyhow!("Sound information not found"))?;
|
|
||||||
Ok(song)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_manager(ctx: &Context) -> Result<Arc<Songbird>> {
|
|
||||||
let manager = songbird::get(ctx)
|
|
||||||
.await
|
|
||||||
.ok_or_else(|| anyhow!("Couldn't start manager"))?;
|
|
||||||
Ok(manager)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_song(song: &Song, client: &sunk::Client) -> Result<Track> {
|
|
||||||
log::info!("Loading song {song:?}");
|
|
||||||
let url = song.stream_url(client)?;
|
|
||||||
let track: Track = HttpRequest::new(client.reqclient.clone(), url).into();
|
|
||||||
Ok(track)
|
|
||||||
}
|
|
||||||
75
src/discord/client.rs
Normal file
75
src/discord/client.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use crate::discord::{commands, Data};
|
||||||
|
use anyhow::Result;
|
||||||
|
use poise::samples::create_application_commands;
|
||||||
|
use serenity::{all::GuildId, client::Client as DiscordClient, prelude::GatewayIntents};
|
||||||
|
use songbird::SerenityInit;
|
||||||
|
use sunk::Client as SubsonicClient;
|
||||||
|
|
||||||
|
async fn on_error(error: poise::FrameworkError<'_, Data, anyhow::Error>) {
|
||||||
|
let context = error.ctx();
|
||||||
|
let message = match error {
|
||||||
|
poise::FrameworkError::Command { error, ctx } => {
|
||||||
|
format!(
|
||||||
|
"Error while running `{}`: **{:?}**",
|
||||||
|
ctx.command().name,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
poise::FrameworkError::Listener { error, event, .. } => {
|
||||||
|
format!(
|
||||||
|
"Error on event `{:?}`: **{:?}**",
|
||||||
|
event.snake_case_name(),
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
error => {
|
||||||
|
if let Err(e) = poise::builtins::on_error(error).await {
|
||||||
|
format!("Error: **{}**", e)
|
||||||
|
} else {
|
||||||
|
format!("Error: **unknown**")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::error!("{message}");
|
||||||
|
if let Some(ctx) = context {
|
||||||
|
ctx.say(message).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_client(
|
||||||
|
token: &str,
|
||||||
|
guild_id: Option<u64>,
|
||||||
|
subsonic_client: SubsonicClient,
|
||||||
|
) -> Result<DiscordClient> {
|
||||||
|
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
|
||||||
|
let commands = commands::commands();
|
||||||
|
let data = Data { subsonic_client };
|
||||||
|
|
||||||
|
let create_commands = create_application_commands(&commands);
|
||||||
|
let options = poise::FrameworkOptions {
|
||||||
|
commands,
|
||||||
|
on_error: |e| Box::pin(on_error(e)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let framework = poise::Framework::new(options, move |ctx, _ready, _framework| {
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(id) = guild_id {
|
||||||
|
let guild = GuildId::new(id);
|
||||||
|
guild
|
||||||
|
.set_commands(&ctx.http, create_commands)
|
||||||
|
.await
|
||||||
|
.expect("Failed to register command");
|
||||||
|
} else {
|
||||||
|
log::warn!("Guild ID not configured. You'll have to run 'register' to register the slash commands.")
|
||||||
|
}
|
||||||
|
log::info!("Registered commands");
|
||||||
|
Ok(data)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let client = DiscordClient::builder(token, intents)
|
||||||
|
.framework(framework)
|
||||||
|
.register_songbird()
|
||||||
|
.await?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
41
src/discord/commands/album.rs
Normal file
41
src/discord/commands/album.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use poise::command;
|
||||||
|
use sunk::search;
|
||||||
|
|
||||||
|
use crate::discord::{common::queue_song, Context};
|
||||||
|
|
||||||
|
/// Search for an album, and queue all its songs
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn album(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "What to search for"] query: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
let music_client = &ctx.data().subsonic_client;
|
||||||
|
|
||||||
|
let search_size = search::SearchPage::new().with_size(1);
|
||||||
|
let ignore = search::NONE;
|
||||||
|
|
||||||
|
let result = music_client
|
||||||
|
.search(&query, ignore, search_size, ignore)
|
||||||
|
.await?
|
||||||
|
.albums;
|
||||||
|
|
||||||
|
let album = result
|
||||||
|
.first()
|
||||||
|
.ok_or_else(|| anyhow!("No albums matching search found"))?;
|
||||||
|
|
||||||
|
let songs = album.songs(&music_client).await?;
|
||||||
|
for song in songs.iter() {
|
||||||
|
queue_song(ctx, &song, &music_client).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"Added album **{} - {}** ({} songs) to the queue",
|
||||||
|
album.name,
|
||||||
|
album.artist.as_deref().unwrap_or_default(),
|
||||||
|
songs.len(),
|
||||||
|
);
|
||||||
|
ctx.say(message).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
20
src/discord/commands/join.rs
Normal file
20
src/discord/commands/join.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use poise::command;
|
||||||
|
|
||||||
|
use crate::discord::{
|
||||||
|
common::{get_channel, get_guild, get_manager},
|
||||||
|
Context,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Join the voice chat where the caller is
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn join(ctx: Context<'_>) -> Result<()> {
|
||||||
|
let guild = get_guild(ctx);
|
||||||
|
let channel = get_channel(ctx)?;
|
||||||
|
let manager = get_manager(ctx).await?;
|
||||||
|
log::info!("Got manager: {manager:?}");
|
||||||
|
let call = manager.join(guild.id, channel).await?;
|
||||||
|
log::info!("Got call: {call:?}");
|
||||||
|
ctx.say("Hi! Try '/song' or '/album' to start").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
17
src/discord/commands/leave.rs
Normal file
17
src/discord/commands/leave.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use poise::command;
|
||||||
|
|
||||||
|
use crate::discord::{
|
||||||
|
common::{get_guild, get_manager},
|
||||||
|
Context,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Leave the voice chat
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn leave(ctx: Context<'_>) -> Result<()> {
|
||||||
|
let guild = get_guild(ctx);
|
||||||
|
let manager = get_manager(ctx).await?;
|
||||||
|
manager.remove(guild.id).await?;
|
||||||
|
ctx.say("Bye!").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
36
src/discord/commands/mod.rs
Normal file
36
src/discord/commands/mod.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
pub mod album;
|
||||||
|
pub mod join;
|
||||||
|
pub mod leave;
|
||||||
|
pub mod nowplaying;
|
||||||
|
pub mod pause;
|
||||||
|
pub mod queue;
|
||||||
|
pub mod random;
|
||||||
|
pub mod remove;
|
||||||
|
pub mod resume;
|
||||||
|
pub mod skip;
|
||||||
|
pub mod song;
|
||||||
|
pub mod stop;
|
||||||
|
|
||||||
|
#[poise::command(prefix_command)]
|
||||||
|
pub async fn register(ctx: crate::discord::Context<'_>) -> Result<(), anyhow::Error> {
|
||||||
|
poise::builtins::register_application_commands_buttons(ctx).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commands() -> Vec<poise::Command<super::Data, anyhow::Error>> {
|
||||||
|
vec![
|
||||||
|
register(),
|
||||||
|
album::album(),
|
||||||
|
join::join(),
|
||||||
|
leave::leave(),
|
||||||
|
nowplaying::nowplaying(),
|
||||||
|
pause::pause(),
|
||||||
|
queue::queue(),
|
||||||
|
random::random(),
|
||||||
|
remove::remove(),
|
||||||
|
resume::resume(),
|
||||||
|
skip::skip(),
|
||||||
|
song::song(),
|
||||||
|
stop::stop(),
|
||||||
|
]
|
||||||
|
}
|
||||||
29
src/discord/commands/nowplaying.rs
Normal file
29
src/discord/commands/nowplaying.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use poise::command;
|
||||||
|
|
||||||
|
use crate::discord::{
|
||||||
|
common::{get_call, get_song},
|
||||||
|
Context,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Show what's currently playing
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn nowplaying(ctx: Context<'_>) -> Result<()> {
|
||||||
|
let call = get_call(ctx).await?;
|
||||||
|
let handler = call.lock().await;
|
||||||
|
|
||||||
|
let queue = handler.queue();
|
||||||
|
let current = queue
|
||||||
|
.current()
|
||||||
|
.ok_or_else(|| anyhow!("Not currently playing"))?;
|
||||||
|
|
||||||
|
let song = get_song(¤t).await?;
|
||||||
|
let text = format!(
|
||||||
|
"**Currently playing**: {} - {}",
|
||||||
|
song.title,
|
||||||
|
song.artist.as_deref().unwrap_or_default()
|
||||||
|
);
|
||||||
|
ctx.say(text).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
22
src/discord/commands/pause.rs
Normal file
22
src/discord/commands/pause.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use poise::command;
|
||||||
|
|
||||||
|
use crate::discord::{common::get_call, Context};
|
||||||
|
|
||||||
|
/// Pause playback
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn pause(ctx: Context<'_>) -> Result<()> {
|
||||||
|
let call = get_call(ctx).await?;
|
||||||
|
let handler = call.lock().await;
|
||||||
|
|
||||||
|
let queue = handler.queue();
|
||||||
|
let current = queue.current();
|
||||||
|
if let Some(track) = current {
|
||||||
|
track.pause()?;
|
||||||
|
ctx.say("Paused playback").await?;
|
||||||
|
} else {
|
||||||
|
ctx.say("Not currently playing").await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
48
src/discord/commands/queue.rs
Normal file
48
src/discord/commands/queue.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use poise::command;
|
||||||
|
|
||||||
|
use crate::discord::{
|
||||||
|
common::{get_call, get_song},
|
||||||
|
Context,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Get play queue list
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn queue(ctx: Context<'_>) -> Result<()> {
|
||||||
|
let call = get_call(ctx).await?;
|
||||||
|
let handler = call.lock().await;
|
||||||
|
|
||||||
|
let current_queue = handler.queue().current_queue();
|
||||||
|
|
||||||
|
let text = if current_queue.is_empty() {
|
||||||
|
"No songs queued".into()
|
||||||
|
} else {
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut queue = current_queue.iter().enumerate();
|
||||||
|
|
||||||
|
let (_, track) = queue.next().unwrap();
|
||||||
|
let song = get_song(track).await?;
|
||||||
|
text.push_str(&format!(
|
||||||
|
"**Currently playing**: {} - {}\n\n",
|
||||||
|
song.title,
|
||||||
|
song.artist.as_deref().unwrap_or_default()
|
||||||
|
));
|
||||||
|
|
||||||
|
text.push_str("**Next songs in queue**:\n");
|
||||||
|
for (i, track) in queue {
|
||||||
|
let song = get_song(track).await?;
|
||||||
|
let song_text = format!(
|
||||||
|
"**{}.** {} - {}",
|
||||||
|
i,
|
||||||
|
song.title,
|
||||||
|
song.artist.as_deref().unwrap_or_default()
|
||||||
|
);
|
||||||
|
text.push_str(&song_text);
|
||||||
|
text.push('\n');
|
||||||
|
}
|
||||||
|
text
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.say(text).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
29
src/discord/commands/random.rs
Normal file
29
src/discord/commands/random.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use poise::command;
|
||||||
|
use serenity::utils::MessageBuilder;
|
||||||
|
use sunk::song::Song;
|
||||||
|
|
||||||
|
use crate::discord::{common::queue_song, Context};
|
||||||
|
|
||||||
|
/// Play a random song
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn random(ctx: Context<'_>) -> Result<()> {
|
||||||
|
let music_client = &ctx.data().subsonic_client;
|
||||||
|
|
||||||
|
let result = Song::random(music_client, 1).await?;
|
||||||
|
let song: Song = result
|
||||||
|
.first()
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.ok_or_else(|| anyhow!("No song matching search found"))?;
|
||||||
|
queue_song(ctx, &song, music_client).await?;
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"Added **{} - {}** to the queue",
|
||||||
|
song.title,
|
||||||
|
song.artist.as_deref().unwrap_or_default(),
|
||||||
|
);
|
||||||
|
ctx.say(&MessageBuilder::new().push(&message).build())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
26
src/discord/commands/remove.rs
Normal file
26
src/discord/commands/remove.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use poise::command;
|
||||||
|
|
||||||
|
use crate::discord::{
|
||||||
|
common::{get_call, get_song},
|
||||||
|
Context,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Remove song from queue
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn remove(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Song position on the queue"] index: usize,
|
||||||
|
) -> Result<()> {
|
||||||
|
let call = get_call(ctx).await?;
|
||||||
|
let handler = call.lock().await;
|
||||||
|
|
||||||
|
let queue = handler.queue();
|
||||||
|
let track = queue
|
||||||
|
.dequeue(index - 1)
|
||||||
|
.ok_or_else(|| anyhow!("Song not found"))?;
|
||||||
|
let song = get_song(&track).await?;
|
||||||
|
let text = format!("Removed track: {}", song.title);
|
||||||
|
ctx.say(text).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
18
src/discord/commands/resume.rs
Normal file
18
src/discord/commands/resume.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use poise::command;
|
||||||
|
|
||||||
|
use crate::discord::{common::get_call, Context};
|
||||||
|
|
||||||
|
/// Resume playing current song
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn resume(ctx: Context<'_>) -> Result<()> {
|
||||||
|
let call = get_call(ctx).await?;
|
||||||
|
let handler = call.lock().await;
|
||||||
|
|
||||||
|
let queue = handler.queue();
|
||||||
|
queue.resume()?;
|
||||||
|
|
||||||
|
ctx.say("Resumed playing").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
25
src/discord/commands/skip.rs
Normal file
25
src/discord/commands/skip.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use poise::command;
|
||||||
|
|
||||||
|
use crate::discord::{common::get_call, Context};
|
||||||
|
|
||||||
|
/// Skip song(s)
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn skip(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Number of songs to skip"] n: Option<usize>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let call = get_call(ctx).await?;
|
||||||
|
let handler = call.lock().await;
|
||||||
|
let n = n.unwrap_or(1);
|
||||||
|
|
||||||
|
let queue = handler.queue();
|
||||||
|
for _ in 0..(n - 1) {
|
||||||
|
queue.dequeue(1).ok_or_else(|| anyhow!("Song not found"))?;
|
||||||
|
}
|
||||||
|
queue.skip()?;
|
||||||
|
|
||||||
|
ctx.say(&format!("{n} song(s) skipped")).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
40
src/discord/commands/song.rs
Normal file
40
src/discord/commands/song.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use anyhow::{anyhow, Context as ErrContext, Result};
|
||||||
|
use poise::command;
|
||||||
|
use sunk::search::{self, SearchPage};
|
||||||
|
|
||||||
|
use crate::discord::{common::queue_song, Context};
|
||||||
|
|
||||||
|
/// Search for a song, and queue it
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn song(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "What to search for"] query: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
let music_client = &ctx.data().subsonic_client;
|
||||||
|
|
||||||
|
let search_size = SearchPage::new().with_size(1);
|
||||||
|
let ignore = search::NONE;
|
||||||
|
|
||||||
|
let result = music_client
|
||||||
|
.search(&query, ignore, ignore, search_size)
|
||||||
|
.await
|
||||||
|
.with_context(|| anyhow!("Could not search for song"))?
|
||||||
|
.songs;
|
||||||
|
|
||||||
|
let song = result
|
||||||
|
.first()
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.ok_or_else(|| anyhow!("No song matching search found"))?;
|
||||||
|
|
||||||
|
log::info!("Found song {song:?}");
|
||||||
|
queue_song(ctx, &song, music_client).await?;
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"Added **{} - {}** to the queue",
|
||||||
|
song.title,
|
||||||
|
song.artist.as_deref().unwrap_or_default(),
|
||||||
|
);
|
||||||
|
ctx.say(message).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
18
src/discord/commands/stop.rs
Normal file
18
src/discord/commands/stop.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use poise::command;
|
||||||
|
|
||||||
|
use crate::discord::{common::get_call, Context};
|
||||||
|
|
||||||
|
/// Stop playing and clear queue
|
||||||
|
#[command(slash_command, prefix_command)]
|
||||||
|
pub async fn stop(ctx: Context<'_>) -> Result<()> {
|
||||||
|
let call = get_call(ctx).await?;
|
||||||
|
let handler = call.lock().await;
|
||||||
|
|
||||||
|
let queue = handler.queue();
|
||||||
|
queue.stop();
|
||||||
|
|
||||||
|
ctx.say("Stopped playing").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
92
src/discord/common.rs
Normal file
92
src/discord/common.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use serenity::all::{ChannelId, Guild};
|
||||||
|
use songbird::{
|
||||||
|
input::HttpRequest,
|
||||||
|
tracks::{Track, TrackHandle},
|
||||||
|
Songbird,
|
||||||
|
};
|
||||||
|
use sunk::{song::Song, Streamable};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::handles::SubsonicSongHandle;
|
||||||
|
|
||||||
|
pub struct Data {
|
||||||
|
pub subsonic_client: sunk::Client,
|
||||||
|
}
|
||||||
|
pub type Context<'a> = poise::Context<'a, Data, anyhow::Error>;
|
||||||
|
|
||||||
|
pub async fn queue_song(ctx: Context<'_>, song: &Song, client: &sunk::Client) -> Result<()> {
|
||||||
|
let call = get_call(ctx).await?;
|
||||||
|
let mut handler = call.lock().await;
|
||||||
|
|
||||||
|
let track = load_song(song, client).await?;
|
||||||
|
let track_handle = handler.enqueue(track).await;
|
||||||
|
let mut type_map = track_handle.typemap().write().await;
|
||||||
|
type_map.insert::<SubsonicSongHandle>(song.clone());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_guild(ctx: Context<'_>) -> Guild {
|
||||||
|
let guild = ctx.guild().expect("No guild!").clone();
|
||||||
|
log::info!("Got guild: {guild:?}");
|
||||||
|
guild
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_channel(ctx: Context<'_>) -> Result<ChannelId> {
|
||||||
|
let guild = get_guild(ctx);
|
||||||
|
let voice_state = guild
|
||||||
|
.voice_states
|
||||||
|
.get(&ctx.author().id)
|
||||||
|
.ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?;
|
||||||
|
let channel = voice_state
|
||||||
|
.channel_id
|
||||||
|
.ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?;
|
||||||
|
|
||||||
|
Ok(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_call(ctx: Context<'_>) -> Result<Arc<Mutex<songbird::Call>>> {
|
||||||
|
let manager = get_manager(ctx).await?;
|
||||||
|
let guild = get_guild(ctx);
|
||||||
|
let call = manager.get(guild.id);
|
||||||
|
|
||||||
|
if let Some(c) = call {
|
||||||
|
Ok(c)
|
||||||
|
} else {
|
||||||
|
let channel = get_channel(ctx)?;
|
||||||
|
log::warn!("Not in a voice channel, trying to join {channel}");
|
||||||
|
let _handler = manager.join(guild.id, channel).await;
|
||||||
|
manager
|
||||||
|
.get(guild.id)
|
||||||
|
.ok_or_else(|| anyhow!("Not in a voice channel, try running 'join'"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_song(track: &TrackHandle) -> Result<Song> {
|
||||||
|
let song = track
|
||||||
|
.typemap()
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get::<SubsonicSongHandle>()
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.ok_or_else(|| anyhow!("Sound information not found"))?;
|
||||||
|
Ok(song)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_manager(ctx: Context<'_>) -> Result<Arc<Songbird>> {
|
||||||
|
let manager = songbird::get(ctx.discord())
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| anyhow!("Couldn't start manager"))?;
|
||||||
|
log::info!("Got manager: {manager:?}");
|
||||||
|
Ok(manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_song(song: &Song, client: &sunk::Client) -> Result<Track> {
|
||||||
|
log::info!("Loading song {song:?}");
|
||||||
|
let url = song.stream_url(client)?;
|
||||||
|
let track: Track = HttpRequest::new(client.reqclient.clone(), url).into();
|
||||||
|
Ok(track)
|
||||||
|
}
|
||||||
14
src/discord/handler.rs
Normal file
14
src/discord/handler.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use serenity::{
|
||||||
|
all::Ready,
|
||||||
|
async_trait,
|
||||||
|
prelude::{Context, EventHandler},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Handler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for Handler {
|
||||||
|
async fn ready(&self, _: Context, ready: Ready) {
|
||||||
|
log::info!("{} is connected!", ready.user.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/discord/mod.rs
Normal file
8
src/discord/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pub mod client;
|
||||||
|
pub mod commands;
|
||||||
|
pub mod common;
|
||||||
|
pub mod handler;
|
||||||
|
|
||||||
|
pub use client::create_client;
|
||||||
|
pub use common::{Context, Data};
|
||||||
|
pub use handler::Handler;
|
||||||
Reference in New Issue
Block a user