From d450660be814443ae0bf32c72a5a609f6ba8424b Mon Sep 17 00:00:00 2001 From: notdeltaxd <151985453+notdeltaxd@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:36:14 +0530 Subject: [PATCH 1/2] Add startup banner and update check Print an ASCII banner with the version and a short facts line before logging starts, gated by logging.banner and skipped under JSON output so a machine-readable stream stays intact. On startup, query the GitHub releases API once and, when a newer tag exists, print an upgrade notice that shares the banner's accent palette. The check runs in the background, honours HTTPS_PROXY, and is turned off by KAIRO_NO_UPDATE_CHECK; any failure falls back to a debug line rather than disturbing the node. Add reqwest and semver as direct dependencies; both were already in the tree through the pinned crates, so the build cost is unchanged. --- Cargo.lock | 4 +- Cargo.toml | 6 ++ application.yml.example | 1 + src/config/logging.rs | 5 ++ src/main.rs | 5 ++ src/utils/ansi.rs | 6 ++ src/utils/banner.rs | 46 ++++++++++++++ src/utils/mod.rs | 5 +- src/utils/update.rs | 129 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/utils/ansi.rs create mode 100644 src/utils/banner.rs create mode 100644 src/utils/update.rs diff --git a/Cargo.lock b/Cargo.lock index 6a1ea20..47baaef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1430,7 +1430,7 @@ dependencies = [ [[package]] name = "kairo" -version = "4.6.0" +version = "4.5.0" dependencies = [ "axum", "bytes", @@ -1445,7 +1445,9 @@ dependencies = [ "perf_monitor", "player", "rand 0.8.8", + "reqwest", "rustls", + "semver", "serde", "serde_json", "serde_yaml", diff --git a/Cargo.toml b/Cargo.toml index f85d88d..8a80346 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ libc = "0.2" mimalloc = "0.1.48" perf_monitor = "0.2.1" rand = "0.8" +semver = "1" serde_json = "1" serde_yaml = "0.9" tower-service = "0.3" @@ -34,6 +35,11 @@ tag = "v0.1.12" git = "https://github.com/bongo-devs/voice" tag = "v0.1.0" +[dependencies.reqwest] +version = "0.12" +default-features = false +features = ["rustls-tls", "json"] + [dependencies.tokio] version = "1.52" features = ["tracing", "macros", "signal", "rt-multi-thread"] diff --git a/application.yml.example b/application.yml.example index b51cebc..018424f 100644 --- a/application.yml.example +++ b/application.yml.example @@ -53,6 +53,7 @@ logging: symphonia_bundle_mp3: error # silences harmless VBR main_data_begin warnings format: compact # compact | pretty | json color: true + banner: true # print the startup banner (skipped when format is json) timestamps: true showTarget: true # prefix lines with the module name request: # REST request logging diff --git a/src/config/logging.rs b/src/config/logging.rs index c1841a2..75a319a 100644 --- a/src/config/logging.rs +++ b/src/config/logging.rs @@ -16,6 +16,10 @@ pub struct LoggingConfig { pub format: LogFormat, /// Colourise console output. Ignored for the file sink. pub color: bool, + /// Print the startup banner to the console. Ignored when [`format`] is JSON. + /// + /// [`format`]: Self::format + pub banner: bool, /// Prefix each line with a `HH:MM:SS` timestamp. pub timestamps: bool, /// Include the module target in each line. @@ -33,6 +37,7 @@ impl Default for LoggingConfig { levels: HashMap::new(), format: LogFormat::Compact, color: true, + banner: true, timestamps: true, show_target: true, file: None, diff --git a/src/main.rs b/src/main.rs index 237be1c..7dd990f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,9 +29,14 @@ async fn main() -> ExitCode { LazyLock::force(&CONFIG); + kairo::utils::banner::print(&CONFIG.logging); + // The guard flushes what the file sink buffered when dropped, so it is held for all of `main`. let _logging = kairo::utils::init(&CONFIG.logging); + // Fire and forget: it logs an upgrade notice if one exists and stays out of the way otherwise. + tokio::spawn(kairo::utils::update::check(&CONFIG.logging)); + let bind = format!("{}:{}", CONFIG.server.address, CONFIG.server.port); let http2 = CONFIG.server.http2.enabled; diff --git a/src/utils/ansi.rs b/src/utils/ansi.rs new file mode 100644 index 0000000..82dbe2d --- /dev/null +++ b/src/utils/ansi.rs @@ -0,0 +1,6 @@ +//! The ANSI escapes the console output shares, so the banner and notices read as one palette. + +pub const ACCENT: &str = "\x1b[38;5;44m"; +pub const BOLD: &str = "\x1b[1m"; +pub const DIM: &str = "\x1b[2m"; +pub const RESET: &str = "\x1b[0m"; diff --git a/src/utils/banner.rs b/src/utils/banner.rs new file mode 100644 index 0000000..c6dc794 --- /dev/null +++ b/src/utils/banner.rs @@ -0,0 +1,46 @@ +//! The startup banner printed to the console before logging begins. + +use crate::config::{LogFormat, LoggingConfig}; +use crate::utils::ansi::{ACCENT, BOLD, DIM, RESET}; + +const LOGO: &str = "\ +██╗ ██╗ █████╗ ██╗██████╗ ██████╗ +██║ ██╔╝██╔══██╗██║██╔══██╗██╔═══██╗ +█████╔╝ ███████║██║██████╔╝██║ ██║ +██╔═██╗ ██╔══██║██║██╔══██╗██║ ██║ +██║ ██╗██║ ██║██║██║ ██║╚██████╔╝ +╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝"; + +const TAGLINE: &str = "Standalone Discord audio sending node · Lavalink v4 compatible"; + +/// Print the logo, version and a short facts line to stdout. +/// +/// Written directly rather than through `tracing` so it lands before the subscriber is installed +/// and never carries a log prefix. Skipped when the banner is turned off, and always when the log +/// format is JSON: stdout is a machine-readable stream then, and a banner would corrupt it. +pub fn print(cfg: &LoggingConfig) { + if !cfg.banner || cfg.format == LogFormat::Json { + return; + } + + let (accent, bold, dim, reset) = if cfg.color { + (ACCENT, BOLD, DIM, RESET) + } else { + ("", "", "", "") + }; + + let cores = std::thread::available_parallelism() + .map(|n| n.get().to_string()) + .unwrap_or_else(|_| "?".to_string()); + let facts = format!( + "v{} · {} cores · {}/{}", + env!("CARGO_PKG_VERSION"), + cores, + std::env::consts::OS, + std::env::consts::ARCH, + ); + + println!("\n{accent}{bold}{LOGO}{reset}\n"); + println!(" {dim}{TAGLINE}{reset}"); + println!(" {dim}{facts}{reset}\n"); +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 9d976c9..65854aa 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,8 @@ -//! Logging, installed once at startup. +//! Startup odds and ends: logging, the console banner, and the update check. +pub(crate) mod ansi; +pub mod banner; mod logging; +pub mod update; pub use logging::init; diff --git a/src/utils/update.rs b/src/utils/update.rs new file mode 100644 index 0000000..0e2d9ce --- /dev/null +++ b/src/utils/update.rs @@ -0,0 +1,129 @@ +//! A one-shot check against the GitHub releases API, announcing an upgrade when one is newer. + +use std::time::Duration; + +use semver::Version; +use serde::Deserialize; + +use crate::config::{LogFormat, LoggingConfig}; +use crate::utils::ansi::{ACCENT, BOLD, DIM, RESET}; + +const RELEASES_API: &str = "https://api.github.com/repos/bongo-devs/Kairo/releases/latest"; +const IMAGE: &str = "ghcr.io/bongo-devs/kairo:latest"; +const TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Deserialize)] +struct Release { + tag_name: String, +} + +/// Ask GitHub for the latest release once and, if its tag outranks the running build, announce how +/// to upgrade. Set `KAIRO_NO_UPDATE_CHECK` to skip it entirely. +/// +/// Every failure is logged at debug and otherwise dropped: a version check has no business keeping +/// the node from serving, and a proxied or air-gapped host is expected to fail here. The default +/// client honours `HTTPS_PROXY`, so a proxied deployment still reaches GitHub without extra config. +pub async fn check(logging: &LoggingConfig) { + if std::env::var_os("KAIRO_NO_UPDATE_CHECK").is_some() { + return; + } + + let Ok(current) = Version::parse(env!("CARGO_PKG_VERSION")) else { + return; + }; + + let release = match latest().await { + Ok(Some(release)) => release, + Ok(None) => return, + Err(err) => { + tracing::debug!("update check failed: {err}"); + return; + } + }; + + match upgrade_to(¤t, &release.tag_name) { + Some(latest) => announce(logging, ¤t, &latest), + None => tracing::debug!("Kairo is up to date (v{current})"), + } +} + +// Draw the upgrade notice as a whitespace-isolated callout with the banner's accent gutter, so it +// carries the same look and reads apart from the log stream around it. Under JSON logging stdout +// must stay machine-readable, so there it is a structured warning instead. +fn announce(logging: &LoggingConfig, current: &Version, latest: &Version) { + if logging.format == LogFormat::Json { + tracing::warn!(%current, %latest, "a new Kairo release is available"); + return; + } + + let (accent, bold, dim, reset) = if logging.color { + (ACCENT, BOLD, DIM, RESET) + } else { + ("", "", "", "") + }; + + println!( + "\n {accent}▍{reset} {bold}Kairo v{latest} is available{reset} {dim}· running v{current}{reset}\ + \n {accent}▍{reset} {dim}docker pull {IMAGE} · then restart the container{reset}\n" + ); +} + +// The release tag outranks `current`, if it parses and is strictly newer. Tags carry an optional +// leading `v` that is not part of the semver. +fn upgrade_to(current: &Version, tag: &str) -> Option { + Version::parse(tag.trim_start_matches('v')) + .ok() + .filter(|latest| latest > current) +} + +async fn latest() -> reqwest::Result> { + let client = reqwest::Client::builder() + .user_agent(concat!("kairo/", env!("CARGO_PKG_VERSION"))) + .timeout(TIMEOUT) + .build()?; + + let response = client + .get(RELEASES_API) + .header("Accept", "application/vnd.github+json") + .send() + .await?; + + // No published releases answers 404; nothing to compare against, and not worth a log line. + if !response.status().is_success() { + return Ok(None); + } + + Ok(Some(response.json::().await?)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn recognises_a_newer_tag() { + let current = Version::parse("4.6.0").unwrap(); + assert_eq!( + upgrade_to(¤t, "v4.7.0"), + Some(Version::parse("4.7.0").unwrap()) + ); + assert_eq!( + upgrade_to(¤t, "5.0.0"), + Some(Version::parse("5.0.0").unwrap()) + ); + } + + #[test] + fn ignores_same_or_older_tags() { + let current = Version::parse("4.6.0").unwrap(); + assert_eq!(upgrade_to(¤t, "v4.6.0"), None); + assert_eq!(upgrade_to(¤t, "4.5.9"), None); + } + + #[test] + fn ignores_unparsable_tags() { + let current = Version::parse("4.6.0").unwrap(); + assert_eq!(upgrade_to(¤t, "nightly"), None); + assert_eq!(upgrade_to(¤t, ""), None); + } +} From 21c9431d88c9642396b0aa8416e40a244429d19f Mon Sep 17 00:00:00 2001 From: notdeltaxd <151985453+notdeltaxd@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:07:10 +0530 Subject: [PATCH 2/2] Pin lyrics v0.2.5 and bump to 4.7.0 Picks up scored provider searches, so lyrics are matched to the right recording by title, artist and length rather than the first search hit. --- Cargo.lock | 6 +++--- Cargo.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47baaef..a212564 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1430,7 +1430,7 @@ dependencies = [ [[package]] name = "kairo" -version = "4.5.0" +version = "4.7.0" dependencies = [ "axum", "bytes", @@ -1749,8 +1749,8 @@ checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "lyrics" -version = "0.2.4" -source = "git+https://github.com/bongo-devs/lyrics?tag=v0.2.4#f2aab97f1f4abcf14f75e8a53669eea40dcee6dd" +version = "0.2.5" +source = "git+https://github.com/bongo-devs/lyrics?tag=v0.2.5#25d82588934aa5c238fe1ca6fb975313f5c7331b" dependencies = [ "async-trait", "base64", diff --git a/Cargo.toml b/Cargo.toml index 8a80346..d1e3691 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kairo" -version = "4.6.0" +version = "4.7.0" edition = "2021" license = "MIT" description = "Standalone Discord audio sending node, written in Rust. Drop-in Lavalink v4 compatible." @@ -21,7 +21,7 @@ tracing-appender = "0.2" [dependencies.lyrics] git = "https://github.com/bongo-devs/lyrics" -tag = "v0.2.4" +tag = "v0.2.5" [dependencies.player] git = "https://github.com/bongo-devs/player"