a Jellyfin & Subsonic client for the terminal — powered by mpv, Chromecast and UPnP MediaRenderer
mpv chromecast mpris navidrome jellyfin upnp tui
16

Configure Feed

Select the types of activity you want to include in your feed.

Fix MPRIS build: zbus interface futures must be Sync

async_trait's boxed renderer futures are Send but not Sync, while
mpris-server's trait_variant::make(Send + Sync) bound requires interface
futures to be both — every method that awaited a renderer call directly
failed to compile on Linux. Route renderer calls through tokio::spawn:
the JoinHandle is Sync, so the interface future never holds the boxed
future. The helper is deliberately not an async fn — async fn arguments
live in the generated future's state, which would re-poison Sync.

Tsiry Sandratraina (Jul 8, 2026, 2:33 PM +0300) 03a5b128 14dc4fe4

+85 -35
+85 -35
crates/fin-mpris/src/lib.rs
··· 19 19 target_os = "openbsd" 20 20 ))] 21 21 22 + use std::future::Future; 22 23 use std::sync::Arc; 23 24 use std::time::{Duration, Instant}; 24 25 ··· 60 61 fn state(&self) -> PlaybackState { 61 62 self.renderer().state() 62 63 } 64 + } 65 + 66 + /// `Renderer` methods come from `async_trait`, whose boxed futures are 67 + /// `Send` but not `Sync` — while zbus (via `trait_variant::make(Send + 68 + /// Sync)`) requires interface futures to be both. Hop through a spawned 69 + /// task: `JoinHandle` *is* `Sync`, so the interface future never holds the 70 + /// boxed renderer future directly. 71 + /// 72 + /// Deliberately NOT an `async fn`: an async fn keeps its arguments in the 73 + /// generated future's state, so the non-`Sync` `fut` would poison it before 74 + /// `spawn` even ran. A plain fn consumes `fut` eagerly and the returned 75 + /// future holds only the handle. 76 + fn sync_call<T, F>(fut: F) -> impl Future<Output = Result<T>> 77 + where 78 + T: Send + 'static, 79 + F: Future<Output = Result<T>> + Send + 'static, 80 + { 81 + let task = tokio::spawn(fut); 82 + async move { task.await.context("renderer task panicked")? } 63 83 } 64 84 65 85 fn to_fdo(e: anyhow::Error) -> fdo::Error { ··· 191 211 192 212 impl PlayerInterface for Player { 193 213 async fn next(&self) -> fdo::Result<()> { 194 - self.renderer().next().await.map_err(to_fdo) 214 + let r = self.renderer(); 215 + sync_call(async move { r.next().await }) 216 + .await 217 + .map_err(to_fdo) 195 218 } 196 219 197 220 async fn previous(&self) -> fdo::Result<()> { 198 - self.renderer().previous().await.map_err(to_fdo) 221 + let r = self.renderer(); 222 + sync_call(async move { r.previous().await }) 223 + .await 224 + .map_err(to_fdo) 199 225 } 200 226 201 227 async fn pause(&self) -> fdo::Result<()> { 202 - self.renderer().pause().await.map_err(to_fdo) 228 + let r = self.renderer(); 229 + sync_call(async move { r.pause().await }) 230 + .await 231 + .map_err(to_fdo) 203 232 } 204 233 205 234 async fn play_pause(&self) -> fdo::Result<()> { 206 235 let r = self.renderer(); 207 - match r.state().status { 208 - PlaybackStatus::Playing | PlaybackStatus::Buffering => r.pause().await.map_err(to_fdo), 209 - _ => r.resume().await.map_err(to_fdo), 210 - } 236 + sync_call(async move { 237 + match r.state().status { 238 + PlaybackStatus::Playing | PlaybackStatus::Buffering => r.pause().await, 239 + _ => r.resume().await, 240 + } 241 + }) 242 + .await 243 + .map_err(to_fdo) 211 244 } 212 245 213 246 async fn stop(&self) -> fdo::Result<()> { 214 - self.renderer().stop().await.map_err(to_fdo) 247 + let r = self.renderer(); 248 + sync_call(async move { r.stop().await }) 249 + .await 250 + .map_err(to_fdo) 215 251 } 216 252 217 253 async fn play(&self) -> fdo::Result<()> { 218 254 // `resume` also kicks a restored-but-not-started queue, so it covers 219 255 // both the paused and the freshly-launched case. 220 - self.renderer().resume().await.map_err(to_fdo) 256 + let r = self.renderer(); 257 + sync_call(async move { r.resume().await }) 258 + .await 259 + .map_err(to_fdo) 221 260 } 222 261 223 262 async fn seek(&self, offset: Time) -> fdo::Result<()> { 224 263 let r = self.renderer(); 225 - let state = r.state(); 226 - let target = state.position_secs + offset.as_micros() as f64 / 1_000_000.0; 227 - // Spec: seeking past the end of the track acts like Next. 228 - if state.duration_secs > 0.0 && target > state.duration_secs { 229 - return r.next().await.map_err(to_fdo); 230 - } 231 - r.seek(target.max(0.0)).await.map_err(to_fdo) 264 + sync_call(async move { 265 + let state = r.state(); 266 + let target = state.position_secs + offset.as_micros() as f64 / 1_000_000.0; 267 + // Spec: seeking past the end of the track acts like Next. 268 + if state.duration_secs > 0.0 && target > state.duration_secs { 269 + return r.next().await; 270 + } 271 + r.seek(target.max(0.0)).await 272 + }) 273 + .await 274 + .map_err(to_fdo) 232 275 } 233 276 234 277 async fn set_position(&self, track: TrackId, position: Time) -> fdo::Result<()> { 235 278 let r = self.renderer(); 236 - let state = r.state(); 237 - // Spec: a stale trackid means the client raced a track change — 238 - // silently ignore rather than seeking the wrong song. 239 - let Some(current) = &state.now_playing else { 240 - return Ok(()); 241 - }; 242 - if track != track_id(current) { 243 - return Ok(()); 244 - } 245 - let target = position.as_micros() as f64 / 1_000_000.0; 246 - if target < 0.0 || (state.duration_secs > 0.0 && target > state.duration_secs) { 247 - return Ok(()); 248 - } 249 - r.seek(target).await.map_err(to_fdo) 279 + sync_call(async move { 280 + let state = r.state(); 281 + // Spec: a stale trackid means the client raced a track change — 282 + // silently ignore rather than seeking the wrong song. 283 + let Some(current) = &state.now_playing else { 284 + return Ok(()); 285 + }; 286 + if track != track_id(current) { 287 + return Ok(()); 288 + } 289 + let target = position.as_micros() as f64 / 1_000_000.0; 290 + if target < 0.0 || (state.duration_secs > 0.0 && target > state.duration_secs) { 291 + return Ok(()); 292 + } 293 + r.seek(target).await 294 + }) 295 + .await 296 + .map_err(to_fdo) 250 297 } 251 298 252 299 async fn open_uri(&self, _uri: String) -> fdo::Result<()> { ··· 264 311 } 265 312 266 313 async fn set_loop_status(&self, status: LoopStatus) -> zbus::Result<()> { 267 - self.renderer() 268 - .set_repeat(repeat_mode(status)) 314 + let r = self.renderer(); 315 + sync_call(async move { r.set_repeat(repeat_mode(status)).await }) 269 316 .await 270 317 .map_err(to_zbus) 271 318 } ··· 284 331 } 285 332 286 333 async fn set_shuffle(&self, shuffle: bool) -> zbus::Result<()> { 287 - self.renderer().set_shuffle(shuffle).await.map_err(to_zbus) 334 + let r = self.renderer(); 335 + sync_call(async move { r.set_shuffle(shuffle).await }) 336 + .await 337 + .map_err(to_zbus) 288 338 } 289 339 290 340 async fn metadata(&self) -> fdo::Result<Metadata> { ··· 296 346 } 297 347 298 348 async fn set_volume(&self, volume: Volume) -> zbus::Result<()> { 299 - self.renderer() 300 - .set_volume(volume.clamp(0.0, 1.0) as f32) 349 + let r = self.renderer(); 350 + sync_call(async move { r.set_volume(volume.clamp(0.0, 1.0) as f32).await }) 301 351 .await 302 352 .map_err(to_zbus) 303 353 }