use audio_player::PlayerMessage; use crabidy_core::proto::crabidy::{ crabidy_service_server::CrabidyServiceServer, InitResponse, LibraryNode, PlayState, Track, }; use crabidy_core::{ProviderClient, ProviderError}; use tracing::{debug, error, info, instrument, warn, Span}; use tracing_subscriber::{prelude::*, EnvFilter}; mod playback; use playback::Playback; mod provider; use provider::ProviderOrchestrator; mod rpc; use rpc::RpcService; use tonic::{transport::Server, Result}; const LISTEN_ADDR: &str = "0.0.0.0:50051"; #[tokio::main] async fn main() -> Result<(), Box> { let _log_guard = init_tracing(); let (update_tx, _) = tokio::sync::broadcast::channel(2048); let orchestrator = ProviderOrchestrator::init("").await.map_err(|err| { error!("failed to init provider orchestrator: {err}"); err })?; // Queue persistence is optional: without a usable queues directory the // server runs with an in-memory queue only. let queue_store = match crabidy_server::queue_store::queues_dir() { Some(dir) => match crabidy_server::queue_store::QueueStore::open(dir).await { Ok(store) => Some(std::sync::Arc::new(store)), Err(err) => { warn!("queue persistence disabled: {err}"); None } }, None => { warn!("queue persistence disabled: no config directory"); None } }; let playback = Playback::new( update_tx.clone(), orchestrator.provider_tx.clone(), queue_store, ); // Reload the persisted current queue before anything can observe or // mutate state; never starts playback. playback.restore_current().await; let playback_tx = playback.playback_tx.clone(); let player_msg = playback.player.messages.clone(); std::thread::spawn(|| { poll_play_bus(player_msg, playback_tx); }); info!("player message forwarder started"); let crabidy_service = RpcService::new( update_tx, playback.playback_tx.clone(), orchestrator.provider_tx.clone(), ); orchestrator.run(); info!("provider orchestrator started"); playback.run(); info!("playback started"); let addr = LISTEN_ADDR.parse()?; info!(%addr, "grpc server listening"); Server::builder() .add_service(CrabidyServiceServer::new(crabidy_service)) .serve(addr) .await?; Ok(()) } /// Installs the global tracing subscriber. /// /// The filter honors `RUST_LOG`; without it, our own crates log at debug and /// everything else at info. Returns the guard that flushes the non-blocking /// writer on shutdown. fn init_tracing() -> tracing_appender::non_blocking::WorkerGuard { let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stderr()); let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { EnvFilter::new( "info,crabidy_server=debug,crabidy_core=debug,tidaldy=debug,audio_player=debug", ) }); let fmt_layer = tracing_subscriber::fmt::layer() .with_writer(non_blocking) .with_target(true) .with_file(true) .with_line_number(true); // .init() also installs the log-to-tracing bridge for libraries that // use the `log` crate (symphonia, cpal, ...). tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .init(); guard } /// Forwards player engine events into the playback message loop. #[instrument(skip(rx, tx))] fn poll_play_bus(rx: flume::Receiver, tx: flume::Sender) { for msg in rx.iter() { let command = match msg { PlayerMessage::EndOfStream => { debug!("player reported end of stream"); PlaybackCommand::Next } PlayerMessage::Stopped => PlaybackCommand::StateChanged { state: PlayState::Stopped, }, PlayerMessage::Paused => PlaybackCommand::StateChanged { state: PlayState::Paused, }, PlayerMessage::Playing => PlaybackCommand::StateChanged { state: PlayState::Playing, }, PlayerMessage::Elapsed { duration, elapsed } => PlaybackCommand::PositionChanged { duration: duration.as_millis() as u32, position: elapsed.as_millis() as u32, }, PlayerMessage::Duration { duration } => PlaybackCommand::PositionChanged { duration: duration.as_millis() as u32, position: 0, }, }; if let Err(err) = tx.send(PlaybackMessage::new(command)) { error!("failed to forward player message: {err}"); return; } } warn!("player message channel closed"); } /// A command for the provider orchestrator, tagged with the tracing span that /// was current when it was sent so the handler can attribute its events to /// the originating request. #[derive(Debug)] pub struct ProviderMessage { pub span: Span, pub command: ProviderCommand, } impl ProviderMessage { pub fn new(command: ProviderCommand) -> Self { Self { span: Span::current(), command, } } } #[derive(Debug)] pub enum ProviderCommand { GetLibraryNode { path: String, result_tx: flume::Sender>, }, GetTrackUrls { path: String, result_tx: flume::Sender, ProviderError>>, }, /// Resolves a path into playable tracks: a track path yields that single /// track, a node path yields all tracks reachable below it. Streamed: /// zero or more in-order chunks arrive on `chunk_tx`, the sender is /// dropped when resolution finishes, and dropping the receiver cancels /// it (see `ProviderClient::resolve_tracks_into`). The orchestrator /// handles this command on a spawned task so its loop stays free for /// other commands (notably `GetTrackUrls` for the first chunk's track). ResolveTracks { path: String, chunk_tx: flume::Sender>, }, /// Creates a child under a creatable node (see /// `ProviderClient::create_lib_node`); replies with the created node. CreateLibraryNode { parent_path: String, title: String, result_tx: flume::Sender>, }, /// Renames an editable node (see `ProviderClient::rename_lib_node`); /// replies with the renamed node at its new path. RenameLibraryNode { path: String, new_title: String, result_tx: flume::Sender>, }, /// Deletes a deletable node (see `ProviderClient::delete_lib_node`); /// replies with the refreshed parent node. DeleteLibraryNode { path: String, result_tx: flume::Sender>, }, /// Captures the queueable subtree at `path` as the bookmark `name` /// (see `architecture/bookmarks.md` D1–D3). Handled on a spawned task — /// a large walk must not block the orchestrator loop. CaptureLibraryNode { path: String, name: String, result_tx: flume::Sender>, }, } impl ProviderCommand { pub fn name(&self) -> &'static str { match self { Self::GetLibraryNode { .. } => "get_library_node", Self::GetTrackUrls { .. } => "get_track_urls", Self::ResolveTracks { .. } => "resolve_tracks", Self::CreateLibraryNode { .. } => "create_library_node", Self::RenameLibraryNode { .. } => "rename_library_node", Self::DeleteLibraryNode { .. } => "delete_library_node", Self::CaptureLibraryNode { .. } => "capture_library_node", } } } /// A command for the playback loop, tagged like [`ProviderMessage`]. #[derive(Debug)] pub struct PlaybackMessage { pub span: Span, pub command: PlaybackCommand, } impl PlaybackMessage { pub fn new(command: PlaybackCommand) -> Self { Self { span: Span::current(), command, } } } #[derive(Debug)] pub enum PlaybackCommand { Init { result_tx: flume::Sender, }, Replace { paths: Vec, }, Queue { paths: Vec, }, Append { paths: Vec, }, Remove { positions: Vec, }, Insert { position: u32, paths: Vec, }, /// Internal: a resolved chunk of tracks for the pending queue operation /// `op_id`, sent by that operation's forwarder task. Chunks for an /// unknown (finished or cancelled) op are dropped silently. ApplyResolvedChunk { op_id: u64, tracks: Vec, }, /// Internal: the forwarder task for `op_id` has seen the provider drop /// its chunk sender — the operation is complete and the `resolving` /// flag clears once no pending operations remain. ResolveFinished { op_id: u64, }, Clear { exclude_current: bool, }, SetCurrent { position: u32, }, /// Saves the current queue under a name (see /// `architecture/queue-persistence.md` D6). Handled on the loop so the /// snapshot is consistent; the disk write happens on a spawned task and /// reports through `result_tx`. SaveQueue { name: String, result_tx: flume::Sender>, }, ToggleShuffle, ToggleRepeat, TogglePlay, Stop, ChangeVolume { delta: f32, }, ToggleMute, Next, Prev, RestartTrack, StateChanged { state: PlayState, }, VolumeChanged { volume: f32, }, MuteChanged { muted: bool, }, PositionChanged { duration: u32, position: u32, }, } impl PlaybackCommand { pub fn name(&self) -> &'static str { match self { Self::Init { .. } => "init", Self::Replace { .. } => "replace", Self::Queue { .. } => "queue", Self::Append { .. } => "append", Self::Remove { .. } => "remove", Self::Insert { .. } => "insert", Self::ApplyResolvedChunk { .. } => "apply_resolved_chunk", Self::ResolveFinished { .. } => "resolve_finished", Self::Clear { .. } => "clear", Self::SetCurrent { .. } => "set_current", Self::SaveQueue { .. } => "save_queue", Self::ToggleShuffle => "toggle_shuffle", Self::ToggleRepeat => "toggle_repeat", Self::TogglePlay => "toggle_play", Self::Stop => "stop", Self::ChangeVolume { .. } => "change_volume", Self::ToggleMute => "toggle_mute", Self::Next => "next", Self::Prev => "prev", Self::RestartTrack => "restart_track", Self::StateChanged { .. } => "state_changed", Self::VolumeChanged { .. } => "volume_changed", Self::MuteChanged { .. } => "mute_changed", Self::PositionChanged { .. } => "position_changed", } } }