10 Commits

Author SHA1 Message Date
mysticmomba 5fa83ba60d fix(codebase): Changed git URL in updator to reflect new git name 2026-03-04 09:24:25 -05:00
brotoskyj 3d7af18dc4 feat(UI/Codebase): Made menu persistent. After starting the bot, the menu will remain on the screen and be interactable for the user while the bot is running 2026-01-20 22:17:28 -05:00
brotoskyj 4924b7e505 Changed Cargo.toml 2026-01-20 21:55:00 -05:00
brotoskyj d9a7bb7fed fix(codebase): Added Proper --no-playlist to command 2026-01-20 21:35:39 -05:00
brotoskyj 8fefbddffd Added --no-playlist to yt-dlp command. This seems to fix the bot randomly skipping songs 2026-01-20 21:30:39 -05:00
brotoskyj 7dea731cbf Added Extra Telemetry for Queue 2026-01-19 14:54:13 -05:00
brotoskyj ff9a46c05c Feature Changes
Added a Token Credential Manager - Closes #4
Added TUI Menu - Closes #2
2026-01-16 10:26:17 -05:00
brotoskyj 6169e6065d Added Telemetry
closes #8
2026-01-15 14:32:28 -05:00
James Brotosky 0ae38b0e01 Merge pull request 'Closes #1' (#10) from feature/queue into master
Reviewed-on: brotoskyj/RhythmWorks#10
2026-01-12 12:37:26 -05:00
brotoskyj b2059a76c6 Closes #1 2026-01-12 12:36:15 -05:00
10 changed files with 1775 additions and 87 deletions
+1
View File
@@ -1 +1,2 @@
/target /target
cliff.toml
Generated
+1297 -9
View File
File diff suppressed because it is too large Load Diff
+18 -1
View File
@@ -1,9 +1,21 @@
[package] [package]
name = "RhythmWorks" name = "RhythmWorks"
version = "0.2.0" version = "0.7.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
color-eyre = "0.6.5"
crossterm = "0.29.0"
dialoguer = "0.12.0"
keyring = { version = "3.6.3", features = ["linux-native", "windows-native"] }
opentelemetry = { version = "0.27.0", features = ["logs", "metrics", "trace"] }
opentelemetry-appender-log = "0.27.0"
opentelemetry-appender-tracing = "0.27.0"
opentelemetry-otlp = { version = "0.27.0", features = ["trace", "metrics", "grpc-tonic", "http-proto", "tls", "reqwest-client", "reqwest-rustls", "logs"] }
opentelemetry-proto = "0.27.0"
opentelemetry-semantic-conventions = "0.27.0"
opentelemetry_sdk = { version = "0.27.0", features = ["logs", "rt-tokio", "trace"] }
ratatui = { version = "0.30.0", features = ["all-widgets"] }
reqwest = { version = "0.12.28", features = ["blocking", "json"] } reqwest = { version = "0.12.28", features = ["blocking", "json"] }
semver = "1.0.27" semver = "1.0.27"
serde = "1.0.228" serde = "1.0.228"
@@ -12,6 +24,11 @@ serenity = { version = "0.12.5", features = ["client", "gateway", "voice"] }
songbird = { version = "0.5.0", features = ["builtin-queue", "driver", "serenity"] } 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"
tokio-util = "0.7.18"
tonic = { version = "0.12.3", features = ["tls-roots"] }
tracing = "0.1.41"
tracing-opentelemetry = "0.32.0"
tracing-subscriber = "0.3.20"
yt-dlp = "1.4.7" yt-dlp = "1.4.7"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
cargo clean
cargo fmt
cargo clippy --fix --all-targets --allow-dirty
cargo build --release
cargo build --release --target x86_64-pc-windows-gnu
+1
View File
@@ -0,0 +1 @@
## [0.5.2] - 2026-01-21
+357 -68
View File
@@ -1,25 +1,77 @@
use dialoguer::Select;
use dialoguer::theme::ColorfulTheme;
use modules::*; use modules::*;
use opentelemetry::KeyValue;
use opentelemetry::global;
use opentelemetry::trace::Status;
use opentelemetry::trace::TraceContextExt;
use opentelemetry::trace::Tracer;
use serenity::all::ChannelId; use serenity::all::ChannelId;
use serenity::async_trait; use serenity::async_trait;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity::model::gateway::Ready; use serenity::model::gateway::Ready;
use serenity::prelude::*; use serenity::prelude::*;
use songbird::Event;
use songbird::EventContext;
use songbird::SerenityInit; use songbird::SerenityInit;
use songbird::events::EventHandler as VoiceEventHandler;
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 tracing::instrument;
use std::collections::HashMap; use std::env;
use std::io;
use std::process::Command; use std::process::Command;
use std::process::Stdio; use std::process::Stdio;
use std::sync::Arc; use tokio::task::JoinHandle;
pub mod modules; use tokio_util::sync::CancellationToken;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::Layer;
use tracing_subscriber::filter::Targets;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
type TrackMap = Arc<Mutex<HashMap<u64, TrackHandle>>>; pub mod modules;
struct Handler { enum BotState {
track_map: TrackMap, Stopped,
Running,
} }
struct TrackTraceHandler {
otel_ctx: opentelemetry::Context,
guild_id: String,
track_url: String,
hls_url: String,
event_name: &'static str,
}
#[async_trait]
impl VoiceEventHandler for TrackTraceHandler {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
let tracer = global::tracer("RhythmWorks");
let track_status = if let EventContext::Track(track_list) = ctx {
track_list.first().map(|state| format!("{:?}", state.0))
} else {
None
};
tracer.in_span(self.event_name, |cx| {
cx.span()
.set_attribute(KeyValue::new("Guild_ID", self.guild_id.clone()));
cx.span()
.set_attribute(KeyValue::new("Youtube_URL", self.track_url.clone()));
cx.span()
.set_attribute(KeyValue::new("Stream_URL", self.hls_url.clone()));
cx.span().add_event(
self.event_name,
vec![KeyValue::new(
"track_status",
track_status.unwrap().to_string(),
)],
);
});
None
}
}
struct Handler;
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)?;
@@ -38,13 +90,51 @@ impl EventHandler for Handler {
println!("Error sending message: {why:?}"); println!("Error sending message: {why:?}");
} }
if msg.content == "!join" { if msg.content == "!join" {
let tracer = global::tracer("RhythmWorks");
let span = tracer.start("Join Message Received");
let cx = opentelemetry::Context::current_with_span(span);
let guild_id = match msg.guild_id { let guild_id = match msg.guild_id {
Some(g) => g, Some(g) => {
None => return, tracing::info!("Received Guild ID");
}; cx.span().add_event(
let channel_id = match get_user_voice_channel(&ctx, &msg) { "Received Guild ID",
Some(c) => c, vec![KeyValue::new("Server ID", g.to_string())],
);
cx.span().set_status(opentelemetry::trace::Status::Ok);
cx.span()
.set_attribute(KeyValue::new("Guild_ID", g.to_string()));
g
}
None => { None => {
cx.span().add_event(
"Failed to Retreive Guild ID",
vec![KeyValue::new("Server ID", "None")],
);
cx.span()
.set_status(opentelemetry::trace::Status::error("Failed"));
return;
}
};
let channel_id = match get_user_voice_channel(&ctx, &msg) {
Some(c) => {
cx.span().add_event(
"Received Channel ID",
vec![KeyValue::new("Channel ID", c.to_string())],
);
cx.span().set_status(Status::Ok);
c
}
None => {
cx.span().add_event(
"Failed to Retreive Channel ID",
vec![KeyValue::new("Channel ID", "None")],
);
cx.span()
.set_status(opentelemetry::trace::Status::error(format!(
"{} not in Voice Channel",
msg.author
)));
let _ = msg let _ = msg
.channel_id .channel_id
.say(&ctx.http, "You must be in a voice channel") .say(&ctx.http, "You must be in a voice channel")
@@ -52,7 +142,6 @@ impl EventHandler for Handler {
return; return;
} }
}; };
let manager = get(&ctx).await.expect("Songbird not initialized").clone(); let manager = get(&ctx).await.expect("Songbird not initialized").clone();
let _ = manager.join(guild_id, channel_id).await; let _ = manager.join(guild_id, channel_id).await;
@@ -61,6 +150,11 @@ impl EventHandler for Handler {
.channel_id .channel_id
.say(&ctx.http, "Joined your voice channel!") .say(&ctx.http, "Joined your voice channel!")
.await; .await;
cx.span().add_event(
"Joined Voice Channel",
vec![KeyValue::new("Voice Channel", msg.channel_id.to_string())],
);
cx.span().set_status(Status::Ok);
} }
if msg.content == "!leave" { if msg.content == "!leave" {
let guild_id = match msg.guild_id { let guild_id = match msg.guild_id {
@@ -77,61 +171,168 @@ impl EventHandler for Handler {
} }
} }
if msg.content.starts_with("!play") { if msg.content.starts_with("!play") {
let mut url = msg let tracer = global::tracer("RhythmWorks");
let span = tracer.start("Play Message Received");
let mut yt_url = msg
.content .content
.strip_prefix("!play ") .strip_prefix("!play ")
.unwrap_or("") .unwrap_or("")
.trim() .trim()
.to_string(); .to_string();
let separator = "&"; if let Some(offset) = yt_url.find('&') {
if let Some(offset) = url.find(separator) { yt_url.truncate(offset);
url.truncate(offset);
} }
if url.is_empty() { 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 cx = opentelemetry::Context::current_with_span(span);
let manager = songbird::get(&ctx) cx.span()
.await .set_attribute(KeyValue::new("Youtube_URL", yt_url.to_string()));
.expect("Songbird Voice client placed in at initialisation.") let guild_id = match msg.guild_id {
.clone(); Some(g) => {
let handler_lock = if let Some(handler) = manager.get(guild_id) { cx.span().add_event(
handler "Received Guild ID",
} else { vec![KeyValue::new("Server ID", g.to_string())],
let channel_id = get_user_voice_channel(&ctx, &msg); );
match channel_id { cx.span().set_status(opentelemetry::trace::Status::Ok);
Some(c) => manager.join(guild_id, c).await.unwrap(), cx.span()
None => { .set_attribute(KeyValue::new("Guild_ID", g.to_string()));
let _ = msg.reply(&ctx, "Join a voice channel first!").await; g
return; }
} None => {
cx.span().add_event(
"Failed to Retreive Guild ID",
vec![KeyValue::new("Server ID", "None")],
);
cx.span()
.set_status(opentelemetry::trace::Status::error("Failed"));
return;
} }
}; };
let output = Command::new("./yt-dlp") let manager = songbird::get(&ctx).await.unwrap().clone();
.args(["-f", "bestaudio", "-g", &url]) let handler_lock = if let Some(call) = manager.get(guild_id) {
call
} else {
let channel_id = match get_user_voice_channel(&ctx, &msg) {
Some(c) => c,
None => {
cx.span().add_event(
"Failed to Retreive Channel ID",
vec![KeyValue::new("Channel ID", "None")],
);
cx.span()
.set_status(opentelemetry::trace::Status::error(format!(
"{} not in Voice Channel",
msg.author
)));
let _ = msg
.channel_id
.say(&ctx.http, "You must be in a voice channel")
.await;
return;
}
};
manager.join(guild_id, channel_id).await.unwrap()
};
let output = match Command::new("./yt-dlp")
.args(["-f", "bestaudio", "-g", "--no-playlist", &yt_url])
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.output() .output()
.expect("Failed"); {
let url_bytes = output.stdout; Ok(output) => {
let url = String::from_utf8(url_bytes).expect("Failed"); cx.span().add_event(
let mut handler = handler_lock.lock().await; "Retreiving Youtube Information",
let input = Input::from(HlsRequest::new(reqwest::Client::new(), url.to_string())); vec![KeyValue::new("Success", yt_url.to_string())],
let track_handle = handler.play_input(input); );
let _ = track_handle.play(); output
self.track_map }
.lock() Err(e) => {
.await cx.span().add_event(
.insert(guild_id.into(), track_handle); "Retreiving Youtube Information",
let _ = msg.channel_id.say(&ctx.http, "🎶 Playing...").await; vec![KeyValue::new("Failed", e.to_string())],
);
cx.span().set_status(Status::error("Failed"));
eprintln!("{}", e);
return;
}
};
let stream_url = String::from_utf8(output.stdout)
.expect("Invalid UTF-8")
.trim()
.to_string();
let telemetry_stream_url = stream_url.clone();
let mut call = handler_lock.lock().await;
let input = Input::from(HlsRequest::new(reqwest::Client::new(), stream_url));
let track_handle = call.enqueue_input(input).await;
let otel_ctx = opentelemetry::Context::current();
let position = call.queue().len();
let response = if position == 1 {
cx.span().add_event(
"Starting Playback",
vec![KeyValue::new("Status", "Playing".to_string())],
);
"🎶 Now playing!".to_string()
} else {
cx.span().add_event(
"Starting Playback",
vec![KeyValue::new("Status", "Added to Queue".to_string())],
);
format!("🎶 Added Song to Queue: #{}", position)
};
let _ = msg.channel_id.say(&ctx.http, response).await;
let _ = track_handle.add_event(
Event::Track(songbird::events::TrackEvent::Play),
TrackTraceHandler {
otel_ctx: otel_ctx.clone(),
guild_id: guild_id.to_string().clone(),
track_url: yt_url.clone(),
hls_url: telemetry_stream_url.clone(),
event_name: "Track Started Playing",
},
);
let _ = track_handle.add_event(
Event::Track(songbird::events::TrackEvent::End),
TrackTraceHandler {
otel_ctx: otel_ctx.clone(),
guild_id: guild_id.to_string().clone(),
track_url: yt_url.clone(),
hls_url: telemetry_stream_url.clone(),
event_name: "Track Finished",
},
);
let _ = track_handle.add_event(
Event::Track(songbird::events::TrackEvent::Error),
TrackTraceHandler {
otel_ctx: otel_ctx.clone(),
guild_id: guild_id.to_string().clone(),
track_url: yt_url.clone(),
hls_url: telemetry_stream_url.clone(),
event_name: "Playback had an error",
},
);
}
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" {
@@ -139,8 +340,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" {
@@ -148,8 +351,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();
} }
} }
} }
@@ -157,28 +362,112 @@ impl EventHandler for Handler {
println!("{} is connected!", ready.user.name); println!("{} is connected!", ready.user.name);
} }
} }
#[tokio::main] async fn run_bot(token: String, shutdown: CancellationToken) {
async fn main() {
println!("RhythmWorks Copyright (C) 2025 James Brotosky\n
This program comes with ABSOLUTELY NO WARRANTY\n
This is free software, and you are welcome to redistribute it under certain conditions.");
updator::update().await;
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");
if let Err(why) = client.start().await {
println!("Client error: {why:?}"); tokio::select! {
result = client.start() => {
if let Err(why) = result {
println!("Client error: {why:?}");
}
}
_ = shutdown.cancelled() => {
println!("Shutting down bot...");
}
}
}
#[tokio::main]
async fn main() {
println!(
"RhythmWorks Copyright (C) 2025 James Brotosky\n
This program comes with ABSOLUTELY NO WARRANTY\n
This is free software, and you are welcome to redistribute it under certain conditions."
);
let (tracer_provider, logger_provider) = telemetry::init_telemetry();
global::set_tracer_provider(tracer_provider.clone());
let filter = Targets::new()
.with_target("RhythmWorks", LevelFilter::INFO)
.with_default(LevelFilter::OFF);
let otel_log_layer =
opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(&logger_provider)
.with_filter(filter);
tracing_subscriber::registry().with(otel_log_layer).init();
let tracer: global::BoxedTracer = global::tracer("tracer");
tracer
.in_span("Checking for Updates", |_cx| updator::update())
.await;
let service_name = "RhythmWorks";
let username = env::var("USER").or_else(|_| env::var("USERNAME")).unwrap();
let mut bot_task: Option<JoinHandle<()>> = None;
let mut shutdown_token: Option<CancellationToken> = None;
let mut bot_state = BotState::Stopped;
loop {
let token = credentialmanager::get_token(service_name, &username);
let options: Vec<&'static str> = vec![
"- Start Bot",
"- Stop Bot",
"- Change Token",
"- Delete Token",
"- Exit",
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose an Option")
.default(0)
.items(&options)
.interact()
.unwrap();
match selection {
0 => {
if matches!(bot_state, BotState::Running) {
println!("[x] Bot is already running.");
continue;
}
let token = credentialmanager::get_token(service_name, &username);
let shutdown = CancellationToken::new();
let shutdown_clone = shutdown.clone();
let task = tokio::spawn(run_bot(token, shutdown_clone));
bot_task = Some(task);
shutdown_token = Some(shutdown);
bot_state = BotState::Running;
println!("✅ Bot started.");
}
1 => {
if let Some(token) = shutdown_token.take() {
token.cancel();
}
if let Some(task) = bot_task.take() {
let _ = task.await;
}
println!("Bot stopped.");
}
2 => {
credentialmanager::change_token(service_name, &username);
}
3 => {
credentialmanager::remove_token(service_name, &username);
}
4 => {
if let Some(token) = shutdown_token {
token.cancel();
}
break;
}
_ => println!("Invalid Option"),
}
} }
} }
+37
View File
@@ -0,0 +1,37 @@
use dialoguer::{Input, theme::ColorfulTheme};
use keyring::Entry;
pub fn get_token<'a>(service: &str, username: &str) -> String {
let entry = Entry::new(service, username).expect("Failed to get keyring entry");
match entry.get_password() {
Ok(token) => token,
Err(_) => {
let token: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter your bot token")
.interact_text()
.unwrap();
match entry.set_password(&token) {
Ok(_) => {}
Err(e) => {
println!("Failed to set token: {}", e);
std::process::abort()
}
};
token
}
}
}
pub fn change_token(service: &str, username: &str) {
let entry = Entry::new(service, username).expect("Failed to get keyring entry");
let new_token: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("New Token")
.interact_text()
.unwrap();
let _ = entry.set_password(&new_token);
}
pub fn remove_token(service: &str, username: &str) {
let entry = Entry::new(service, username).expect("Failed to get keyring entry");
let _ = entry.delete_credential();
}
+2
View File
@@ -1 +1,3 @@
pub mod credentialmanager;
pub mod telemetry;
pub mod updator; pub mod updator;
+44
View File
@@ -0,0 +1,44 @@
use opentelemetry::KeyValue;
use opentelemetry::global;
use opentelemetry_otlp::{WithExportConfig, WithTonicConfig};
use opentelemetry_sdk::{Resource, logs, runtime};
use tonic::transport::{Channel, ClientTlsConfig};
pub fn init_telemetry() -> (
opentelemetry_sdk::trace::TracerProvider,
opentelemetry_sdk::logs::LoggerProvider,
) {
let endpoint = "https://signoz.racooncity.org".to_string();
let channel = Channel::from_shared(endpoint.clone())
.unwrap()
.tls_config(ClientTlsConfig::new().with_native_roots())
.unwrap()
.connect_lazy();
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint.clone())
.with_channel(channel.clone())
.build()
.expect("Failed to Build Exporter");
let log_exporter = opentelemetry_otlp::LogExporter::builder()
.with_tonic()
.with_endpoint(endpoint.clone())
.with_channel(channel.clone())
.build()
.expect("Failed to build log exporter");
let logger_provider = logs::LoggerProvider::builder()
.with_resource(Resource::new(vec![KeyValue::new(
"service.name",
"RhythmWorks",
)]))
.with_batch_exporter(log_exporter, runtime::Tokio)
.build();
let tracer_provider = opentelemetry_sdk::trace::TracerProvider::builder()
.with_batch_exporter(exporter, runtime::Tokio)
.with_resource(Resource::new(vec![KeyValue::new(
"service.name",
"RhythmWorks",
)]))
.build();
(tracer_provider, logger_provider)
}
+11 -8
View File
@@ -1,26 +1,29 @@
use std::thread; use std::thread;
use serde::Deserialize;
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/mysticmomba/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! Please download the latest version"); println!("Update Available! Please download the latest version");
println!("URL: https://git.racooncity.org/brotoskyj/RhythmWorks/releases"); println!("URL: https://git.racooncity.org/mysticmomba/RhythmWorks/releases");
thread::sleep(std::time::Duration::from_secs(10)); thread::sleep(std::time::Duration::from_secs(10));
std::process::exit(1); std::process::exit(1);
}
else {
} }
} }