Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2059a76c6 | |||
| fcbe163d18 | |||
| 6a3f75af71 | |||
| a4d3146076 | |||
| eaaba5a053 | |||
| 464d7f6fab | |||
| 547a067e54 | |||
| acce38cd44 | |||
| 29fefc118b | |||
| 8feab199fe | |||
| 97f4a4374d | |||
| f27376f8eb | |||
| 9b19be2a55 | |||
| 1512018a4f |
Generated
+15
-15
@@ -2,6 +2,21 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "RhythmWorks"
|
||||||
|
version = "0.3.0"
|
||||||
|
dependencies = [
|
||||||
|
"reqwest",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serenity",
|
||||||
|
"songbird",
|
||||||
|
"symphonia",
|
||||||
|
"tokio",
|
||||||
|
"yt-dlp",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -690,21 +705,6 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "discord_music"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"reqwest",
|
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serenity",
|
|
||||||
"songbird",
|
|
||||||
"symphonia",
|
|
||||||
"tokio",
|
|
||||||
"yt-dlp",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "discortp"
|
name = "discortp"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
|||||||
+13
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "discord_music"
|
name = "RhythmWorks"
|
||||||
version = "0.1.0"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -13,3 +13,14 @@ songbird = { version = "0.5.0", features = ["builtin-queue", "driver", "serenity
|
|||||||
symphonia = { version = "0.5.5", features = ["aac", "alac", "isomp4", "mp3"] }
|
symphonia = { version = "0.5.5", features = ["aac", "alac", "isomp4", "mp3"] }
|
||||||
tokio = "1.48.0"
|
tokio = "1.48.0"
|
||||||
yt-dlp = "1.4.7"
|
yt-dlp = "1.4.7"
|
||||||
|
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = "fat"
|
||||||
|
debug = false
|
||||||
|
codegen-units = 1
|
||||||
|
panic = 'abort'
|
||||||
|
strip = true
|
||||||
|
debug-assertions = false
|
||||||
|
overflow-checks = true
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
## Welcome to RhythmWorks
|
||||||
|
RhythmWorks is a lightweight Discord music bot designed for simple, direct audio playback from YouTube links. It focuses on manual control, minimal complexity, and local execution, making it ideal for personal servers or small communities.
|
||||||
|
|
||||||
|
Unlike many modern Discord bots, RhythmWorks does not rely on hosted services or third-party dashboards. You run it yourself, connect it to your Discord server, and control playback using straightforward text commands.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Play Audio directly from YouTube
|
||||||
|
- Basic Playback Controls
|
||||||
|
- Runs locally on your machine
|
||||||
|
- No external accounts or external bot hosting required
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
RhythmWorks connects to Discord using a custom bot that you generate through the Discord Developer Portal
|
||||||
|
|
||||||
|
### Roadmap
|
||||||
|
[Project Roadmap](https://git.racooncity.org/brotoskyj/RhythmWorks/projects/4)
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
Please follow the [Getting Started](https://git.racooncity.org/brotoskyj/RhythmWorks/wiki/Getting-Started) Documentation for details on how to get RhythmWorks up and running
|
||||||
|
|
||||||
|
### Discord Commands
|
||||||
|
Please review the [Commands](https://git.racooncity.org/brotoskyj/RhythmWorks/wiki/Commands) for usage
|
||||||
|
|
||||||
|
### Wiki
|
||||||
|
[RhythmWorks Wiki](https://git.racooncity.org/brotoskyj/RhythmWorks/wiki/?action=_pages) - Please review this all for all documentation
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[AGPLv3](https://choosealicense.com/licenses/agpl-3.0/)
|
||||||
|
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
+71
-46
@@ -8,18 +8,12 @@ use songbird::SerenityInit;
|
|||||||
use songbird::get;
|
use songbird::get;
|
||||||
use songbird::input::HlsRequest;
|
use songbird::input::HlsRequest;
|
||||||
use songbird::input::Input;
|
use songbird::input::Input;
|
||||||
use songbird::tracks::TrackHandle;
|
use std::io;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::env;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::Arc;
|
|
||||||
pub mod modules;
|
pub mod modules;
|
||||||
|
|
||||||
type TrackMap = Arc<Mutex<HashMap<u64, TrackHandle>>>;
|
struct Handler;
|
||||||
struct Handler {
|
|
||||||
track_map: TrackMap,
|
|
||||||
}
|
|
||||||
fn get_user_voice_channel(ctx: &Context, msg: &Message) -> Option<ChannelId> {
|
fn get_user_voice_channel(ctx: &Context, msg: &Message) -> Option<ChannelId> {
|
||||||
let guild_id = msg.guild_id?;
|
let guild_id = msg.guild_id?;
|
||||||
let guild = ctx.cache.guild(guild_id)?;
|
let guild = ctx.cache.guild(guild_id)?;
|
||||||
@@ -77,58 +71,78 @@ impl EventHandler for Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if msg.content.starts_with("!play") {
|
if msg.content.starts_with("!play") {
|
||||||
let url = msg
|
let mut yt_url = msg
|
||||||
.content
|
.content
|
||||||
.strip_prefix("!play ")
|
.strip_prefix("!play ")
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.trim()
|
.trim()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
if let Some(offset) = yt_url.find('&') {
|
||||||
if url.is_empty() {
|
yt_url.truncate(offset);
|
||||||
|
}
|
||||||
|
if yt_url.is_empty() {
|
||||||
let _ = msg.reply(&ctx, "Please provide a YouTube URL.").await;
|
let _ = msg.reply(&ctx, "Please provide a YouTube URL.").await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let guild_id = msg.guild_id.unwrap();
|
let guild_id = msg.guild_id.unwrap();
|
||||||
let manager = songbird::get(&ctx)
|
let manager = songbird::get(&ctx).await.unwrap().clone();
|
||||||
.await
|
let handler_lock = if let Some(call) = manager.get(guild_id) {
|
||||||
.expect("Songbird Voice client placed in at initialisation.")
|
call
|
||||||
.clone();
|
|
||||||
let handler_lock = if let Some(handler) = manager.get(guild_id) {
|
|
||||||
handler
|
|
||||||
} else {
|
} else {
|
||||||
let channel_id = get_user_voice_channel(&ctx, &msg);
|
let channel_id = match get_user_voice_channel(&ctx, &msg) {
|
||||||
match channel_id {
|
Some(c) => c,
|
||||||
Some(c) => manager.join(guild_id, c).await.unwrap(),
|
|
||||||
None => {
|
None => {
|
||||||
let _ = msg.reply(&ctx, "Join a voice channel first!").await;
|
let _ = msg.reply(&ctx, "Join a voice channel first!").await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
manager.join(guild_id, channel_id).await.unwrap()
|
||||||
};
|
};
|
||||||
let output = Command::new("yt-dlp")
|
let output = Command::new("./yt-dlp")
|
||||||
.args(["-f", "bestaudio", "-g", &url])
|
.args(["-f", "bestaudio", "-g", &yt_url])
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed");
|
.expect("yt-dlp failed");
|
||||||
let url_bytes = output.stdout;
|
if !output.status.success() {
|
||||||
let url = String::from_utf8(url_bytes).expect("Failed");
|
let _ = msg.reply(&ctx, "Failed to fetch audio stream.").await;
|
||||||
let mut handler = handler_lock.lock().await;
|
return;
|
||||||
let input = Input::from(HlsRequest::new(reqwest::Client::new(), url.to_string()));
|
}
|
||||||
let track_handle = handler.play_input(input);
|
let stream_url = String::from_utf8(output.stdout)
|
||||||
let _ = track_handle.play();
|
.expect("Invalid UTF-8")
|
||||||
self.track_map
|
.trim()
|
||||||
.lock()
|
.to_string();
|
||||||
.await
|
let mut call = handler_lock.lock().await;
|
||||||
.insert(guild_id.into(), track_handle);
|
let input = Input::from(HlsRequest::new(reqwest::Client::new(), stream_url));
|
||||||
let _ = msg.channel_id.say(&ctx.http, "🎶 Playing...").await;
|
let _ = call.enqueue_input(input).await;
|
||||||
|
let position = call.queue().len();
|
||||||
|
let response = if position == 1 {
|
||||||
|
"🎶 Now playing!".to_string()
|
||||||
|
} else {
|
||||||
|
format!("🎶 Added Song to Queue: #{}", position)
|
||||||
|
};
|
||||||
|
let _ = msg.channel_id.say(&ctx.http, response).await;
|
||||||
|
}
|
||||||
|
if msg.content == "!skip" {
|
||||||
|
let guild_id = match msg.guild_id {
|
||||||
|
Some(g) => g,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let manager = songbird::get(&ctx).await.unwrap();
|
||||||
|
if let Some(call) = manager.get(guild_id) {
|
||||||
|
let call = call.lock().await;
|
||||||
|
let _ = call.queue().skip();
|
||||||
|
let _ = msg.channel_id.say(&ctx.http, "Skipped").await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if msg.content == "!pause" {
|
if msg.content == "!pause" {
|
||||||
let guild_id = match msg.guild_id {
|
let guild_id = match msg.guild_id {
|
||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
if let Some(handle) = self.track_map.lock().await.get(&guild_id.into()) {
|
let manager = songbird::get(&ctx).await.unwrap();
|
||||||
let _ = handle.pause();
|
if let Some(call) = manager.get(guild_id) {
|
||||||
|
let call = call.lock().await;
|
||||||
|
let _ = call.queue().pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if msg.content == "!resume" {
|
if msg.content == "!resume" {
|
||||||
@@ -136,8 +150,10 @@ impl EventHandler for Handler {
|
|||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
if let Some(handle) = self.track_map.lock().await.get(&guild_id.into()) {
|
let manager = songbird::get(&ctx).await.unwrap();
|
||||||
let _ = handle.play();
|
if let Some(call) = manager.get(guild_id) {
|
||||||
|
let call = call.lock().await;
|
||||||
|
let _ = call.queue().resume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if msg.content == "!stop" {
|
if msg.content == "!stop" {
|
||||||
@@ -145,8 +161,10 @@ impl EventHandler for Handler {
|
|||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
if let Some(handle) = self.track_map.lock().await.get(&guild_id.into()) {
|
let manager = songbird::get(&ctx).await.unwrap();
|
||||||
let _ = handle.stop();
|
if let Some(call) = manager.get(guild_id) {
|
||||||
|
let call = call.lock().await;
|
||||||
|
let _ = call.queue().stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,21 +172,28 @@ impl EventHandler for Handler {
|
|||||||
println!("{} is connected!", ready.user.name);
|
println!("{} is connected!", ready.user.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
println!("RhythmWorks Copyright (C) 2025 James Brotosky\n
|
println!(
|
||||||
|
"RhythmWorks Copyright (C) 2025 James Brotosky\n
|
||||||
This program comes with ABSOLUTELY NO WARRANTY\n
|
This program comes with ABSOLUTELY NO WARRANTY\n
|
||||||
This is free software, and you are welcome to redistribute it under certain conditions.");
|
This is free software, and you are welcome to redistribute it under certain conditions."
|
||||||
|
);
|
||||||
updator::update().await;
|
updator::update().await;
|
||||||
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
|
println!("Please paste in your bot token");
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin()
|
||||||
|
.read_line(&mut input)
|
||||||
|
.expect("Failed to read line");
|
||||||
|
let token = input.trim();
|
||||||
let intents = GatewayIntents::GUILD_MESSAGES
|
let intents = GatewayIntents::GUILD_MESSAGES
|
||||||
| GatewayIntents::GUILDS
|
| GatewayIntents::GUILDS
|
||||||
| GatewayIntents::DIRECT_MESSAGES
|
| GatewayIntents::DIRECT_MESSAGES
|
||||||
| GatewayIntents::MESSAGE_CONTENT
|
| GatewayIntents::MESSAGE_CONTENT
|
||||||
| GatewayIntents::GUILD_VOICE_STATES;
|
| GatewayIntents::GUILD_VOICE_STATES;
|
||||||
let track_map: TrackMap = Arc::new(Mutex::new(HashMap::new()));
|
|
||||||
let mut client = Client::builder(&token, intents)
|
let mut client = Client::builder(&token, intents)
|
||||||
.event_handler(Handler { track_map })
|
.event_handler(Handler)
|
||||||
.register_songbird()
|
.register_songbird()
|
||||||
.await
|
.await
|
||||||
.expect("Err creating client");
|
.expect("Err creating client");
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
pub mod updator;
|
pub mod updator;
|
||||||
pub mod downloadyt;
|
|
||||||
+18
-8
@@ -1,20 +1,30 @@
|
|||||||
use serde::Deserialize;
|
use std::thread;
|
||||||
use semver::Version;
|
|
||||||
use reqwest;
|
use reqwest;
|
||||||
|
use semver::Version;
|
||||||
|
use serde::Deserialize;
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct Release {
|
struct Release {
|
||||||
tag_name: String,
|
tag_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update() {
|
pub async fn update() {
|
||||||
let body = reqwest::get("https://git.racooncity.org/api/v1/repos/brotoskyj/RhythmWorks/releases/latest").await.unwrap().text().await.unwrap();
|
let body = reqwest::get(
|
||||||
|
"https://git.racooncity.org/api/v1/repos/brotoskyj/RhythmWorks/releases/latest",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let release_info: Release = serde_json::from_str(&body).unwrap();
|
let release_info: Release = serde_json::from_str(&body).unwrap();
|
||||||
let latest_version = Version::parse(&release_info.tag_name).unwrap();
|
let latest_version = Version::parse(&release_info.tag_name).unwrap();
|
||||||
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||||
if latest_version > current_version {
|
if latest_version > current_version {
|
||||||
println!("Update Available")
|
println!("Update Available! Please download the latest version");
|
||||||
|
println!("URL: https://git.racooncity.org/brotoskyj/RhythmWorks/releases");
|
||||||
|
thread::sleep(std::time::Duration::from_secs(10));
|
||||||
|
std::process::exit(1);
|
||||||
|
} else {
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
println!("Already on current version");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user