1use async_stream::stream;
8use futures::Stream;
9use std::sync::Arc;
10
11use crate::client::MonoClient;
12use crate::types::{MonoEvent, SearchKind};
13
14#[derive(Clone)]
16pub struct MonoHub {
17 client: Arc<MonoClient>,
18}
19
20impl MonoHub {
21 pub async fn new() -> Self {
23 let client = Arc::new(MonoClient::default_instance());
24 Self { client }
25 }
26
27 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}