use anyhow::anyhow;
use clap::Parser;
use plexus_core::plexus::DynamicHub;
use plexus_mono::{MonoHub, PlayerHub};
use plexus_transport::TransportServer;
use std::sync::Arc;
#[derive(Parser, Debug)]
#[command(name = "plexus-mono")]
#[command(
about = "Monochrome music API standalone Plexus RPC server — search, metadata, lyrics, recommendations, playback"
)]
struct Args {
#[arg(long)]
stdio: bool,
#[arg(short, long, default_value = "4448")]
port: u16,
#[arg(long)]
mcp: bool,
#[arg(long, env = "MONO_API_URL")]
api_url: Option<String>,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let filter = if args.stdio {
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"))
} else {
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new("warn,plexus_mono=debug")
})
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
tracing::info!("Starting plexus-mono at {}", chrono::Utc::now());
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let is_stdio = args.stdio;
rt.spawn(async move {
if let Err(e) = run_server(args).await {
tracing::error!("server error: {e}");
std::process::exit(1);
}
});
if is_stdio {
rt.block_on(futures::future::pending::<()>());
} else {
run_main_loop();
}
Ok(())
}
async fn run_server(args: Args) -> anyhow::Result<()> {
let mono_hub = if let Some(ref url) = args.api_url {
tracing::info!("Using custom API URL: {}", url);
MonoHub::with_url(url).await
} else {
tracing::info!("Using default API: https://api.monochrome.tf");
MonoHub::new().await
};
let player_hub = PlayerHub::new(mono_hub.client()).await;
let hub = Arc::new(
DynamicHub::new("monochrome")
.register(mono_hub) .register_hub(player_hub), );
tracing::info!("plexus-mono initialized");
tracing::info!(" Hub: monochrome");
tracing::info!(" Activations: monochrome (API), player (playback)");
tracing::info!(" Version: {}", env!("CARGO_PKG_VERSION"));
let rpc_converter = |arc: Arc<DynamicHub>| {
DynamicHub::arc_into_rpc_module(arc)
.map_err(|e| anyhow!("Failed to create RPC module: {e}"))
};
let mut builder = TransportServer::builder(hub, rpc_converter);
if args.stdio {
builder = builder.with_stdio();
} else {
builder = builder.with_websocket(args.port);
if args.mcp {
builder = builder.with_mcp_http(args.port + 1);
}
}
if args.stdio {
tracing::info!("Starting stdio transport (MCP-compatible)");
} else {
tracing::info!("plexus-mono server started");
tracing::info!(" WebSocket: ws://127.0.0.1:{}", args.port);
if args.mcp {
tracing::info!(
" MCP HTTP: http://127.0.0.1:{}/mcp",
args.port + 1
);
}
tracing::info!("");
tracing::info!("Usage examples:");
tracing::info!(
" synapse -P {} monochrome monochrome search --query 'radiohead'",
args.port
);
tracing::info!(
" synapse -P {} monochrome monochrome track --id 12345",
args.port
);
tracing::info!(
" synapse -P {} monochrome player play --id 55391801",
args.port
);
tracing::info!(
" synapse -P {} monochrome player now_playing",
args.port
);
tracing::info!(
" synapse -P {} monochrome player playlist list",
args.port
);
}
builder.build().await?.serve().await
}
#[cfg(target_os = "macos")]
fn run_main_loop() {
extern "C" {
fn CFRunLoopRun();
}
unsafe {
CFRunLoopRun();
}
}
#[cfg(not(target_os = "macos"))]
fn run_main_loop() {
loop {
std::thread::park();
}
}