plexus-mono 0.2.0

Monochrome music API Plexus RPC activation — search, metadata, lyrics, and recommendations
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
//! PlayerHub — Plexus RPC activation for stateful playback
//!
//! Owns the audio playback engine, queue, and playlist child router.
//! Requires speakers. Registered as a hub activation under `monochrome`.

use async_stream::stream;
use async_trait::async_trait;
use futures::Stream;
use std::sync::Arc;

use plexus_core::plexus::schema::ChildSummary;
use plexus_core::plexus::{ChildRouter, PlexusError, PlexusStream};
use plexus_core::Activation;

use crate::client::MonoClient;
use crate::player::Player;
use crate::playlist::PlaylistHub;
use crate::types::MonoEvent;

/// Stateful playback engine activation with queue and playlist management.
#[derive(Clone)]
pub struct PlayerHub {
    player: Arc<Player>,
    playlist: PlaylistHub,
}

impl PlayerHub {
    /// Create a new PlayerHub from a shared MonoClient.
    pub async fn new(client: Arc<MonoClient>) -> Self {
        let player = Player::new(client.clone()).await;
        let playlist = PlaylistHub::new(player.clone(), client);
        Self { player, playlist }
    }

    /// Return child activation summaries for schema discovery
    pub fn plugin_children(&self) -> Vec<ChildSummary> {
        vec![ChildSummary {
            namespace: "playlist".into(),
            description: "Persistent playlist management".into(),
            hash: String::new(),
        }]
    }
}

#[async_trait]
impl ChildRouter for PlayerHub {
    fn router_namespace(&self) -> &str {
        "player"
    }

    async fn router_call(
        &self,
        method: &str,
        params: serde_json::Value,
    ) -> Result<PlexusStream, PlexusError> {
        self.call(method, params).await
    }

    async fn get_child(&self, name: &str) -> Option<Box<dyn ChildRouter>> {
        match name {
            "playlist" => Some(Box::new(self.playlist.clone())),
            _ => None,
        }
    }
}

#[plexus_macros::hub_methods(
    namespace = "player",
    version = "0.2.0",
    hub,
    description = "Playback engine — play, queue, and control audio with playlist management",
    crate_path = "plexus_core"
)]
impl PlayerHub {
    /// Play a track immediately (stops current playback)
    #[plexus_macros::hub_method(
        description = "Play a track through speakers. Stops any current playback.",
        params(
            id = "Tidal track ID",
            quality = "Quality: LOSSLESS (default), HI_RES_LOSSLESS, HIGH, LOW"
        )
    )]
    pub async fn play(
        &self,
        id: u64,
        quality: Option<String>,
    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        let quality = quality.unwrap_or_else(|| "LOSSLESS".to_string());
        stream! {
            match player.play_track(id, &quality).await {
                Ok(()) => yield MonoEvent::PlayerAck {
                    action: "play".to_string(),
                    message: format!("playing track {id}"),
                },
                Err(e) => yield MonoEvent::Error { message: e },
            }
        }
    }

    /// Pause playback
    #[plexus_macros::hub_method(
        description = "Pause the current playback"
    )]
    pub async fn pause(&self) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            player.pause().await;
            yield MonoEvent::PlayerAck {
                action: "pause".to_string(),
                message: "playback paused".to_string(),
            };
        }
    }

    /// Resume playback
    #[plexus_macros::hub_method(
        description = "Resume paused playback"
    )]
    pub async fn resume(&self) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            player.resume().await;
            yield MonoEvent::PlayerAck {
                action: "resume".to_string(),
                message: "playback resumed".to_string(),
            };
        }
    }

    /// Stop playback
    #[plexus_macros::hub_method(
        description = "Stop playback and clear current track"
    )]
    pub async fn stop(&self) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            player.stop().await;
            yield MonoEvent::PlayerAck {
                action: "stop".to_string(),
                message: "playback stopped".to_string(),
            };
        }
    }

    /// Skip to next track in queue
    #[plexus_macros::hub_method(
        description = "Skip to the next track in the queue"
    )]
    pub async fn next(&self) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            match player.next().await {
                Ok(()) => yield MonoEvent::PlayerAck {
                    action: "next".to_string(),
                    message: "skipped to next track".to_string(),
                },
                Err(e) => yield MonoEvent::Error { message: e },
            }
        }
    }

    /// Go to previous track
    #[plexus_macros::hub_method(
        description = "Go back to the previous track"
    )]
    pub async fn previous(&self) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            match player.previous().await {
                Ok(()) => yield MonoEvent::PlayerAck {
                    action: "previous".to_string(),
                    message: "went to previous track".to_string(),
                },
                Err(e) => yield MonoEvent::Error { message: e },
            }
        }
    }

    /// Set volume level
    #[plexus_macros::hub_method(
        description = "Set playback volume",
        params(level = "Volume level from 0.0 (mute) to 1.0 (full)")
    )]
    pub async fn volume(
        &self,
        level: f32,
    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            player.set_volume(level).await;
            yield MonoEvent::PlayerAck {
                action: "volume".to_string(),
                message: format!("volume set to {:.0}%", level * 100.0),
            };
        }
    }

    /// Set pre-amp gain level
    #[plexus_macros::hub_method(
        description = "Set pre-amp gain. Values above 1.0 boost the signal (max 4.0). Effective volume = preamp × volume.",
        params(level = "Gain level from 0.0 (silent) to 4.0 (4× boost)")
    )]
    pub async fn preamp(
        &self,
        level: f32,
    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            player.set_preamp(level).await;
            yield MonoEvent::PlayerAck {
                action: "preamp".to_string(),
                message: format!("preamp set to {:.1}×", level.clamp(0.0, 4.0)),
            };
        }
    }

    /// Add an entire album to the playback queue
    #[plexus_macros::hub_method(
        streaming,
        description = "Add all tracks from an album to the queue. Auto-starts if nothing is playing.",
        params(
            id = "Tidal album ID",
            quality = "Quality: LOSSLESS (default), HI_RES_LOSSLESS, HIGH, LOW"
        )
    )]
    pub async fn queue_album(
        &self,
        id: u64,
        quality: Option<String>,
    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        let quality = quality.unwrap_or_else(|| "LOSSLESS".to_string());
        stream! {
            match player.queue_album(id, &quality).await {
                Ok(tracks) => {
                    let count = tracks.len();
                    yield MonoEvent::PlayerAck {
                        action: "queue_album".to_string(),
                        message: format!("{count} tracks queued"),
                    };
                    let current_index = Some(0usize);
                    yield MonoEvent::Queue {
                        tracks,
                        current_index,
                    };
                }
                Err(e) => yield MonoEvent::Error { message: e },
            }
        }
    }

    /// Add a track to the playback queue
    #[plexus_macros::hub_method(
        description = "Add a track to the end of the playback queue. Auto-starts if nothing is playing.",
        params(
            id = "Tidal track ID",
            quality = "Quality: LOSSLESS (default), HI_RES_LOSSLESS, HIGH, LOW"
        )
    )]
    pub async fn queue_add(
        &self,
        id: u64,
        quality: Option<String>,
    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        let quality = quality.unwrap_or_else(|| "LOSSLESS".to_string());
        stream! {
            match player.queue_add(id, &quality).await {
                Ok(()) => yield MonoEvent::PlayerAck {
                    action: "queue_add".to_string(),
                    message: format!("track {id} added to queue"),
                },
                Err(e) => yield MonoEvent::Error { message: e },
            }
        }
    }

    /// Add multiple tracks to the queue at once
    #[plexus_macros::hub_method(
        streaming,
        description = "Add multiple tracks by ID in one call. Resolves metadata in parallel. Auto-starts if nothing is playing.",
        params(
            ids = "List of Tidal track IDs",
            quality = "Quality: LOSSLESS (default), HI_RES_LOSSLESS, HIGH, LOW"
        )
    )]
    pub async fn queue_batch(
        &self,
        ids: Vec<u64>,
        quality: Option<String>,
    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        let quality = quality.unwrap_or_else(|| "LOSSLESS".to_string());
        stream! {
            match player.queue_batch(&ids, &quality).await {
                Ok(tracks) => {
                    let count = tracks.len();
                    yield MonoEvent::PlayerAck {
                        action: "queue_batch".to_string(),
                        message: format!("{count} tracks queued"),
                    };
                    yield MonoEvent::Queue {
                        tracks,
                        current_index: Some(0),
                    };
                }
                Err(e) => yield MonoEvent::Error { message: e },
            }
        }
    }

    /// Clear the playback queue
    #[plexus_macros::hub_method(
        description = "Clear all tracks from the queue (does not stop current track)"
    )]
    pub async fn queue_clear(&self) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            player.queue_clear().await;
            yield MonoEvent::PlayerAck {
                action: "queue_clear".to_string(),
                message: "queue cleared".to_string(),
            };
        }
    }

    /// List queue contents
    #[plexus_macros::hub_method(
        description = "Get the current queue contents including the now-playing track"
    )]
    pub async fn queue_get(&self) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            let (current, upcoming) = player.queue_get().await;
            let current_index = if current.is_some() { Some(0usize) } else { None };
            let mut tracks = Vec::new();
            if let Some(c) = current {
                tracks.push(c);
            }
            tracks.extend(upcoming);
            yield MonoEvent::Queue {
                tracks,
                current_index,
            };
        }
    }

    /// Reorder tracks in the queue
    #[plexus_macros::hub_method(
        description = "Move a track within the queue",
        params(
            from = "Source index in the queue (0-based)",
            to = "Destination index in the queue (0-based)"
        )
    )]
    pub async fn queue_reorder(
        &self,
        from: u32,
        to: u32,
    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let player = self.player.clone();
        stream! {
            match player.queue_reorder(from as usize, to as usize).await {
                Ok(()) => yield MonoEvent::PlayerAck {
                    action: "queue_reorder".to_string(),
                    message: format!("moved track from position {from} to {to}"),
                },
                Err(e) => yield MonoEvent::Error { message: e },
            }
        }
    }

    /// Get current playback status (single snapshot, returns immediately)
    #[plexus_macros::hub_method(
        description = "Get current playback status — track, position, volume, queue length"
    )]
    pub async fn status(&self) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let rx = self.player.subscribe_now_playing();
        let np = rx.borrow().clone();
        stream! {
            yield MonoEvent::NowPlaying {
                track_id: np.track_id,
                title: np.title,
                artist: np.artist,
                album: np.album,
                status: np.status,
                position_secs: np.position_secs,
                duration_secs: np.duration_secs,
                volume: np.volume,
                preamp: np.preamp,
                queue_length: np.queue_length,
                url: np.url,
            };
        }
    }

    /// Stream now-playing updates (~1s while playing)
    #[plexus_macros::hub_method(
        streaming,
        description = "Stream real-time playback position and status updates (~1s interval while playing)"
    )]
    pub async fn now_playing(&self) -> impl Stream<Item = MonoEvent> + Send + 'static {
        let mut rx = self.player.subscribe_now_playing();
        stream! {
            // Emit current state immediately
            {
                let np = rx.borrow().clone();
                yield MonoEvent::NowPlaying {
                    track_id: np.track_id,
                    title: np.title,
                    artist: np.artist,
                    album: np.album,
                    status: np.status,
                    position_secs: np.position_secs,
                    duration_secs: np.duration_secs,
                    volume: np.volume,
                    preamp: np.preamp,
                    queue_length: np.queue_length,
                    url: np.url,
                };
            }
            // Then stream updates
            while rx.changed().await.is_ok() {
                let np = rx.borrow().clone();
                yield MonoEvent::NowPlaying {
                    track_id: np.track_id,
                    title: np.title,
                    artist: np.artist,
                    album: np.album,
                    status: np.status,
                    position_secs: np.position_secs,
                    duration_secs: np.duration_secs,
                    volume: np.volume,
                    preamp: np.preamp,
                    queue_length: np.queue_length,
                    url: np.url,
                };
            }
        }
    }
}