A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1use std::{
2 fs,
3 sync::{mpsc::Sender, Arc, Mutex},
4};
5
6use crate::{check_and_load_player, read_files, schema::objects, AUDIO_EXTENSIONS};
7use async_graphql::*;
8use futures_util::Stream;
9use rockbox_library::repo;
10use rockbox_sys::{
11 events::RockboxCommand,
12 types::{audio_status::AudioStatus, file_position::FilePosition, mp3_entry::Mp3Entry},
13};
14use rockbox_types::device::Device;
15use sqlx::{Pool, Sqlite};
16
17use crate::{rockbox_url, schema::objects::track::Track, simplebroker::SimpleBroker};
18
19#[derive(Default)]
20pub struct PlaybackQuery;
21
22#[Object]
23impl PlaybackQuery {
24 async fn status(&self) -> Result<i32, Error> {
25 let client = reqwest::Client::new();
26 let url = format!("{}/player/status", rockbox_url());
27 let response = client.get(&url).send().await?;
28 let response = response.json::<AudioStatus>().await?;
29 Ok(response.status)
30 }
31
32 async fn current_track(&self, ctx: &Context<'_>) -> Result<Option<Track>, Error> {
33 let pool = ctx.data::<Pool<Sqlite>>()?;
34 let client = ctx.data::<reqwest::Client>().unwrap();
35 let url = format!("{}/player/current-track", rockbox_url());
36 let response = client.get(&url).send().await?;
37 let track = response.json::<Option<Mp3Entry>>().await?;
38 let mut track: Option<Track> = track.map(|t| t.into());
39 let path: Option<String> = track.as_ref().map(|t| t.path.clone());
40 if let Some(path) = path {
41 let hash = format!("{:x}", md5::compute(path.as_bytes()));
42 if let Some(metadata) = repo::track::find_by_md5(pool.clone(), &hash).await? {
43 track.as_mut().unwrap().id = Some(metadata.id);
44 track.as_mut().unwrap().album_art = metadata.album_art;
45 track.as_mut().unwrap().album_id = Some(metadata.album_id);
46 track.as_mut().unwrap().artist_id = Some(metadata.artist_id);
47 }
48 }
49 Ok(track)
50 }
51
52 async fn next_track(&self, ctx: &Context<'_>) -> Result<Option<Track>, Error> {
53 let pool = ctx.data::<Pool<Sqlite>>()?;
54 let client = ctx.data::<reqwest::Client>().unwrap();
55 let url = format!("{}/player/next-track", rockbox_url());
56 let response = client.get(&url).send().await?;
57 let track = response.json::<Option<Mp3Entry>>().await?;
58 let mut track: Option<Track> = track.map(|t| t.into());
59 let path: Option<String> = track.as_ref().map(|t| t.path.clone());
60 if let Some(path) = path {
61 let hash = format!("{:x}", md5::compute(path.as_bytes()));
62 if let Some(metadata) = repo::track::find_by_md5(pool.clone(), &hash).await? {
63 track.as_mut().unwrap().id = Some(metadata.id);
64 track.as_mut().unwrap().album_art = metadata.album_art;
65 track.as_mut().unwrap().album_id = Some(metadata.album_id);
66 track.as_mut().unwrap().artist_id = Some(metadata.artist_id);
67 }
68 }
69 Ok(track)
70 }
71
72 async fn get_file_position(&self, ctx: &Context<'_>) -> Result<i32, Error> {
73 let client = ctx.data::<reqwest::Client>().unwrap();
74 let url = format!("{}/player/file-position", rockbox_url());
75 let response = client.get(&url).send().await?;
76 let response = response.json::<FilePosition>().await?;
77 Ok(response.position)
78 }
79}
80
81#[derive(Default)]
82pub struct PlaybackMutation;
83
84#[Object]
85impl PlaybackMutation {
86 async fn play(&self, ctx: &Context<'_>, elapsed: i64, offset: i64) -> Result<i32, Error> {
87 let cmd = ctx.data::<Arc<Mutex<Sender<RockboxCommand>>>>().unwrap();
88 cmd.lock()
89 .unwrap()
90 .send(RockboxCommand::Play(elapsed, offset))?;
91 Ok(0)
92 }
93
94 async fn pause(&self, ctx: &Context<'_>) -> Result<i32, Error> {
95 let client = ctx.data::<reqwest::Client>().unwrap();
96 let url = format!("{}/player/pause", rockbox_url());
97 client.put(&url).send().await?;
98 Ok(0)
99 }
100
101 async fn resume(&self, ctx: &Context<'_>) -> Result<i32, Error> {
102 let client = ctx.data::<reqwest::Client>().unwrap();
103 let url = format!("{}/player/resume", rockbox_url());
104 client.put(&url).send().await?;
105 Ok(0)
106 }
107
108 async fn next(&self, ctx: &Context<'_>) -> Result<i32, Error> {
109 let client = ctx.data::<reqwest::Client>().unwrap();
110 let url = format!("{}/player/next", rockbox_url());
111 client.put(&url).send().await?;
112 Ok(0)
113 }
114
115 async fn previous(&self, ctx: &Context<'_>) -> Result<i32, Error> {
116 let client = ctx.data::<reqwest::Client>().unwrap();
117 let url = format!("{}/player/previous", rockbox_url());
118 client.put(&url).send().await?;
119 Ok(0)
120 }
121
122 async fn fast_forward_rewind(&self, ctx: &Context<'_>, new_time: i32) -> Result<i32, Error> {
123 ctx.data::<Arc<Mutex<Sender<RockboxCommand>>>>()
124 .unwrap()
125 .lock()
126 .unwrap()
127 .send(RockboxCommand::FfRewind(new_time))?;
128 Ok(0)
129 }
130
131 async fn flush_and_reload_tracks(&self, ctx: &Context<'_>) -> Result<i32, Error> {
132 ctx.data::<Arc<Mutex<Sender<RockboxCommand>>>>()
133 .unwrap()
134 .lock()
135 .unwrap()
136 .send(RockboxCommand::FlushAndReloadTracks)?;
137 Ok(0)
138 }
139
140 async fn hard_stop(&self, ctx: &Context<'_>) -> Result<i32, Error> {
141 ctx.data::<Arc<Mutex<Sender<RockboxCommand>>>>()
142 .unwrap()
143 .lock()
144 .unwrap()
145 .send(RockboxCommand::Stop)?;
146 Ok(0)
147 }
148
149 async fn play_album(
150 &self,
151 ctx: &Context<'_>,
152 album_id: String,
153 shuffle: Option<bool>,
154 position: Option<i32>,
155 ) -> Result<i32, Error> {
156 let pool = ctx.data::<Pool<Sqlite>>()?;
157 let tracks = repo::album_tracks::find_by_album(pool.clone(), &album_id).await?;
158 let client = ctx.data::<reqwest::Client>().unwrap();
159 let tracks = tracks.into_iter().map(|t| t.path).collect::<Vec<String>>();
160 let body = serde_json::json!({
161 "tracks": tracks,
162 });
163
164 check_and_load_player!(client, tracks, shuffle.unwrap_or_default());
165
166 let url = format!("{}/playlists", rockbox_url());
167 client.post(&url).json(&body).send().await?;
168
169 if let Some(true) = shuffle {
170 let url = format!("{}/playlists/shuffle", rockbox_url());
171 client.put(&url).send().await?;
172 }
173
174 let url = match position {
175 Some(p) => format!("{}/playlists/start?start_index={}", rockbox_url(), p),
176 None => format!("{}/playlists/start", rockbox_url()),
177 };
178
179 client.put(&url).send().await?;
180
181 Ok(0)
182 }
183
184 async fn play_artist_tracks(
185 &self,
186 ctx: &Context<'_>,
187 artist_id: String,
188 shuffle: Option<bool>,
189 position: Option<i32>,
190 ) -> Result<i32, Error> {
191 let pool = ctx.data::<Pool<Sqlite>>()?;
192 let client = ctx.data::<reqwest::Client>().unwrap();
193 let tracks = repo::artist_tracks::find_by_artist(pool.clone(), &artist_id).await?;
194 let tracks = tracks.into_iter().map(|t| t.path).collect::<Vec<String>>();
195 let body = serde_json::json!({
196 "tracks": tracks,
197 });
198
199 check_and_load_player!(client, tracks, shuffle.unwrap_or_default());
200
201 let url = format!("{}/playlists", rockbox_url());
202 client.post(&url).json(&body).send().await?;
203
204 if let Some(true) = shuffle {
205 let url = format!("{}/playlists/shuffle", rockbox_url());
206 client.put(&url).send().await?;
207 }
208
209 let url = match position {
210 Some(p) => format!("{}/playlists/start?start_index={}", rockbox_url(), p),
211 None => format!("{}/playlists/start", rockbox_url()),
212 };
213
214 client.put(&url).send().await?;
215
216 Ok(0)
217 }
218
219 async fn play_playlist(
220 &self,
221 _ctx: &Context<'_>,
222 _playlist_id: String,
223 _shuffle: Option<bool>,
224 _position: Option<i32>,
225 ) -> Result<i32, Error> {
226 todo!()
227 }
228
229 async fn play_directory(
230 &self,
231 ctx: &Context<'_>,
232 path: String,
233 recurse: Option<bool>,
234 shuffle: Option<bool>,
235 position: Option<i32>,
236 ) -> Result<i32, Error> {
237 let client = ctx.data::<reqwest::Client>().unwrap();
238 let mut tracks: Vec<String> = vec![];
239
240 let recurse = match position {
241 Some(_) => Some(false),
242 None => recurse,
243 };
244
245 if !std::path::Path::new(&path).is_dir() {
246 return Err(Error::new("Invalid path"));
247 }
248
249 match recurse {
250 Some(true) => tracks = read_files(path).await?,
251 _ => {
252 for file in fs::read_dir(&path)? {
253 let file = file?;
254
255 if file.metadata()?.is_file() {
256 if !AUDIO_EXTENSIONS.iter().any(|ext| {
257 file.path()
258 .to_string_lossy()
259 .ends_with(&format!(".{}", ext))
260 }) {
261 continue;
262 }
263
264 tracks.push(file.path().to_string_lossy().to_string());
265 }
266 }
267 }
268 }
269
270 let body = serde_json::json!({
271 "tracks": tracks,
272 });
273
274 check_and_load_player!(client, tracks, shuffle.unwrap_or_default());
275
276 let url = format!("{}/playlists", rockbox_url());
277 client.post(&url).json(&body).send().await?;
278
279 if let Some(true) = shuffle {
280 let url = format!("{}/playlists/shuffle", rockbox_url());
281 client.put(&url).send().await?;
282 }
283
284 let url = match position {
285 Some(p) => format!("{}/playlists/start?start_index={}", rockbox_url(), p),
286 None => format!("{}/playlists/start", rockbox_url()),
287 };
288
289 client.put(&url).send().await?;
290
291 Ok(0)
292 }
293
294 async fn play_track(&self, ctx: &Context<'_>, path: String) -> Result<i32, Error> {
295 let client = ctx.data::<reqwest::Client>().unwrap();
296 let path = path.replace("file://", "");
297
298 let body = serde_json::json!({
299 "tracks": vec![path.clone()],
300 });
301
302 check_and_load_player!(client, vec![path], false);
303
304 let client = reqwest::Client::new();
305
306 let url = format!("{}/playlists", rockbox_url());
307 client.post(&url).json(&body).send().await?;
308
309 let client = reqwest::Client::new();
310 let url = format!("{}/playlists/start", rockbox_url());
311 client.put(&url).send().await?;
312
313 Ok(0)
314 }
315
316 async fn play_liked_tracks(
317 &self,
318 ctx: &Context<'_>,
319 shuffle: Option<bool>,
320 position: Option<i32>,
321 ) -> Result<i32, Error> {
322 let pool = ctx.data::<Pool<Sqlite>>()?;
323 let tracks = repo::favourites::all_tracks(pool.clone())
324 .await?
325 .into_iter()
326 .map(|t| t.path)
327 .collect::<Vec<String>>();
328
329 let client = ctx.data::<reqwest::Client>().unwrap();
330 let body = serde_json::json!({
331 "tracks": tracks,
332 });
333
334 check_and_load_player!(client, tracks, shuffle.unwrap_or_default());
335
336 let url = format!("{}/playlists", rockbox_url());
337 client.post(&url).json(&body).send().await?;
338
339 if let Some(true) = shuffle {
340 let url = format!("{}/playlists/shuffle", rockbox_url());
341 client.put(&url).send().await?;
342 }
343
344 let url = match position {
345 Some(p) => format!("{}/playlists/start?start_index={}", rockbox_url(), p),
346 None => format!("{}/playlists/start", rockbox_url()),
347 };
348
349 client.put(&url).send().await?;
350
351 Ok(0)
352 }
353
354 async fn play_all_tracks(
355 &self,
356 ctx: &Context<'_>,
357 shuffle: Option<bool>,
358 position: Option<i32>,
359 ) -> Result<i32, Error> {
360 let pool = ctx.data::<Pool<Sqlite>>()?;
361 let tracks = repo::track::all(pool.clone())
362 .await?
363 .into_iter()
364 .map(|t| t.path)
365 .collect::<Vec<String>>();
366
367 let client = ctx.data::<reqwest::Client>().unwrap();
368 let body = serde_json::json!({
369 "tracks": tracks,
370 });
371
372 check_and_load_player!(client, tracks, shuffle.unwrap_or_default());
373
374 let url = format!("{}/playlists", rockbox_url());
375 client.post(&url).json(&body).send().await?;
376
377 if let Some(true) = shuffle {
378 let url = format!("{}/playlists/shuffle", rockbox_url());
379 client.put(&url).send().await?;
380 }
381
382 let url = match position {
383 Some(p) => format!("{}/playlists/start?start_index={}", rockbox_url(), p),
384 None => format!("{}/playlists/start", rockbox_url()),
385 };
386
387 client.put(&url).send().await?;
388
389 Ok(0)
390 }
391}
392
393#[derive(Default)]
394pub struct PlaybackSubscription;
395
396#[Subscription]
397impl PlaybackSubscription {
398 async fn currently_playing_song(&self) -> impl Stream<Item = Track> {
399 SimpleBroker::<Track>::subscribe()
400 }
401
402 async fn playback_status(&self) -> impl Stream<Item = objects::audio_status::AudioStatus> {
403 SimpleBroker::<objects::audio_status::AudioStatus>::subscribe()
404 }
405}