crabidy/crabidy-server/src/main.rs

351 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<dyn std::error::Error>> {
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<PlayerMessage>, tx: flume::Sender<PlaybackMessage>) {
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<Result<LibraryNode, ProviderError>>,
},
GetTrackUrls {
path: String,
result_tx: flume::Sender<Result<Vec<String>, 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<Vec<Track>>,
},
/// 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<Result<LibraryNode, ProviderError>>,
},
/// 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<Result<LibraryNode, ProviderError>>,
},
/// Deletes a deletable node (see `ProviderClient::delete_lib_node`);
/// replies with the refreshed parent node.
DeleteLibraryNode {
path: String,
result_tx: flume::Sender<Result<LibraryNode, ProviderError>>,
},
/// Captures the queueable subtree at `path` as the bookmark `name`
/// (see `architecture/bookmarks.md` D1D3). Handled on a spawned task —
/// a large walk must not block the orchestrator loop.
CaptureLibraryNode {
path: String,
name: String,
result_tx: flume::Sender<Result<(), crabidy_server::bookmark_store::CaptureError>>,
},
}
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<InitResponse>,
},
Replace {
paths: Vec<String>,
},
Queue {
paths: Vec<String>,
},
Append {
paths: Vec<String>,
},
Remove {
positions: Vec<u32>,
},
Insert {
position: u32,
paths: Vec<String>,
},
/// 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<Track>,
},
/// 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<Result<(), crabidy_server::queue_store::SaveQueueError>>,
},
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",
}
}
}