Skip to main content

plexus_mono/
hub.rs

1//! MonoHub — Plexus RPC activation for the Monochrome music API
2//!
3//! Stateless API proxy: track metadata, album listings, artist info,
4//! search, lyrics, recommendations, cover art, stream URLs, and downloads.
5//! No audio hardware, no persistence.
6
7use async_stream::stream;
8use futures::Stream;
9use std::sync::Arc;
10
11use crate::client::MonoClient;
12use crate::types::{MonoEvent, SearchKind};
13
14/// Monochrome music API activation — stateless API proxy.
15#[derive(Clone)]
16pub struct MonoHub {
17    client: Arc<MonoClient>,
18}
19
20impl MonoHub {
21    /// Create a hub targeting the default Monochrome API instance.
22    pub async fn new() -> Self {
23        let client = Arc::new(MonoClient::default_instance());
24        Self { client }
25    }
26
27    /// Create a hub targeting a specific API base URL (no trailing slash).
28    pub async fn with_url(base_url: impl Into<String>) -> Self {
29        let client = Arc::new(MonoClient::new(base_url));
30        Self { client }
31    }
32
33    /// Get a shared reference to the underlying MonoClient.
34    pub fn client(&self) -> Arc<MonoClient> {
35        self.client.clone()
36    }
37}
38
39#[plexus_macros::hub_methods(
40    namespace = "monochrome",
41    version = "0.2.0",
42    description = "Monochrome music API — track metadata, search, lyrics, recommendations, cover art",
43    crate_path = "plexus_core"
44)]
45impl MonoHub {
46    /// Get track metadata by Tidal track ID
47    #[plexus_macros::hub_method(
48        description = "Fetch track metadata (title, artist, album, duration, audio quality)",
49        params(id = "Tidal track ID (integer)")
50    )]
51    pub async fn track(
52        &self,
53        id: u64,
54    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
55        let client = self.client.clone();
56        stream! {
57            match client.track_info(id).await {
58                Ok(event) => yield event,
59                Err(e) => yield MonoEvent::Error { message: e },
60            }
61        }
62    }
63
64    /// Get album metadata and its full track listing by Tidal album ID
65    #[plexus_macros::hub_method(
66        streaming,
67        description = "Fetch album metadata then stream each track. Yields Album followed by AlbumTrack events.",
68        params(id = "Tidal album ID (integer)")
69    )]
70    pub async fn album(
71        &self,
72        id: u64,
73    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
74        let client = self.client.clone();
75        stream! {
76            match client.album(id).await {
77                Ok((album, tracks)) => {
78                    yield album;
79                    for track in tracks {
80                        yield track;
81                    }
82                }
83                Err(e) => yield MonoEvent::Error { message: e },
84            }
85        }
86    }
87
88    /// Get artist metadata by Tidal artist ID
89    #[plexus_macros::hub_method(
90        description = "Fetch artist name and image",
91        params(id = "Tidal artist ID (integer)")
92    )]
93    pub async fn artist(
94        &self,
95        id: u64,
96    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
97        let client = self.client.clone();
98        stream! {
99            match client.artist(id).await {
100                Ok(event) => yield event,
101                Err(e) => yield MonoEvent::Error { message: e },
102            }
103        }
104    }
105
106    /// Search for tracks, albums, or artists
107    #[plexus_macros::hub_method(
108        streaming,
109        description = "Search the Monochrome API. Streams one event per result.",
110        params(
111            query = "Search query string",
112            kind = "What to search: tracks (default), albums, or artists",
113            limit = "Maximum number of results (default 25, max 500)",
114            offset = "Pagination offset (default 0)"
115        )
116    )]
117    pub async fn search(
118        &self,
119        query: String,
120        kind: Option<SearchKind>,
121        limit: Option<u32>,
122        offset: Option<u32>,
123    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
124        let client = self.client.clone();
125        let kind = kind.unwrap_or_default();
126        let limit = limit.unwrap_or(25).min(500);
127        let offset = offset.unwrap_or(0);
128
129        stream! {
130            match client.search(&query, &kind, limit, offset).await {
131                Ok(results) => {
132                    if results.is_empty() {
133                        yield MonoEvent::Error {
134                            message: format!("no results for {:?}", query),
135                        };
136                    } else {
137                        for event in results {
138                            yield event;
139                        }
140                    }
141                }
142                Err(e) => yield MonoEvent::Error { message: e },
143            }
144        }
145    }
146
147    /// Get synchronized lyrics for a track by Tidal track ID
148    #[plexus_macros::hub_method(
149        streaming,
150        description = "Fetch lyrics. Streams one LyricLine per line (with timestamps if available).",
151        params(id = "Tidal track ID (integer)")
152    )]
153    pub async fn lyrics(
154        &self,
155        id: u64,
156    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
157        let client = self.client.clone();
158        stream! {
159            match client.lyrics(id).await {
160                Ok(lines) => {
161                    for line in lines {
162                        yield line;
163                    }
164                }
165                Err(e) => yield MonoEvent::Error { message: e },
166            }
167        }
168    }
169
170    /// Get recommended tracks similar to a given track
171    #[plexus_macros::hub_method(
172        streaming,
173        description = "Fetch track recommendations. Streams Recommendation events.",
174        params(id = "Tidal track ID to base recommendations on")
175    )]
176    pub async fn recommendations(
177        &self,
178        id: u64,
179    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
180        let client = self.client.clone();
181        stream! {
182            match client.recommendations(id).await {
183                Ok(recs) => {
184                    for rec in recs {
185                        yield rec;
186                    }
187                }
188                Err(e) => yield MonoEvent::Error { message: e },
189            }
190        }
191    }
192
193    /// Resolve the direct CDN stream URL for a track
194    #[plexus_macros::hub_method(
195        description = "Resolve the pre-signed stream URL for a track. Use the url immediately — it expires in ~60s.",
196        params(
197            id = "Tidal track ID",
198            quality = "Quality: LOSSLESS (default), HI_RES_LOSSLESS, HIGH, LOW"
199        )
200    )]
201    pub async fn stream_url(
202        &self,
203        id: u64,
204        quality: Option<String>,
205    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
206        let client = self.client.clone();
207        let quality = quality.unwrap_or_else(|| "LOSSLESS".to_string());
208        stream! {
209            match client.stream_manifest(id, &quality).await {
210                Ok(event) => yield event,
211                Err(e) => yield MonoEvent::Error { message: e },
212            }
213        }
214    }
215
216    /// Download a track to a local file
217    #[plexus_macros::hub_method(
218        streaming,
219        description = "Download a track to disk. Streams DownloadProgress events then DownloadComplete.",
220        params(
221            id = "Tidal track ID",
222            path = "Output file path (e.g. /tmp/track.flac)",
223            quality = "Quality: LOSSLESS (default), HI_RES_LOSSLESS, HIGH, LOW"
224        )
225    )]
226    pub async fn download(
227        &self,
228        id: u64,
229        path: String,
230        quality: Option<String>,
231    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
232        let client = self.client.clone();
233        let quality = quality.unwrap_or_else(|| "LOSSLESS".to_string());
234        stream! {
235            match client.download(id, &quality, &path).await {
236                Ok(events) => {
237                    for event in events {
238                        yield event;
239                    }
240                }
241                Err(e) => yield MonoEvent::Error { message: e },
242            }
243        }
244    }
245
246    /// Get cover art URL(s) for a track or album
247    #[plexus_macros::hub_method(
248        description = "Fetch cover art URL. Yields one or more Cover events with image URLs.",
249        params(
250            id = "Tidal track ID (integer)",
251            size = "Image size in pixels (0 = all sizes: 80, 640, 1280 — default 1280)"
252        )
253    )]
254    pub async fn cover(
255        &self,
256        id: u64,
257        size: Option<u32>,
258    ) -> impl Stream<Item = MonoEvent> + Send + 'static {
259        let client = self.client.clone();
260        let size = size.unwrap_or(1280);
261        stream! {
262            match client.cover(id, size).await {
263                Ok(covers) => {
264                    for cover in covers {
265                        yield cover;
266                    }
267                }
268                Err(e) => yield MonoEvent::Error { message: e },
269            }
270        }
271    }
272}