Overhaul tracing: fix span misattribution, broaden coverage
The old pattern passed a Span in every channel message and entered it
with a guard that was held across await points, which misattributed
events from interleaved tasks. Messages are now {span, command} pairs:
the span is captured automatically at send time (Span::current) and the
consumer instruments the whole handler future with a child span
(playback_command/provider_command with a command name field), so events
are attributed correctly across the queue boundary and all the manual
in_current_span() plumbing is gone.
Also:
- server: EnvFilter with RUST_LOG support (default: own crates at
debug, rest at info), log-crate bridge for symphonia/cpal
- cbd-tui: logs to a file under the state dir (the terminal belongs to
the TUI), EnvFilter, no more println into the alternate screen
- tidaldy: fix misused levels (error->debug), structured fields,
payload dumps moved to trace, login flow at info/warn
- no panic on missing notification daemon in the TUI
- no panic on backwards clock steps in QueueManager
- provider init errors propagate instead of expect()
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6eb5a87b15
commit
6d8bc7f166
|
|
@ -454,6 +454,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"crabidy-core",
|
||||
"crossterm",
|
||||
"dirs",
|
||||
"flume",
|
||||
"notify-rust",
|
||||
"ratatui",
|
||||
|
|
@ -461,6 +462,9 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ edition.workspace = true
|
|||
[dependencies]
|
||||
crabidy-core.workspace = true
|
||||
crossterm.workspace = true
|
||||
dirs.workspace = true
|
||||
flume.workspace = true
|
||||
notify-rust.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
|
@ -13,3 +14,6 @@ serde.workspace = true
|
|||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-stream.workspace = true
|
||||
tonic.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-appender.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
|
|
|||
|
|
@ -56,11 +56,14 @@ impl NowPlaying {
|
|||
} else {
|
||||
format!("{} by {}", track.title, track.artist,)
|
||||
};
|
||||
Notification::new()
|
||||
// A missing notification daemon must not crash the TUI.
|
||||
if let Err(err) = Notification::new()
|
||||
.summary("Now playing")
|
||||
.body(&body)
|
||||
.show()
|
||||
.unwrap();
|
||||
{
|
||||
tracing::debug!("could not show desktop notification: {err}");
|
||||
}
|
||||
}
|
||||
self.track = active;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,45 @@ use tokio_stream::StreamExt;
|
|||
use app::{App, MessageFromUi, MessageToUi, StatefulList, UiFocus};
|
||||
use config::Config;
|
||||
use rpc::RpcClient;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
static CONFIG: OnceLock<Config> = OnceLock::new();
|
||||
|
||||
/// Logs to a file: the terminal is owned by the TUI, so writing log lines to
|
||||
/// stdout/stderr would corrupt the interface.
|
||||
fn init_tracing() -> Option<tracing_appender::non_blocking::WorkerGuard> {
|
||||
use tracing_subscriber::{prelude::*, EnvFilter};
|
||||
|
||||
let log_dir = dirs::state_dir()
|
||||
.or_else(dirs::cache_dir)
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join("crabidy");
|
||||
if let Err(err) = std::fs::create_dir_all(&log_dir) {
|
||||
eprintln!(
|
||||
"could not create log directory {}: {err}",
|
||||
log_dir.display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let file_appender = tracing_appender::rolling::daily(&log_dir, "cbd-tui.log");
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info,cbd_tui=debug,crabidy_core=debug"));
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_writer(non_blocking)
|
||||
.with_ansi(false)
|
||||
.with_target(true),
|
||||
)
|
||||
.init();
|
||||
Some(guard)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _log_guard = init_tracing();
|
||||
let config = CONFIG.get_or_init(|| crabidy_core::init_config("cbd-tui.toml"));
|
||||
|
||||
let (ui_tx, rx): (Sender<MessageFromUi>, Receiver<MessageFromUi>) = flume::unbounded();
|
||||
|
|
@ -52,6 +86,7 @@ async fn orchestrate<'a>(
|
|||
config: &'static Config,
|
||||
(tx, rx): (Sender<MessageToUi>, Receiver<MessageFromUi>),
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
info!(address = config.server.address, "connecting to server");
|
||||
let mut rpc_client = rpc::RpcClient::connect(&config.server.address).await?;
|
||||
|
||||
if let Some(root_node) = rpc_client.get_library_node("node:/").await? {
|
||||
|
|
@ -59,11 +94,12 @@ async fn orchestrate<'a>(
|
|||
}
|
||||
|
||||
let init_data = rpc_client.init().await?;
|
||||
info!("received initial state from server");
|
||||
tx.send_async(MessageToUi::Init(init_data)).await?;
|
||||
|
||||
loop {
|
||||
if let Err(er) = poll(&mut rpc_client, &rx, &tx).await {
|
||||
println!("ERROR");
|
||||
if let Err(err) = poll(&mut rpc_client, &rx, &tx).await {
|
||||
error!("request to server failed: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -135,8 +171,10 @@ async fn poll(
|
|||
tx.send_async(MessageToUi::Update(update)).await?;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
Err(err) => {
|
||||
warn!("update stream broke, reconnecting: {err}");
|
||||
rpc_client.reconnect_update_stream().await;
|
||||
info!("update stream reconnected");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ impl std::fmt::Display for ProviderError {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ProviderError {}
|
||||
|
||||
impl LibraryNode {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ pub struct QueueManager {
|
|||
impl From<QueueManager> for Queue {
|
||||
fn from(queue_manager: QueueManager) -> Self {
|
||||
Self {
|
||||
// A clock step backwards must not panic the playback loop.
|
||||
timestamp: queue_manager
|
||||
.created_at
|
||||
.elapsed()
|
||||
.expect("failed to get elapsed time")
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
current_position: queue_manager.current_position() as u32,
|
||||
tracks: queue_manager.tracks,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use crabidy_core::proto::crabidy::{
|
|||
crabidy_service_server::CrabidyServiceServer, InitResponse, LibraryNode, PlayState, Track,
|
||||
};
|
||||
use crabidy_core::{ProviderClient, ProviderError};
|
||||
use tracing::{debug_span, error, info, instrument, level_filters, warn, Span};
|
||||
use tracing_subscriber::{filter::Targets, prelude::*};
|
||||
use tracing::{debug, error, info, instrument, warn, Span};
|
||||
use tracing_subscriber::{prelude::*, EnvFilter};
|
||||
|
||||
mod playback;
|
||||
use playback::Playback;
|
||||
|
|
@ -15,34 +15,17 @@ 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>> {
|
||||
if let Err(err) = tracing_log::LogTracer::init_with_filter(log::LevelFilter::Debug) {
|
||||
println!("Failed to initialize log tracer: {}", err);
|
||||
}
|
||||
let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stderr());
|
||||
|
||||
let targets_filter = Targets::new()
|
||||
.with_target("crabidy_server", tracing::level_filters::LevelFilter::DEBUG)
|
||||
.with_target("tidaldy", level_filters::LevelFilter::DEBUG);
|
||||
let subscriber = tracing_subscriber::fmt::layer()
|
||||
.with_writer(non_blocking)
|
||||
.with_file(true)
|
||||
.with_line_number(true);
|
||||
|
||||
let registry = tracing_subscriber::registry()
|
||||
.with(targets_filter)
|
||||
.with(subscriber);
|
||||
|
||||
tracing::subscriber::set_global_default(registry)
|
||||
.expect("Setting the default tracing subscriber failed");
|
||||
|
||||
info!("audio player started initialized");
|
||||
let _log_guard = init_tracing();
|
||||
|
||||
let (update_tx, _) = tokio::sync::broadcast::channel(2048);
|
||||
let orchestrator = ProviderOrchestrator::init("")
|
||||
.await
|
||||
.expect("failed to init orchestrator");
|
||||
let orchestrator = ProviderOrchestrator::init("").await.map_err(|err| {
|
||||
error!("failed to init provider orchestrator: {err}");
|
||||
err
|
||||
})?;
|
||||
|
||||
let playback = Playback::new(update_tx.clone(), orchestrator.provider_tx.clone());
|
||||
|
||||
|
|
@ -52,7 +35,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
std::thread::spawn(|| {
|
||||
poll_play_bus(player_msg, playback_tx);
|
||||
});
|
||||
info!("gstreamer bus handler started");
|
||||
info!("player message forwarder started");
|
||||
|
||||
let crabidy_service = RpcService::new(
|
||||
update_tx,
|
||||
|
|
@ -64,7 +47,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
playback.run();
|
||||
info!("playback started");
|
||||
|
||||
let addr = "0.0.0.0:50051".parse()?;
|
||||
let addr = LISTEN_ADDR.parse()?;
|
||||
info!(%addr, "grpc server listening");
|
||||
Server::builder()
|
||||
.add_service(CrabidyServiceServer::new(crabidy_service))
|
||||
.serve(addr)
|
||||
|
|
@ -73,165 +57,216 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(fmt_layer)
|
||||
.init();
|
||||
|
||||
// Forward records from libraries that use the `log` crate (symphonia,
|
||||
// cpal, ...) into tracing.
|
||||
if let Err(err) = tracing_log::LogTracer::init() {
|
||||
warn!("failed to initialize log-to-tracing bridge: {err}");
|
||||
}
|
||||
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 span = debug_span!("play-chan");
|
||||
match msg {
|
||||
let command = match msg {
|
||||
PlayerMessage::EndOfStream => {
|
||||
if let Err(err) = tx.send(PlaybackMessage::Next { span }) {
|
||||
error!("failed to send next message: {}", err);
|
||||
}
|
||||
}
|
||||
PlayerMessage::Stopped => {
|
||||
if let Err(err) = tx.send(PlaybackMessage::StateChanged {
|
||||
state: PlayState::Stopped,
|
||||
span,
|
||||
}) {
|
||||
error!("failed to send stopped message: {}", err);
|
||||
}
|
||||
}
|
||||
PlayerMessage::Paused => {
|
||||
if let Err(err) = tx.send(PlaybackMessage::StateChanged {
|
||||
state: PlayState::Paused,
|
||||
span,
|
||||
}) {
|
||||
error!("failed to send paused message: {}", err);
|
||||
}
|
||||
}
|
||||
PlayerMessage::Playing => {
|
||||
if let Err(err) = tx.send(PlaybackMessage::StateChanged {
|
||||
state: PlayState::Playing,
|
||||
span,
|
||||
}) {
|
||||
error!("failed to send playing message: {}", err);
|
||||
}
|
||||
}
|
||||
PlayerMessage::Elapsed { duration, elapsed } => {
|
||||
if let Err(err) = tx.send(PlaybackMessage::PostitionChanged {
|
||||
duration: duration.as_millis() as u32,
|
||||
position: elapsed.as_millis() as u32,
|
||||
span,
|
||||
}) {
|
||||
error!("failed to send elapsed message: {}", err);
|
||||
}
|
||||
}
|
||||
PlayerMessage::Duration { duration } => {
|
||||
if let Err(err) = tx.send(PlaybackMessage::PostitionChanged {
|
||||
duration: duration.as_millis() as u32,
|
||||
position: 0,
|
||||
span,
|
||||
}) {
|
||||
error!("failed to send duration message: {}", err);
|
||||
}
|
||||
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 ProviderMessage {
|
||||
pub enum ProviderCommand {
|
||||
GetLibraryNode {
|
||||
uuid: String,
|
||||
result_tx: flume::Sender<Result<LibraryNode, ProviderError>>,
|
||||
span: Span,
|
||||
},
|
||||
GetTrack {
|
||||
uuid: String,
|
||||
result_tx: flume::Sender<Result<Track, ProviderError>>,
|
||||
span: Span,
|
||||
},
|
||||
GetTrackUrls {
|
||||
uuid: String,
|
||||
result_tx: flume::Sender<Result<Vec<String>, ProviderError>>,
|
||||
span: Span,
|
||||
},
|
||||
FlattenNode {
|
||||
uuid: String,
|
||||
result_tx: flume::Sender<Vec<Track>>,
|
||||
span: Span,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProviderCommand {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::GetLibraryNode { .. } => "get_library_node",
|
||||
Self::GetTrack { .. } => "get_track",
|
||||
Self::GetTrackUrls { .. } => "get_track_urls",
|
||||
Self::FlattenNode { .. } => "flatten_node",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A command for the playback loop, tagged like [`ProviderMessage`].
|
||||
#[derive(Debug)]
|
||||
pub enum PlaybackMessage {
|
||||
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>,
|
||||
span: Span,
|
||||
},
|
||||
Replace {
|
||||
uuids: Vec<String>,
|
||||
span: Span,
|
||||
},
|
||||
Queue {
|
||||
uuids: Vec<String>,
|
||||
span: Span,
|
||||
},
|
||||
Append {
|
||||
uuids: Vec<String>,
|
||||
span: Span,
|
||||
},
|
||||
Remove {
|
||||
positions: Vec<u32>,
|
||||
span: Span,
|
||||
},
|
||||
Insert {
|
||||
position: u32,
|
||||
uuids: Vec<String>,
|
||||
span: Span,
|
||||
},
|
||||
Clear {
|
||||
exclude_current: bool,
|
||||
span: Span,
|
||||
},
|
||||
SetCurrent {
|
||||
position: u32,
|
||||
span: Span,
|
||||
},
|
||||
ToggleShuffle {
|
||||
span: Span,
|
||||
},
|
||||
ToggleRepeat {
|
||||
span: Span,
|
||||
},
|
||||
TogglePlay {
|
||||
span: Span,
|
||||
},
|
||||
Stop {
|
||||
span: Span,
|
||||
},
|
||||
ToggleShuffle,
|
||||
ToggleRepeat,
|
||||
TogglePlay,
|
||||
Stop,
|
||||
ChangeVolume {
|
||||
delta: f32,
|
||||
span: Span,
|
||||
},
|
||||
ToggleMute {
|
||||
span: Span,
|
||||
},
|
||||
Next {
|
||||
span: Span,
|
||||
},
|
||||
Prev {
|
||||
span: Span,
|
||||
},
|
||||
RestartTrack {
|
||||
span: Span,
|
||||
},
|
||||
ToggleMute,
|
||||
Next,
|
||||
Prev,
|
||||
RestartTrack,
|
||||
StateChanged {
|
||||
state: PlayState,
|
||||
span: Span,
|
||||
},
|
||||
VolumeChanged {
|
||||
volume: f32,
|
||||
span: Span,
|
||||
},
|
||||
|
||||
MuteChanged {
|
||||
muted: bool,
|
||||
span: Span,
|
||||
},
|
||||
PostitionChanged {
|
||||
PositionChanged {
|
||||
duration: u32,
|
||||
position: u32,
|
||||
span: Span,
|
||||
},
|
||||
}
|
||||
|
||||
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::Clear { .. } => "clear",
|
||||
Self::SetCurrent { .. } => "set_current",
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,88 +1,80 @@
|
|||
use crate::ProviderMessage;
|
||||
use crate::{ProviderCommand, ProviderMessage};
|
||||
use async_trait::async_trait;
|
||||
use crabidy_core::{
|
||||
proto::crabidy::{LibraryNode, LibraryNodeChild, Track},
|
||||
ProviderClient, ProviderError,
|
||||
};
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use tracing::{debug, error, instrument, warn, Instrument};
|
||||
use tracing::{debug, debug_span, error, instrument, warn, Instrument};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProviderOrchestrator {
|
||||
pub provider_tx: flume::Sender<ProviderMessage>,
|
||||
provider_rx: flume::Receiver<ProviderMessage>,
|
||||
// known_tracks: RwLock<HashMap<String, Track>>,
|
||||
// known_nodes: RwLock<HashMap<String, LibraryNode>>,
|
||||
tidal_client: Arc<tidaldy::Client>,
|
||||
}
|
||||
|
||||
impl ProviderOrchestrator {
|
||||
pub fn run(self) {
|
||||
tokio::spawn(async move {
|
||||
while let Ok(msg) = self.provider_rx.recv_async().await {
|
||||
match msg {
|
||||
ProviderMessage::GetLibraryNode {
|
||||
uuid,
|
||||
result_tx,
|
||||
span,
|
||||
} => {
|
||||
let _e = span.enter();
|
||||
let result = self.get_lib_node(&uuid).in_current_span().await;
|
||||
if let Err(err) = result_tx.send_async(result).in_current_span().await {
|
||||
error!("failed to send result: {}", err);
|
||||
}
|
||||
}
|
||||
ProviderMessage::GetTrack {
|
||||
uuid,
|
||||
result_tx,
|
||||
span,
|
||||
} => {
|
||||
let _e = span.enter();
|
||||
let result = self.get_metadata_for_track(&uuid).in_current_span().await;
|
||||
if let Err(err) = result_tx.send_async(result).in_current_span().await {
|
||||
error!("failed to send result: {}", err);
|
||||
}
|
||||
}
|
||||
ProviderMessage::GetTrackUrls {
|
||||
uuid,
|
||||
result_tx,
|
||||
span,
|
||||
} => {
|
||||
let _e = span.enter();
|
||||
let result = self.get_urls_for_track(&uuid).in_current_span().await;
|
||||
if let Err(err) = result_tx.send_async(result).in_current_span().await {
|
||||
error!("failed to send result: {}", err);
|
||||
}
|
||||
}
|
||||
ProviderMessage::FlattenNode {
|
||||
uuid,
|
||||
result_tx,
|
||||
span,
|
||||
} => {
|
||||
let _e = span.enter();
|
||||
let result = self.flatten_node(&uuid).in_current_span().await;
|
||||
if let Err(err) = result_tx.send_async(result).in_current_span().await {
|
||||
error!("failed to send result: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
while let Ok(ProviderMessage { span, command }) = self.provider_rx.recv_async().await {
|
||||
let handler_span =
|
||||
debug_span!(parent: &span, "provider_command", command = command.name());
|
||||
self.handle_command(command).instrument(handler_span).await;
|
||||
}
|
||||
warn!("provider message channel closed, loop exiting");
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_command(&self, command: ProviderCommand) {
|
||||
match command {
|
||||
ProviderCommand::GetLibraryNode { uuid, result_tx } => {
|
||||
let result = self.get_lib_node(&uuid).await;
|
||||
if let Err(err) = result_tx.send_async(result).await {
|
||||
error!("failed to send get_library_node result: {err}");
|
||||
}
|
||||
}
|
||||
ProviderCommand::GetTrack { uuid, result_tx } => {
|
||||
let result = self.get_metadata_for_track(&uuid).await;
|
||||
if let Err(err) = result_tx.send_async(result).await {
|
||||
error!("failed to send get_track result: {err}");
|
||||
}
|
||||
}
|
||||
ProviderCommand::GetTrackUrls { uuid, result_tx } => {
|
||||
let result = self.get_urls_for_track(&uuid).await;
|
||||
if let Err(err) = result_tx.send_async(result).await {
|
||||
error!("failed to send get_track_urls result: {err}");
|
||||
}
|
||||
}
|
||||
ProviderCommand::FlattenNode { uuid, result_tx } => {
|
||||
let result = self.flatten_node(&uuid).await;
|
||||
if let Err(err) = result_tx.send_async(result).await {
|
||||
error!("failed to send flatten_node result: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects all tracks reachable from the given node by walking its
|
||||
/// queueable descendants.
|
||||
#[instrument(skip(self))]
|
||||
async fn flatten_node(&self, node_uuid: &str) -> Vec<Track> {
|
||||
let mut tracks = Vec::with_capacity(1000);
|
||||
let mut nodes_to_go = Vec::with_capacity(100);
|
||||
nodes_to_go.push(node_uuid.to_string());
|
||||
let mut tracks = Vec::new();
|
||||
let mut nodes_to_go = vec![node_uuid.to_string()];
|
||||
while let Some(node_uuid) = nodes_to_go.pop() {
|
||||
let Ok(node) = self.get_lib_node(&node_uuid).in_current_span().await else {
|
||||
continue;
|
||||
let node = match self.get_lib_node(&node_uuid).await {
|
||||
Ok(node) => node,
|
||||
Err(err) => {
|
||||
warn!(node = node_uuid, "skipping unreadable node: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if node.is_queable {
|
||||
tracks.extend(node.tracks);
|
||||
nodes_to_go.extend(node.children.into_iter().map(|c| c.uuid))
|
||||
}
|
||||
}
|
||||
debug!(count = tracks.len(), "flattened node into tracks");
|
||||
tracks
|
||||
}
|
||||
}
|
||||
|
|
@ -95,29 +87,25 @@ impl ProviderClient for ProviderOrchestrator {
|
|||
.map(|d| d.join("crabidy"))
|
||||
.unwrap_or(PathBuf::from("/tmp"));
|
||||
let dir_exists = tokio::fs::try_exists(&config_dir)
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|e| ProviderError::Config(e.to_string()))?;
|
||||
if !dir_exists {
|
||||
tokio::fs::create_dir(&config_dir)
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|e| ProviderError::Config(e.to_string()))?;
|
||||
}
|
||||
let config_file = config_dir.join("tidaly.toml");
|
||||
let raw_toml_settings = fs::read_to_string(&config_file).unwrap_or("".to_owned());
|
||||
let tidal_client = Arc::new(
|
||||
tidaldy::Client::init(&raw_toml_settings)
|
||||
.in_current_span()
|
||||
.await
|
||||
.expect("Failed to init Tidal clienta"),
|
||||
);
|
||||
debug!(config_file = %config_file.display(), "loading tidal config");
|
||||
let raw_toml_settings = fs::read_to_string(&config_file).unwrap_or_default();
|
||||
let tidal_client = Arc::new(tidaldy::Client::init(&raw_toml_settings).await.map_err(
|
||||
|err| {
|
||||
error!("failed to init tidal client: {err}");
|
||||
err
|
||||
},
|
||||
)?);
|
||||
let new_toml_config = tidal_client.settings();
|
||||
if let Err(err) = tokio::fs::write(&config_file, new_toml_config)
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to write config file: {}", err);
|
||||
if let Err(err) = tokio::fs::write(&config_file, new_toml_config).await {
|
||||
error!("failed to write tidal config file: {err}");
|
||||
};
|
||||
let (provider_tx, provider_rx) = flume::bounded(100);
|
||||
Ok(Self {
|
||||
|
|
@ -126,46 +114,38 @@ impl ProviderClient for ProviderOrchestrator {
|
|||
tidal_client,
|
||||
})
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
|
||||
fn settings(&self) -> String {
|
||||
"".to_owned()
|
||||
String::new()
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn get_urls_for_track(&self, track_uuid: &str) -> Result<Vec<String>, ProviderError> {
|
||||
debug!("get_urls_for_track");
|
||||
self.tidal_client
|
||||
.get_urls_for_track(track_uuid)
|
||||
.in_current_span()
|
||||
.await
|
||||
self.tidal_client.get_urls_for_track(track_uuid).await
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn get_metadata_for_track(&self, track_uuid: &str) -> Result<Track, ProviderError> {
|
||||
debug!("get_metadata_for_track");
|
||||
self.tidal_client
|
||||
.get_metadata_for_track(track_uuid)
|
||||
.in_current_span()
|
||||
.await
|
||||
self.tidal_client.get_metadata_for_track(track_uuid).await
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
|
||||
fn get_lib_root(&self) -> LibraryNode {
|
||||
debug!("get_lib_root in provider manager");
|
||||
let mut root_node = LibraryNode::new();
|
||||
let child = LibraryNodeChild::new("node:tidal".to_owned(), "tidal".to_owned(), false);
|
||||
root_node.children.push(child);
|
||||
root_node
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn get_lib_node(&self, uuid: &str) -> Result<LibraryNode, ProviderError> {
|
||||
debug!("get_lib_node in provider manager");
|
||||
if uuid == "node:/" {
|
||||
debug!("get global root");
|
||||
debug!("serving global library root");
|
||||
return Ok(self.get_lib_root());
|
||||
}
|
||||
if uuid == "node:tidal" {
|
||||
debug!("get tidal root");
|
||||
debug!("serving tidal library root");
|
||||
return Ok(self.tidal_client.get_lib_root());
|
||||
}
|
||||
debug!("tidal node");
|
||||
self.tidal_client.get_lib_node(uuid).in_current_span().await
|
||||
self.tidal_client.get_lib_node(uuid).await
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{PlaybackMessage, ProviderMessage};
|
||||
use crate::{PlaybackCommand, PlaybackMessage, ProviderCommand, ProviderMessage};
|
||||
use crabidy_core::proto::crabidy::{
|
||||
crabidy_service_server::CrabidyService, get_update_stream_response::Update as StreamUpdate,
|
||||
AppendRequest, AppendResponse, ChangeVolumeRequest, ChangeVolumeResponse, ClearQueueRequest,
|
||||
|
|
@ -10,11 +10,10 @@ use crabidy_core::proto::crabidy::{
|
|||
StopResponse, ToggleMuteRequest, ToggleMuteResponse, TogglePlayRequest, TogglePlayResponse,
|
||||
ToggleRepeatRequest, ToggleRepeatResponse, ToggleShuffleRequest, ToggleShuffleResponse,
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use std::pin::Pin;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::{Request, Response, Status};
|
||||
use tracing::{debug, debug_span, error, instrument, trace, Instrument, Span};
|
||||
use tracing::{debug, error, instrument, trace};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RpcService {
|
||||
|
|
@ -25,16 +24,29 @@ pub struct RpcService {
|
|||
|
||||
impl RpcService {
|
||||
pub fn new(
|
||||
update_rx: tokio::sync::broadcast::Sender<StreamUpdate>,
|
||||
update_tx: tokio::sync::broadcast::Sender<StreamUpdate>,
|
||||
playback_tx: flume::Sender<PlaybackMessage>,
|
||||
provider_tx: flume::Sender<ProviderMessage>,
|
||||
) -> Self {
|
||||
Self {
|
||||
update_tx: update_rx,
|
||||
update_tx,
|
||||
playback_tx,
|
||||
provider_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a command to the playback loop, mapping channel failure to an
|
||||
/// internal error status.
|
||||
async fn send_playback(&self, command: PlaybackCommand) -> Result<(), Status> {
|
||||
let name = command.name();
|
||||
self.playback_tx
|
||||
.send_async(PlaybackMessage::new(command))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(command = name, "playback channel closed: {err}");
|
||||
Status::internal("playback loop unavailable")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
|
|
@ -44,26 +56,14 @@ impl CrabidyService for RpcService {
|
|||
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn init(&self, _request: Request<InitRequest>) -> Result<Response<InitResponse>, Status> {
|
||||
debug!("Received init request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
debug!("received init request");
|
||||
let (result_tx, result_rx) = flume::bounded(1);
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::Init { result_tx, span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("{:?}", err);
|
||||
return Err(Status::internal("Sending Init via internal channel failed"));
|
||||
}
|
||||
let response = result_rx
|
||||
.recv_async()
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{:?}", e);
|
||||
Status::internal("Failed to receive response from provider channel")
|
||||
})?;
|
||||
self.send_playback(PlaybackCommand::Init { result_tx })
|
||||
.await?;
|
||||
let response = result_rx.recv_async().await.map_err(|err| {
|
||||
error!("no reply from playback loop: {err}");
|
||||
Status::internal("playback loop did not reply")
|
||||
})?;
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
|
|
@ -73,32 +73,27 @@ impl CrabidyService for RpcService {
|
|||
request: Request<GetLibraryNodeRequest>,
|
||||
) -> Result<Response<GetLibraryNodeResponse>, Status> {
|
||||
let uuid = request.into_inner().uuid;
|
||||
Span::current().record("uuid", &uuid);
|
||||
debug!("Received get_library_node request");
|
||||
let provider_tx = self.provider_tx.clone();
|
||||
tracing::Span::current().record("uuid", uuid.as_str());
|
||||
debug!("received get_library_node request");
|
||||
let (result_tx, result_rx) = flume::bounded(1);
|
||||
let span = debug_span!("prov-chan");
|
||||
provider_tx
|
||||
.send_async(ProviderMessage::GetLibraryNode {
|
||||
self.provider_tx
|
||||
.send_async(ProviderMessage::new(ProviderCommand::GetLibraryNode {
|
||||
uuid,
|
||||
result_tx,
|
||||
span,
|
||||
})
|
||||
.in_current_span()
|
||||
}))
|
||||
.await
|
||||
.map_err(|_| Status::internal("Failed to send request via channel"))?;
|
||||
let result = result_rx
|
||||
.recv_async()
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{:?}", e);
|
||||
Status::internal("Failed to receive response from provider channel")
|
||||
.map_err(|err| {
|
||||
error!("provider channel closed: {err}");
|
||||
Status::internal("provider unavailable")
|
||||
})?;
|
||||
let result = result_rx.recv_async().await.map_err(|err| {
|
||||
error!("no reply from provider: {err}");
|
||||
Status::internal("provider did not reply")
|
||||
})?;
|
||||
match result {
|
||||
Ok(node) => Ok(Response::new(GetLibraryNodeResponse { node: Some(node) })),
|
||||
Err(err) => {
|
||||
error!("{:?}", err);
|
||||
error!("get_library_node failed: {err}");
|
||||
Err(Status::internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
|
@ -107,204 +102,137 @@ impl CrabidyService for RpcService {
|
|||
#[instrument(skip(self, request), fields(uuids))]
|
||||
async fn queue(
|
||||
&self,
|
||||
request: tonic::Request<QueueRequest>,
|
||||
) -> std::result::Result<tonic::Response<QueueResponse>, tonic::Status> {
|
||||
let uuids = request.into_inner().uuids.clone();
|
||||
Span::current().record("uuids", format!("{:?}", uuids));
|
||||
debug!("Received queue request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
playback_tx
|
||||
.send_async(PlaybackMessage::Queue { uuids, span })
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|_| Status::internal("Failed to send request via channel"))?;
|
||||
|
||||
let reply = QueueResponse {};
|
||||
Ok(Response::new(reply))
|
||||
request: Request<QueueRequest>,
|
||||
) -> Result<Response<QueueResponse>, Status> {
|
||||
let uuids = request.into_inner().uuids;
|
||||
tracing::Span::current().record("uuids", format!("{uuids:?}"));
|
||||
debug!("received queue request");
|
||||
self.send_playback(PlaybackCommand::Queue { uuids }).await?;
|
||||
Ok(Response::new(QueueResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, request), fields(uuids))]
|
||||
async fn replace(
|
||||
&self,
|
||||
request: tonic::Request<ReplaceRequest>,
|
||||
) -> std::result::Result<tonic::Response<ReplaceResponse>, tonic::Status> {
|
||||
let uuids = request.into_inner().uuids.clone();
|
||||
Span::current().record("uuids", format!("{:?}", uuids));
|
||||
debug!("Received replace request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
playback_tx
|
||||
.send_async(PlaybackMessage::Replace { uuids, span })
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|_| Status::internal("Failed to send request via channel"))?;
|
||||
let reply = ReplaceResponse {};
|
||||
Ok(Response::new(reply))
|
||||
request: Request<ReplaceRequest>,
|
||||
) -> Result<Response<ReplaceResponse>, Status> {
|
||||
let uuids = request.into_inner().uuids;
|
||||
tracing::Span::current().record("uuids", format!("{uuids:?}"));
|
||||
debug!("received replace request");
|
||||
self.send_playback(PlaybackCommand::Replace { uuids })
|
||||
.await?;
|
||||
Ok(Response::new(ReplaceResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, request), fields(uuids))]
|
||||
async fn append(
|
||||
&self,
|
||||
request: tonic::Request<AppendRequest>,
|
||||
) -> std::result::Result<tonic::Response<AppendResponse>, tonic::Status> {
|
||||
let uuids = request.into_inner().uuids.clone();
|
||||
Span::current().record("uuids", format!("{:?}", uuids));
|
||||
debug!("Received append request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
playback_tx
|
||||
.send_async(PlaybackMessage::Append { uuids, span })
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|_| Status::internal("Failed to send request via channel"))?;
|
||||
let reply = AppendResponse {};
|
||||
Ok(Response::new(reply))
|
||||
request: Request<AppendRequest>,
|
||||
) -> Result<Response<AppendResponse>, Status> {
|
||||
let uuids = request.into_inner().uuids;
|
||||
tracing::Span::current().record("uuids", format!("{uuids:?}"));
|
||||
debug!("received append request");
|
||||
self.send_playback(PlaybackCommand::Append { uuids })
|
||||
.await?;
|
||||
Ok(Response::new(AppendResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, request), fields(positions))]
|
||||
async fn remove(
|
||||
&self,
|
||||
request: tonic::Request<RemoveRequest>,
|
||||
) -> std::result::Result<tonic::Response<RemoveResponse>, tonic::Status> {
|
||||
request: Request<RemoveRequest>,
|
||||
) -> Result<Response<RemoveResponse>, Status> {
|
||||
let positions = request.into_inner().positions;
|
||||
Span::current().record("positions", format!("{:?}", positions));
|
||||
debug!("Received remove request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
playback_tx
|
||||
.send_async(PlaybackMessage::Remove { positions, span })
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|_| Status::internal("Failed to send request via channel"))?;
|
||||
let reply = RemoveResponse {};
|
||||
Ok(Response::new(reply))
|
||||
tracing::Span::current().record("positions", format!("{positions:?}"));
|
||||
debug!("received remove request");
|
||||
self.send_playback(PlaybackCommand::Remove { positions })
|
||||
.await?;
|
||||
Ok(Response::new(RemoveResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, request), fields(uuids, position))]
|
||||
async fn insert(
|
||||
&self,
|
||||
request: tonic::Request<InsertRequest>,
|
||||
) -> std::result::Result<tonic::Response<InsertResponse>, tonic::Status> {
|
||||
request: Request<InsertRequest>,
|
||||
) -> Result<Response<InsertResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
let uuids = req.uuids.clone();
|
||||
let position = req.position;
|
||||
Span::current().record("uuids", format!("{:?}", uuids));
|
||||
Span::current().record("position", position);
|
||||
debug!("Received insert request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
playback_tx
|
||||
.send_async(PlaybackMessage::Insert {
|
||||
position: req.position,
|
||||
uuids,
|
||||
span,
|
||||
})
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|_| Status::internal("Failed to send request via channel"))?;
|
||||
let reply = InsertResponse {};
|
||||
Ok(Response::new(reply))
|
||||
tracing::Span::current().record("uuids", format!("{:?}", req.uuids));
|
||||
tracing::Span::current().record("position", req.position);
|
||||
debug!("received insert request");
|
||||
self.send_playback(PlaybackCommand::Insert {
|
||||
position: req.position,
|
||||
uuids: req.uuids,
|
||||
})
|
||||
.await?;
|
||||
Ok(Response::new(InsertResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, request), fields(exclude_current))]
|
||||
async fn clear_queue(
|
||||
&self,
|
||||
request: tonic::Request<ClearQueueRequest>,
|
||||
) -> std::result::Result<tonic::Response<ClearQueueResponse>, tonic::Status> {
|
||||
request: Request<ClearQueueRequest>,
|
||||
) -> Result<Response<ClearQueueResponse>, Status> {
|
||||
let exclude_current = request.into_inner().exclude_current;
|
||||
Span::current().record("exclude_current", exclude_current);
|
||||
debug!("Received clear_queue request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
playback_tx
|
||||
.send_async(PlaybackMessage::Clear {
|
||||
exclude_current,
|
||||
span,
|
||||
})
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|_| Status::internal("Failed to send request via channel"))?;
|
||||
let reply = ClearQueueResponse {};
|
||||
Ok(Response::new(reply))
|
||||
tracing::Span::current().record("exclude_current", exclude_current);
|
||||
debug!("received clear_queue request");
|
||||
self.send_playback(PlaybackCommand::Clear { exclude_current })
|
||||
.await?;
|
||||
Ok(Response::new(ClearQueueResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, request), fields(position))]
|
||||
async fn set_current(
|
||||
&self,
|
||||
request: tonic::Request<SetCurrentRequest>,
|
||||
) -> std::result::Result<tonic::Response<SetCurrentResponse>, tonic::Status> {
|
||||
request: Request<SetCurrentRequest>,
|
||||
) -> Result<Response<SetCurrentResponse>, Status> {
|
||||
let position = request.into_inner().position;
|
||||
Span::current().record("position", position);
|
||||
debug!("Received set_current request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
playback_tx
|
||||
.send_async(PlaybackMessage::SetCurrent { position, span })
|
||||
.in_current_span()
|
||||
.await
|
||||
.map_err(|_| Status::internal("Failed to send request via channel"))?;
|
||||
let reply = SetCurrentResponse {};
|
||||
Ok(Response::new(reply))
|
||||
tracing::Span::current().record("position", position);
|
||||
debug!("received set_current request");
|
||||
self.send_playback(PlaybackCommand::SetCurrent { position })
|
||||
.await?;
|
||||
Ok(Response::new(SetCurrentResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn toggle_shuffle(
|
||||
&self,
|
||||
_request: tonic::Request<ToggleShuffleRequest>,
|
||||
) -> std::result::Result<tonic::Response<ToggleShuffleResponse>, tonic::Status> {
|
||||
debug!("Received toggle_shuffle request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::ToggleShuffle { span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to send request via channel: {}", err);
|
||||
}
|
||||
let reply = ToggleShuffleResponse {};
|
||||
Ok(Response::new(reply))
|
||||
_request: Request<ToggleShuffleRequest>,
|
||||
) -> Result<Response<ToggleShuffleResponse>, Status> {
|
||||
debug!("received toggle_shuffle request");
|
||||
self.send_playback(PlaybackCommand::ToggleShuffle).await?;
|
||||
Ok(Response::new(ToggleShuffleResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn toggle_repeat(
|
||||
&self,
|
||||
_request: tonic::Request<ToggleRepeatRequest>,
|
||||
) -> std::result::Result<tonic::Response<ToggleRepeatResponse>, tonic::Status> {
|
||||
debug!("Received toggle_repeat request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::ToggleRepeat { span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to send request via channel: {}", err);
|
||||
}
|
||||
let reply = ToggleRepeatResponse {};
|
||||
Ok(Response::new(reply))
|
||||
_request: Request<ToggleRepeatRequest>,
|
||||
) -> Result<Response<ToggleRepeatResponse>, Status> {
|
||||
debug!("received toggle_repeat request");
|
||||
self.send_playback(PlaybackCommand::ToggleRepeat).await?;
|
||||
Ok(Response::new(ToggleRepeatResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn get_update_stream(
|
||||
&self,
|
||||
_request: tonic::Request<GetUpdateStreamRequest>,
|
||||
) -> std::result::Result<tonic::Response<Self::GetUpdateStreamStream>, tonic::Status> {
|
||||
debug!("Received get_update_stream request");
|
||||
_request: Request<GetUpdateStreamRequest>,
|
||||
) -> Result<Response<Self::GetUpdateStreamStream>, Status> {
|
||||
debug!("received get_update_stream request, subscribing client");
|
||||
let update_rx = self.update_tx.subscribe();
|
||||
let update_stream = tokio_stream::wrappers::BroadcastStream::new(update_rx);
|
||||
|
||||
let output_stream = update_stream.into_stream().map(|update_result| {
|
||||
trace!("Got update: {:?}", update_result);
|
||||
let output_stream = update_stream.map(|update_result| {
|
||||
trace!(?update_result, "forwarding update");
|
||||
match update_result {
|
||||
Ok(update) => Ok(GetUpdateStreamResponse {
|
||||
update: Some(update),
|
||||
}),
|
||||
Err(_) => Err(tonic::Status::new(
|
||||
tonic::Code::Unknown,
|
||||
"Internal channel error",
|
||||
)),
|
||||
Err(err) => {
|
||||
// The client lagged too far behind the broadcast channel.
|
||||
error!("update stream lagged: {err}");
|
||||
Err(Status::data_loss("update stream lagged"))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -314,146 +242,73 @@ impl CrabidyService for RpcService {
|
|||
#[instrument(skip(self, _request))]
|
||||
async fn save_queue(
|
||||
&self,
|
||||
_request: tonic::Request<SaveQueueRequest>,
|
||||
) -> std::result::Result<tonic::Response<SaveQueueResponse>, tonic::Status> {
|
||||
debug!("Received save_queue request");
|
||||
let reply = SaveQueueResponse {};
|
||||
Ok(Response::new(reply))
|
||||
_request: Request<SaveQueueRequest>,
|
||||
) -> Result<Response<SaveQueueResponse>, Status> {
|
||||
debug!("received save_queue request (not implemented)");
|
||||
Ok(Response::new(SaveQueueResponse {}))
|
||||
}
|
||||
|
||||
/// Playback
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn toggle_play(
|
||||
&self,
|
||||
_request: tonic::Request<TogglePlayRequest>,
|
||||
) -> std::result::Result<tonic::Response<TogglePlayResponse>, tonic::Status> {
|
||||
debug!("Received toggle_play request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::TogglePlay { span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to send request via channel: {}", err);
|
||||
}
|
||||
let reply = TogglePlayResponse {};
|
||||
Ok(Response::new(reply))
|
||||
_request: Request<TogglePlayRequest>,
|
||||
) -> Result<Response<TogglePlayResponse>, Status> {
|
||||
debug!("received toggle_play request");
|
||||
self.send_playback(PlaybackCommand::TogglePlay).await?;
|
||||
Ok(Response::new(TogglePlayResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn stop(
|
||||
&self,
|
||||
_request: tonic::Request<StopRequest>,
|
||||
) -> std::result::Result<tonic::Response<StopResponse>, tonic::Status> {
|
||||
debug!("Received stop request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::Stop { span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to send request via channel: {}", err);
|
||||
}
|
||||
let reply = StopResponse {};
|
||||
Ok(Response::new(reply))
|
||||
async fn stop(&self, _request: Request<StopRequest>) -> Result<Response<StopResponse>, Status> {
|
||||
debug!("received stop request");
|
||||
self.send_playback(PlaybackCommand::Stop).await?;
|
||||
Ok(Response::new(StopResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, request), fields(delta))]
|
||||
async fn change_volume(
|
||||
&self,
|
||||
request: tonic::Request<ChangeVolumeRequest>,
|
||||
) -> std::result::Result<tonic::Response<ChangeVolumeResponse>, tonic::Status> {
|
||||
request: Request<ChangeVolumeRequest>,
|
||||
) -> Result<Response<ChangeVolumeResponse>, Status> {
|
||||
let delta = request.into_inner().delta;
|
||||
Span::current().record("delta", delta);
|
||||
debug!("Received change_volume request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::ChangeVolume { delta, span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to send request via channel: {}", err);
|
||||
}
|
||||
let reply = ChangeVolumeResponse {};
|
||||
Ok(Response::new(reply))
|
||||
tracing::Span::current().record("delta", delta);
|
||||
debug!("received change_volume request");
|
||||
self.send_playback(PlaybackCommand::ChangeVolume { delta })
|
||||
.await?;
|
||||
Ok(Response::new(ChangeVolumeResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn toggle_mute(
|
||||
&self,
|
||||
_request: tonic::Request<ToggleMuteRequest>,
|
||||
) -> std::result::Result<tonic::Response<ToggleMuteResponse>, tonic::Status> {
|
||||
debug!("Received toggle_mute request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::ToggleMute { span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to send request via channel: {}", err);
|
||||
}
|
||||
let reply = ToggleMuteResponse {};
|
||||
Ok(Response::new(reply))
|
||||
_request: Request<ToggleMuteRequest>,
|
||||
) -> Result<Response<ToggleMuteResponse>, Status> {
|
||||
debug!("received toggle_mute request");
|
||||
self.send_playback(PlaybackCommand::ToggleMute).await?;
|
||||
Ok(Response::new(ToggleMuteResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn next(
|
||||
&self,
|
||||
_request: tonic::Request<NextRequest>,
|
||||
) -> std::result::Result<tonic::Response<NextResponse>, tonic::Status> {
|
||||
debug!("Received next request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::Next { span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to send request via channel: {}", err);
|
||||
}
|
||||
let reply = NextResponse {};
|
||||
Ok(Response::new(reply))
|
||||
async fn next(&self, _request: Request<NextRequest>) -> Result<Response<NextResponse>, Status> {
|
||||
debug!("received next request");
|
||||
self.send_playback(PlaybackCommand::Next).await?;
|
||||
Ok(Response::new(NextResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn prev(
|
||||
&self,
|
||||
_request: tonic::Request<PrevRequest>,
|
||||
) -> std::result::Result<tonic::Response<PrevResponse>, tonic::Status> {
|
||||
debug!("Received prev request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::Prev { span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to send request via channel: {}", err);
|
||||
}
|
||||
let reply = PrevResponse {};
|
||||
Ok(Response::new(reply))
|
||||
async fn prev(&self, _request: Request<PrevRequest>) -> Result<Response<PrevResponse>, Status> {
|
||||
debug!("received prev request");
|
||||
self.send_playback(PlaybackCommand::Prev).await?;
|
||||
Ok(Response::new(PrevResponse {}))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _request))]
|
||||
async fn restart_track(
|
||||
&self,
|
||||
_request: tonic::Request<RestartTrackRequest>,
|
||||
) -> std::result::Result<tonic::Response<RestartTrackResponse>, tonic::Status> {
|
||||
debug!("Received restart_track request");
|
||||
let playback_tx = self.playback_tx.clone();
|
||||
let span = debug_span!("play-chan");
|
||||
if let Err(err) = playback_tx
|
||||
.send_async(PlaybackMessage::RestartTrack { span })
|
||||
.in_current_span()
|
||||
.await
|
||||
{
|
||||
error!("Failed to send request via channel: {}", err);
|
||||
}
|
||||
let reply = RestartTrackResponse {};
|
||||
Ok(Response::new(reply))
|
||||
_request: Request<RestartTrackRequest>,
|
||||
) -> Result<Response<RestartTrackResponse>, Status> {
|
||||
debug!("received restart_track request");
|
||||
self.send_playback(PlaybackCommand::RestartTrack).await?;
|
||||
Ok(Response::new(RestartTrackResponse {}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use reqwest::Client as HttpClient;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::time::{sleep, Duration, Instant};
|
||||
use tracing::{debug, error, info, instrument};
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
pub mod config;
|
||||
pub mod models;
|
||||
use async_trait::async_trait;
|
||||
|
|
@ -22,12 +22,8 @@ impl crabidy_core::ProviderClient for Client {
|
|||
let settings: config::Settings = if let Ok(settings) = toml::from_str(raw_toml_settings) {
|
||||
settings
|
||||
} else {
|
||||
let settings = config::Settings::default();
|
||||
println!(
|
||||
"could not parse toml settings: {:#?} using default settings instead: {:#?}",
|
||||
raw_toml_settings, settings
|
||||
);
|
||||
settings
|
||||
warn!("could not parse toml settings, using defaults");
|
||||
config::Settings::default()
|
||||
};
|
||||
|
||||
let mut client = Self::new(settings)?;
|
||||
|
|
@ -48,16 +44,21 @@ impl crabidy_core::ProviderClient for Client {
|
|||
&self,
|
||||
track_uuid: &str,
|
||||
) -> Result<Vec<String>, crabidy_core::ProviderError> {
|
||||
debug!("get_urls_for_track {}", track_uuid);
|
||||
let (_, track_uuid, _) = split_uuid(track_uuid);
|
||||
let Ok(playback) = self.get_track_playback(&track_uuid).await else {
|
||||
return Err(crabidy_core::ProviderError::FetchError);
|
||||
};
|
||||
debug!("playback {:?}", playback);
|
||||
let Ok(manifest) = playback.get_manifest() else {
|
||||
return Err(crabidy_core::ProviderError::FetchError);
|
||||
};
|
||||
debug!("manifest {:?}", manifest);
|
||||
let playback = self.get_track_playback(&track_uuid).await.map_err(|err| {
|
||||
warn!(track = track_uuid, "failed to fetch playback info: {err}");
|
||||
crabidy_core::ProviderError::FetchError
|
||||
})?;
|
||||
trace!(?playback, "got playback info");
|
||||
let manifest = playback.get_manifest().map_err(|err| {
|
||||
warn!(track = track_uuid, "failed to decode manifest: {err}");
|
||||
crabidy_core::ProviderError::FetchError
|
||||
})?;
|
||||
debug!(
|
||||
track = track_uuid,
|
||||
urls = manifest.urls.len(),
|
||||
"resolved stream urls"
|
||||
);
|
||||
Ok(manifest.urls)
|
||||
}
|
||||
|
||||
|
|
@ -66,10 +67,10 @@ impl crabidy_core::ProviderClient for Client {
|
|||
&self,
|
||||
track_uuid: &str,
|
||||
) -> Result<crabidy_core::proto::crabidy::Track, crabidy_core::ProviderError> {
|
||||
debug!("get_metadata_for_track {}", track_uuid);
|
||||
let Ok(track) = self.get_track(track_uuid).await else {
|
||||
return Err(crabidy_core::ProviderError::FetchError);
|
||||
};
|
||||
let track = self.get_track(track_uuid).await.map_err(|err| {
|
||||
warn!(track = track_uuid, "failed to fetch track metadata: {err}");
|
||||
crabidy_core::ProviderError::FetchError
|
||||
})?;
|
||||
Ok(track.into())
|
||||
}
|
||||
|
||||
|
|
@ -107,9 +108,8 @@ impl crabidy_core::ProviderClient for Client {
|
|||
let Some(user_id) = self.settings.login.user_id.clone() else {
|
||||
return Err(crabidy_core::ProviderError::UnknownUser);
|
||||
};
|
||||
debug!("get_lib_node in tidaldy{}", uuid);
|
||||
let (_kind, module, uuid) = split_uuid(uuid);
|
||||
error!("module:{},uuid: {}", module, uuid);
|
||||
debug!(module, uuid, "resolving library node");
|
||||
let node = match module.as_str() {
|
||||
"userplaylists" => {
|
||||
let mut node = crabidy_core::proto::crabidy::LibraryNode {
|
||||
|
|
@ -166,7 +166,6 @@ impl crabidy_core::ProviderClient for Client {
|
|||
node
|
||||
}
|
||||
"artist" => {
|
||||
info!("artist");
|
||||
let mut node: crabidy_core::proto::crabidy::LibraryNode =
|
||||
self.get_artist(&uuid).await?.into();
|
||||
let children: Vec<crabidy_core::proto::crabidy::LibraryNodeChild> = self
|
||||
|
|
@ -371,7 +370,7 @@ impl Client {
|
|||
error!("{:?}", e);
|
||||
e
|
||||
})?;
|
||||
println!("{:?}", response);
|
||||
debug!(?response, "explorer response");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -504,7 +503,13 @@ impl Client {
|
|||
pub async fn login_web(&mut self) -> Result<(), ClientError> {
|
||||
let code_response = self.get_device_code().await?;
|
||||
let now = Instant::now();
|
||||
// The verification link must reach the user even without a log
|
||||
// subscriber configured.
|
||||
println!("https://{}", code_response.verification_uri_complete);
|
||||
info!(
|
||||
"waiting for device login at https://{}",
|
||||
code_response.verification_uri_complete
|
||||
);
|
||||
while now.elapsed().as_secs() <= code_response.expires_in {
|
||||
let login = self.check_auth_status(&code_response.device_code).await;
|
||||
if login.is_err() {
|
||||
|
|
@ -520,9 +525,10 @@ impl Client {
|
|||
self.settings.login.expires_after = Some(login_results.expires_in + timestamp);
|
||||
self.settings.login.user_id = Some(login_results.user.user_id.to_string());
|
||||
self.settings.login.country_code = Some(login_results.user.country_code);
|
||||
info!("device login succeeded");
|
||||
return Ok(());
|
||||
}
|
||||
println!("login attempt expired");
|
||||
warn!("device login attempt expired");
|
||||
Err(ClientError::ConnectionError)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue