A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 405 lines 14 kB view raw
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}