Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #63 from tsirysndr/feat/mpd-commands

mpd: handle more MPD commands

authored by

Tsiry Sandratraina and committed by
GitHub
d7342939 3bd2bbbc

+1570 -211
+7
Cargo.lock
··· 6299 6299 version = "0.1.0" 6300 6300 dependencies = [ 6301 6301 "anyhow", 6302 + "chrono", 6302 6303 "regex", 6304 + "rockbox-graphql", 6305 + "rockbox-library", 6303 6306 "rockbox-rpc", 6307 + "rockbox-settings", 6308 + "rockbox-sys", 6309 + "sqlx", 6304 6310 "tokio", 6311 + "tokio-stream", 6305 6312 "tonic", 6306 6313 ] 6307 6314
+1 -1
README.md
··· 66 66 67 67 ## 📦 Downloads 68 68 69 - - `Linux`: intel: [rockbox_2024.11.10_x86_64-linux.tar.gz](https://github.com/tsirysndr/rockbox-zig/releases/download/2024.11.10/rockbox_2024.11.10_x86_64-linux.tar.gz) arm64: [rockbox_2024.11.10_aarch64-linux.tar.gz](https://github.com/tsirysndr/rockbox-zig/releases/download/2024.11.10/rockbox_2024.11.10_aarch64-linux.tar.gz) 69 + - `Linux`: intel: [rockbox_2024.11.13_x86_64-linux.tar.gz](https://github.com/tsirysndr/rockbox-zig/releases/download/2024.11.13/rockbox_2024.11.13_x86_64-linux.tar.gz) arm64: [rockbox_2024.11.13_aarch64-linux.tar.gz](https://github.com/tsirysndr/rockbox-zig/releases/download/2024.11.13/rockbox_2024.11.13_aarch64-linux.tar.gz) 70 70 71 71 72 72 ## ✨ Features
+53
crates/library/src/repo/track.rs
··· 86 86 .await?; 87 87 Ok(result) 88 88 } 89 + 90 + pub async fn find_by_artist(pool: Pool<Sqlite>, artist: &str) -> Result<Vec<Track>, Error> { 91 + let result: Vec<Track> = 92 + sqlx::query_as("SELECT * FROM track WHERE artist = $1 ORDER BY title ASC") 93 + .bind(artist) 94 + .fetch_all(&pool) 95 + .await?; 96 + Ok(result) 97 + } 98 + 99 + pub async fn find_by_album(pool: Pool<Sqlite>, album: &str) -> Result<Vec<Track>, Error> { 100 + let result: Vec<Track> = 101 + sqlx::query_as("SELECT * FROM track WHERE album = $1 ORDER BY title ASC") 102 + .bind(album) 103 + .fetch_all(&pool) 104 + .await?; 105 + Ok(result) 106 + } 107 + 108 + pub async fn find_by_title(pool: Pool<Sqlite>, title: &str) -> Result<Vec<Track>, Error> { 109 + let result: Vec<Track> = 110 + sqlx::query_as("SELECT * FROM track WHERE title = $1 ORDER BY title ASC") 111 + .bind(title) 112 + .fetch_all(&pool) 113 + .await?; 114 + Ok(result) 115 + } 116 + 117 + pub async fn find_by_filename(pool: Pool<Sqlite>, filename: &str) -> Result<Vec<Track>, Error> { 118 + let result: Vec<Track> = 119 + sqlx::query_as("SELECT * FROM track WHERE path = $1 ORDER BY title ASC") 120 + .bind(filename) 121 + .fetch_all(&pool) 122 + .await?; 123 + Ok(result) 124 + } 125 + 126 + pub async fn find_by_artist_album_date( 127 + pool: Pool<Sqlite>, 128 + artist: &str, 129 + album: &str, 130 + date: &str, 131 + ) -> Result<Vec<Track>, Error> { 132 + let result: Vec<Track> = sqlx::query_as( 133 + "SELECT * FROM track WHERE artist = $1 AND album = $2 AND year_string = $3 ORDER BY title ASC", 134 + ) 135 + .bind(artist) 136 + .bind(album) 137 + .bind(date) 138 + .fetch_all(&pool) 139 + .await?; 140 + Ok(result) 141 + }
+7
crates/mpd/Cargo.toml
··· 5 5 6 6 [dependencies] 7 7 anyhow = "1.0.93" 8 + chrono = "0.4.38" 8 9 regex = "1.11.1" 10 + rockbox-graphql = {path = "../graphql"} 11 + rockbox-library = {path = "../library"} 9 12 rockbox-rpc = {path = "../rpc"} 13 + rockbox-settings = {path = "../settings"} 14 + rockbox-sys = {path = "../sys"} 15 + sqlx = {version = "0.8.2", features = ["runtime-tokio", "tls-rustls", "sqlite", "chrono", "derive", "macros"]} 10 16 tokio = {version = "1.36.0", features = ["full"]} 17 + tokio-stream = "0.1" 11 18 tonic = "0.12.2"
+315
crates/mpd/src/consts.rs
··· 1 + pub const AUDIO_EXTENSIONS: [&str; 17] = [ 2 + "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus", 3 + "spx", "sid", "ape", "wma", 4 + ]; 5 + pub const PLAYLIST_INSERT_FIRST: i32 = -4; 6 + pub const PLAYLIST_INSERT_LAST: i32 = -3; 7 + pub const DECODERS: &str = r#"plugin: mpg123 8 + suffix: mp3 9 + plugin: vorbis 10 + suffix: ogg 11 + suffix: oga 12 + mime_type: application/ogg 13 + mime_type: application/x-ogg 14 + mime_type: audio/ogg 15 + mime_type: audio/vorbis 16 + mime_type: audio/vorbis+ogg 17 + mime_type: audio/x-ogg 18 + mime_type: audio/x-vorbis 19 + mime_type: audio/x-vorbis+ogg 20 + plugin: oggflac 21 + suffix: ogg 22 + suffix: oga 23 + mime_type: application/ogg 24 + mime_type: application/x-ogg 25 + mime_type: audio/ogg 26 + mime_type: audio/x-flac+ogg 27 + mime_type: audio/x-ogg 28 + plugin: flac 29 + suffix: flac 30 + mime_type: application/flac 31 + mime_type: application/x-flac 32 + mime_type: audio/flac 33 + mime_type: audio/x-flac 34 + plugin: opus 35 + suffix: opus 36 + suffix: ogg 37 + suffix: oga 38 + mime_type: audio/ogg 39 + mime_type: application/ogg 40 + mime_type: audio/opus 41 + plugin: sndfile 42 + suffix: wav 43 + suffix: aiff 44 + suffix: aif 45 + suffix: au 46 + suffix: snd 47 + suffix: paf 48 + suffix: iff 49 + suffix: svx 50 + suffix: sf 51 + suffix: voc 52 + suffix: w64 53 + suffix: pvf 54 + suffix: xi 55 + suffix: htk 56 + suffix: caf 57 + suffix: sd2 58 + mime_type: audio/wav 59 + mime_type: audio/aiff 60 + mime_type: audio/x-wav 61 + mime_type: audio/x-aiff 62 + plugin: dsdiff 63 + suffix: dff 64 + mime_type: application/x-dff 65 + mime_type: audio/x-dff 66 + mime_type: audio/x-dsd 67 + plugin: dsf 68 + suffix: dsf 69 + mime_type: application/x-dsf 70 + mime_type: audio/x-dsf 71 + mime_type: audio/x-dsd 72 + plugin: faad 73 + suffix: aac 74 + mime_type: audio/aac 75 + mime_type: audio/aacp 76 + plugin: wavpack 77 + suffix: wv 78 + mime_type: audio/x-wavpack 79 + plugin: fluidsynth 80 + suffix: mid 81 + plugin: ffmpeg 82 + suffix: 16sv 83 + suffix: 3g2 84 + suffix: 3gp 85 + suffix: 4xm 86 + suffix: 8svx 87 + suffix: aa3 88 + suffix: aac 89 + suffix: ac3 90 + suffix: adx 91 + suffix: afc 92 + suffix: aif 93 + suffix: aifc 94 + suffix: aiff 95 + suffix: al 96 + suffix: alaw 97 + suffix: amr 98 + suffix: anim 99 + suffix: apc 100 + suffix: ape 101 + suffix: asf 102 + suffix: atrac 103 + suffix: au 104 + suffix: aud 105 + suffix: avi 106 + suffix: avm2 107 + suffix: avs 108 + suffix: bap 109 + suffix: bfi 110 + suffix: c93 111 + suffix: cak 112 + suffix: cin 113 + suffix: cmv 114 + suffix: cpk 115 + suffix: daud 116 + suffix: dct 117 + suffix: divx 118 + suffix: dts 119 + suffix: dv 120 + suffix: dvd 121 + suffix: dxa 122 + suffix: eac3 123 + suffix: film 124 + suffix: flac 125 + suffix: flc 126 + suffix: fli 127 + suffix: fll 128 + suffix: flx 129 + suffix: flv 130 + suffix: g726 131 + suffix: gsm 132 + suffix: gxf 133 + suffix: iss 134 + suffix: m1v 135 + suffix: m2v 136 + suffix: m2t 137 + suffix: m2ts 138 + suffix: m4a 139 + suffix: m4b 140 + suffix: m4v 141 + suffix: mad 142 + suffix: mj2 143 + suffix: mjpeg 144 + suffix: mjpg 145 + suffix: mka 146 + suffix: mkv 147 + suffix: mlp 148 + suffix: mm 149 + suffix: mmf 150 + suffix: mov 151 + suffix: mp+ 152 + suffix: mp1 153 + suffix: mp2 154 + suffix: mp3 155 + suffix: mp4 156 + suffix: mpc 157 + suffix: mpeg 158 + suffix: mpg 159 + suffix: mpga 160 + suffix: mpp 161 + suffix: mpu 162 + suffix: mve 163 + suffix: mvi 164 + suffix: mxf 165 + suffix: nc 166 + suffix: nsv 167 + suffix: nut 168 + suffix: nuv 169 + suffix: oga 170 + suffix: ogm 171 + suffix: ogv 172 + suffix: ogx 173 + suffix: oma 174 + suffix: ogg 175 + suffix: omg 176 + suffix: opus 177 + suffix: psp 178 + suffix: pva 179 + suffix: qcp 180 + suffix: qt 181 + suffix: r3d 182 + suffix: ra 183 + suffix: ram 184 + suffix: rl2 185 + suffix: rm 186 + suffix: rmvb 187 + suffix: roq 188 + suffix: rpl 189 + suffix: rvc 190 + suffix: shn 191 + suffix: smk 192 + suffix: snd 193 + suffix: sol 194 + suffix: son 195 + suffix: spx 196 + suffix: str 197 + suffix: swf 198 + suffix: tak 199 + suffix: tgi 200 + suffix: tgq 201 + suffix: tgv 202 + suffix: thp 203 + suffix: ts 204 + suffix: tsp 205 + suffix: tta 206 + suffix: xa 207 + suffix: xvid 208 + suffix: uv 209 + suffix: uv2 210 + suffix: vb 211 + suffix: vid 212 + suffix: vob 213 + suffix: voc 214 + suffix: vp6 215 + suffix: vmd 216 + suffix: wav 217 + suffix: webm 218 + suffix: wma 219 + suffix: wmv 220 + suffix: wsaud 221 + suffix: wsvga 222 + suffix: wv 223 + suffix: wve 224 + mime_type: application/flv 225 + mime_type: application/m4a 226 + mime_type: application/mp4 227 + mime_type: application/octet-stream 228 + mime_type: application/ogg 229 + mime_type: application/x-ms-wmz 230 + mime_type: application/x-ms-wmd 231 + mime_type: application/x-ogg 232 + mime_type: application/x-shockwave-flash 233 + mime_type: application/x-shorten 234 + mime_type: audio/8svx 235 + mime_type: audio/16sv 236 + mime_type: audio/aac 237 + mime_type: audio/aacp 238 + mime_type: audio/ac3 239 + mime_type: audio/aiff 240 + mime_type: audio/amr 241 + mime_type: audio/basic 242 + mime_type: audio/flac 243 + mime_type: audio/m4a 244 + mime_type: audio/mp4 245 + mime_type: audio/mpeg 246 + mime_type: audio/musepack 247 + mime_type: audio/ogg 248 + mime_type: audio/opus 249 + mime_type: audio/qcelp 250 + mime_type: audio/vorbis 251 + mime_type: audio/vorbis+ogg 252 + mime_type: audio/wav 253 + mime_type: audio/x-8svx 254 + mime_type: audio/x-16sv 255 + mime_type: audio/x-aac 256 + mime_type: audio/x-ac3 257 + mime_type: audio/x-adx 258 + mime_type: audio/x-aiff 259 + mime_type: audio/x-alaw 260 + mime_type: audio/x-au 261 + mime_type: audio/x-dca 262 + mime_type: audio/x-eac3 263 + mime_type: audio/x-flac 264 + mime_type: audio/x-gsm 265 + mime_type: audio/x-mace 266 + mime_type: audio/x-matroska 267 + mime_type: audio/x-monkeys-audio 268 + mime_type: audio/x-mpeg 269 + mime_type: audio/x-ms-wma 270 + mime_type: audio/x-ms-wax 271 + mime_type: audio/x-musepack 272 + mime_type: audio/x-ogg 273 + mime_type: audio/x-vorbis 274 + mime_type: audio/x-vorbis+ogg 275 + mime_type: audio/x-pn-realaudio 276 + mime_type: audio/x-pn-multirate-realaudio 277 + mime_type: audio/x-speex 278 + mime_type: audio/x-tta 279 + mime_type: audio/x-voc 280 + mime_type: audio/x-wav 281 + mime_type: audio/x-wma 282 + mime_type: audio/x-wv 283 + mime_type: video/anim 284 + mime_type: video/quicktime 285 + mime_type: video/msvideo 286 + mime_type: video/ogg 287 + mime_type: video/theora 288 + mime_type: video/webm 289 + mime_type: video/x-dv 290 + mime_type: video/x-flv 291 + mime_type: video/x-matroska 292 + mime_type: video/x-mjpeg 293 + mime_type: video/x-mpeg 294 + mime_type: video/x-ms-asf 295 + mime_type: video/x-msvideo 296 + mime_type: video/x-ms-wmv 297 + mime_type: video/x-ms-wvx 298 + mime_type: video/x-ms-wm 299 + mime_type: video/x-ms-wmx 300 + mime_type: video/x-nut 301 + mime_type: video/x-pva 302 + mime_type: video/x-theora 303 + mime_type: video/x-vid 304 + mime_type: video/x-wmv 305 + mime_type: video/x-xvid 306 + mime_type: audio/x-mpd-ffmpeg 307 + plugin: pcm 308 + mime_type: audio/L16 309 + mime_type: audio/L24 310 + mime_type: audio/x-mpd-float 311 + mime_type: audio/x-mpd-cdda-pcm 312 + mime_type: audio/x-mpd-cdda-pcm-reverse 313 + mime_type: audio/x-mpd-alsa-pcm 314 + OK 315 + "#;
+109
crates/mpd/src/dir.rs
··· 1 + use std::{fs, thread}; 2 + 3 + use anyhow::Error; 4 + use rockbox_settings::get_music_dir; 5 + 6 + use crate::{consts::AUDIO_EXTENSIONS, Context}; 7 + 8 + pub fn read_dir( 9 + ctx: Context, 10 + path: String, 11 + recursive: bool, 12 + with_metadata: bool, 13 + ) -> Result<String, Error> { 14 + let music_dir = get_music_dir()?; 15 + let mut files = String::new(); 16 + 17 + fn read_dir_recursive( 18 + ctx: &Context, 19 + path: String, 20 + music_dir: &str, 21 + recursive: bool, 22 + files: &mut String, 23 + with_metadata: bool, 24 + ) -> Result<(), Error> { 25 + for entry in fs::read_dir(path)? { 26 + let entry = entry?; 27 + let path = entry.path(); 28 + let path_str = path.to_str().unwrap().to_string(); 29 + 30 + if path.is_dir() { 31 + if recursive { 32 + read_dir_recursive( 33 + ctx, 34 + path_str.clone(), 35 + music_dir, 36 + recursive, 37 + files, 38 + with_metadata, 39 + )?; 40 + } 41 + } 42 + 43 + if AUDIO_EXTENSIONS.iter().any(|ext| path_str.ends_with(ext)) || path.is_dir() { 44 + let last_modified = entry.metadata()?.modified()?; 45 + let last_modified = chrono::DateTime::from_timestamp( 46 + last_modified 47 + .duration_since(std::time::UNIX_EPOCH)? 48 + .as_secs() as i64, 49 + 0, 50 + ) 51 + .unwrap(); 52 + 53 + let last_modified = last_modified.format("%Y-%m-%dT%H:%M:%SZ").to_string(); 54 + let entry_str = path_str.replace(music_dir, ""); 55 + let entry_str = if entry_str.starts_with('/') { 56 + &entry_str[1..] 57 + } else { 58 + &entry_str 59 + }; 60 + 61 + let mut metadata = format!( 62 + "{}: {}\nLast-Modified: {}\n", 63 + if path.is_dir() { "directory" } else { "file" }, 64 + entry_str, 65 + last_modified 66 + ); 67 + 68 + if !with_metadata { 69 + files.push_str(&metadata); 70 + continue; 71 + } 72 + 73 + let kv = ctx.kv.clone(); 74 + let track = thread::spawn(move || { 75 + let rt = tokio::runtime::Runtime::new().unwrap(); 76 + let kv_clone = kv.clone(); 77 + let kv = rt.block_on(kv_clone.lock()); 78 + let track = kv.get(&path_str); 79 + track.map(|x| x.clone()) 80 + }) 81 + .join() 82 + .unwrap(); 83 + 84 + if let Some(track) = track { 85 + metadata.push_str(&format!( 86 + "Title: {}\nArtist: {}\nAlbum: {}\nTime: {}\nDuration: {}\n", 87 + track.title, 88 + track.artist, 89 + track.album, 90 + (track.length / 1000) as u32, 91 + track.length / 1000 92 + )); 93 + if let Some(track_number) = track.track_number { 94 + metadata.push_str(&format!("Track: {}\n", track_number)); 95 + } 96 + 97 + if let Some(year) = track.year { 98 + metadata.push_str(&format!("Date: {}\n", year)); 99 + } 100 + } 101 + files.push_str(&metadata); 102 + } 103 + } 104 + Ok(()) 105 + } 106 + 107 + read_dir_recursive(&ctx, path, &music_dir, recursive, &mut files, with_metadata)?; 108 + Ok(files) 109 + }
+77 -103
crates/mpd/src/handlers/batch.rs
··· 7 7 use crate::{parse_command, setup_context, Context}; 8 8 9 9 use super::{ 10 + browse::{handle_listall, handle_listallinfo, handle_listfiles, handle_lsinfo}, 10 11 library::{ 11 - handle_config, handle_list_album, handle_list_artist, handle_list_title, handle_rescan, 12 - handle_search, handle_stats, handle_tagtypes, handle_tagtypes_clear, 13 - handle_tagtypes_enable, 12 + handle_config, handle_find_album, handle_find_artist, handle_find_title, handle_list_album, 13 + handle_list_artist, handle_list_title, handle_rescan, handle_search, handle_stats, 14 + handle_tagtypes, handle_tagtypes_clear, handle_tagtypes_enable, 14 15 }, 15 16 playback::{ 16 17 handle_currentsong, handle_getvol, handle_next, handle_outputs, handle_pause, handle_play, ··· 18 19 handle_seekid, handle_setvol, handle_single, handle_status, handle_toggle, 19 20 }, 20 21 queue::{ 21 - handle_add, handle_clear, handle_delete, handle_move, handle_playlistinfo, handle_shuffle, 22 + handle_add, handle_addid, handle_clear, handle_delete, handle_move, handle_playlistinfo, 23 + handle_shuffle, 22 24 }, 25 + system::handle_decoders, 23 26 }; 24 27 25 28 pub async fn handle_command_list_begin( 26 - _ctx: &mut Context, 29 + ctx: &mut Context, 27 30 request: &str, 28 31 stream: &mut BufReader<TcpStream>, 29 32 ) -> Result<String, Error> { 30 - let mut ctx = setup_context(true).await?; 33 + let mut ctx = setup_context(true, Some(ctx.clone())).await?; 31 34 32 35 let commands: Vec<&str> = request 33 36 .split("\n") ··· 37 40 let mut response = String::new(); 38 41 for request in commands { 39 42 let command = parse_command(&request)?; 40 - response.push_str(&match command.as_str() { 41 - "play" => handle_play(&mut ctx, &request, stream).await?, 42 - "pause" => handle_pause(&mut ctx, &request, stream).await?, 43 - "toggle" => handle_toggle(&mut ctx, &request, stream).await?, 44 - "next" => handle_next(&mut ctx, &request, stream).await?, 45 - "previous" => handle_previous(&mut ctx, &request, stream).await?, 46 - "playid" => handle_playid(&mut ctx, &request, stream).await?, 47 - "seek" => handle_seek(&mut ctx, &request, stream).await?, 48 - "seekid" => handle_seekid(&mut ctx, &request, stream).await?, 49 - "seekcur" => handle_seekcur(&mut ctx, &request, stream).await?, 50 - "random" => handle_random(&mut ctx, &request, stream).await?, 51 - "repeat" => handle_repeat(&mut ctx, &request, stream).await?, 52 - "getvol" => handle_getvol(&mut ctx, &request, stream).await?, 53 - "setvol" => handle_setvol(&mut ctx, &request, stream).await?, 54 - "volume" => handle_setvol(&mut ctx, &request, stream).await?, 55 - "single" => handle_single(&mut ctx, &request, stream).await?, 56 - "shuffle" => handle_shuffle(&mut ctx, &request, stream).await?, 57 - "add" => handle_add(&mut ctx, &request, stream).await?, 58 - "playlistinfo" => handle_playlistinfo(&mut ctx, &request, stream).await?, 59 - "delete" => handle_delete(&mut ctx, &request, stream).await?, 60 - "clear" => handle_clear(&mut ctx, &request, stream).await?, 61 - "move" => handle_move(&mut ctx, &request, stream).await?, 62 - "list album" => handle_list_album(&mut ctx, &request, stream).await?, 63 - "list artist" => handle_list_artist(&mut ctx, &request, stream).await?, 64 - "list title" => handle_list_title(&mut ctx, &request, stream).await?, 65 - "update" => handle_rescan(&mut ctx, &request, stream).await?, 66 - "search" => handle_search(&mut ctx, &request, stream).await?, 67 - "rescan" => handle_rescan(&mut ctx, &request, stream).await?, 68 - "status" => handle_status(&mut ctx, &request, stream).await?, 69 - "currentsong" => handle_currentsong(&mut ctx, &request, stream).await?, 70 - "config" => handle_config(&mut ctx, &request, stream).await?, 71 - "tagtypes " => handle_tagtypes(&mut ctx, &request, stream).await?, 72 - "tagtypes clear" => handle_tagtypes_clear(&mut ctx, &request, stream).await?, 73 - "tagtypes enable" => handle_tagtypes_enable(&mut ctx, &request, stream).await?, 74 - "stats" => handle_stats(&mut ctx, &request, stream).await?, 75 - "plchanges" => handle_playlistinfo(&mut ctx, &request, stream).await?, 76 - "outputs" => handle_outputs(&mut ctx, &request, stream).await?, 77 - _ => { 78 - println!("Unhandled command: {}", request); 79 - if !ctx.batch { 80 - stream 81 - .write_all(b"ACK [5@0] {unhandled} unknown command\n") 82 - .await?; 83 - } 84 - "ACK [5@0] {unhandled} unknown command\n".to_string() 85 - } 86 - }); 43 + response.push_str(&match_command(&command, &mut ctx, request, stream).await?); 87 44 } 88 45 89 46 stream.write_all(response.as_bytes()).await?; ··· 92 49 } 93 50 94 51 pub async fn handle_command_list_ok_begin( 95 - _ctx: &mut Context, 52 + ctx: &mut Context, 96 53 request: &str, 97 54 stream: &mut BufReader<TcpStream>, 98 55 ) -> Result<String, Error> { 99 - let mut ctx = setup_context(true).await?; 56 + let mut ctx = setup_context(true, Some(ctx.clone())).await?; 100 57 101 58 let commands: Vec<&str> = request 102 59 .split("\n") ··· 107 64 108 65 for request in commands { 109 66 let command = parse_command(&request)?; 110 - 111 - response.push_str(&match command.as_str() { 112 - "play" => handle_play(&mut ctx, &request, stream).await?, 113 - "pause" => handle_pause(&mut ctx, &request, stream).await?, 114 - "toggle" => handle_toggle(&mut ctx, &request, stream).await?, 115 - "next" => handle_next(&mut ctx, &request, stream).await?, 116 - "previous" => handle_previous(&mut ctx, &request, stream).await?, 117 - "playid" => handle_playid(&mut ctx, &request, stream).await?, 118 - "seek" => handle_seek(&mut ctx, &request, stream).await?, 119 - "seekid" => handle_seekid(&mut ctx, &request, stream).await?, 120 - "seekcur" => handle_seekcur(&mut ctx, &request, stream).await?, 121 - "random" => handle_random(&mut ctx, &request, stream).await?, 122 - "repeat" => handle_repeat(&mut ctx, &request, stream).await?, 123 - "getvol" => handle_getvol(&mut ctx, &request, stream).await?, 124 - "setvol" => handle_setvol(&mut ctx, &request, stream).await?, 125 - "volume" => handle_setvol(&mut ctx, &request, stream).await?, 126 - "single" => handle_single(&mut ctx, &request, stream).await?, 127 - "shuffle" => handle_shuffle(&mut ctx, &request, stream).await?, 128 - "add" => handle_add(&mut ctx, &request, stream).await?, 129 - "playlistinfo" => handle_playlistinfo(&mut ctx, &request, stream).await?, 130 - "delete" => handle_delete(&mut ctx, &request, stream).await?, 131 - "clear" => handle_clear(&mut ctx, &request, stream).await?, 132 - "move" => handle_move(&mut ctx, &request, stream).await?, 133 - "list album" => handle_list_album(&mut ctx, &request, stream).await?, 134 - "list artist" => handle_list_artist(&mut ctx, &request, stream).await?, 135 - "list title" => handle_list_title(&mut ctx, &request, stream).await?, 136 - "update" => handle_rescan(&mut ctx, &request, stream).await?, 137 - "search" => handle_search(&mut ctx, &request, stream).await?, 138 - "rescan" => handle_rescan(&mut ctx, &request, stream).await?, 139 - "status" => handle_status(&mut ctx, &request, stream).await?, 140 - "currentsong" => handle_currentsong(&mut ctx, &request, stream).await?, 141 - "config" => handle_config(&mut ctx, &request, stream).await?, 142 - "tagtypes " => handle_tagtypes(&mut ctx, &request, stream).await?, 143 - "tagtypes clear" => handle_tagtypes_clear(&mut ctx, &request, stream).await?, 144 - "tagtypes enable" => handle_tagtypes_enable(&mut ctx, &request, stream).await?, 145 - "stats" => handle_stats(&mut ctx, &request, stream).await?, 146 - "plchanges" => handle_playlistinfo(&mut ctx, &request, stream).await?, 147 - "outputs" => handle_outputs(&mut ctx, &request, stream).await?, 148 - _ => { 149 - println!("Unhandled command: {}", request); 150 - if !ctx.batch { 151 - stream 152 - .write_all(b"ACK [5@0] {unhandled} unknown command\n") 153 - .await?; 154 - } 155 - "ACK [5@0] {unhandled} unknown command\n".to_string() 156 - } 157 - }); 67 + response.push_str(&match_command(&command, &mut ctx, request, stream).await?); 158 68 } 159 69 160 70 let mut response = response.replace("OK\n", "list_OK\n"); ··· 162 72 stream.write_all(response.as_bytes()).await?; 163 73 Ok(response) 164 74 } 75 + 76 + pub async fn match_command( 77 + command: &str, 78 + ctx: &mut Context, 79 + request: &str, 80 + stream: &mut BufReader<TcpStream>, 81 + ) -> Result<String, Error> { 82 + match command { 83 + "play" => handle_play(ctx, request, stream).await, 84 + "pause" => handle_pause(ctx, request, stream).await, 85 + "toggle" => handle_toggle(ctx, request, stream).await, 86 + "next" => handle_next(ctx, request, stream).await, 87 + "previous" => handle_previous(ctx, request, stream).await, 88 + "playid" => handle_playid(ctx, request, stream).await, 89 + "seek" => handle_seek(ctx, request, stream).await, 90 + "seekid" => handle_seekid(ctx, request, stream).await, 91 + "seekcur" => handle_seekcur(ctx, request, stream).await, 92 + "random" => handle_random(ctx, request, stream).await, 93 + "repeat" => handle_repeat(ctx, request, stream).await, 94 + "getvol" => handle_getvol(ctx, request, stream).await, 95 + "setvol" => handle_setvol(ctx, request, stream).await, 96 + "volume" => handle_setvol(ctx, request, stream).await, 97 + "single" => handle_single(ctx, request, stream).await, 98 + "shuffle" => handle_shuffle(ctx, request, stream).await, 99 + "add" => handle_add(ctx, request, stream).await, 100 + "addid" => handle_addid(ctx, request, stream).await, 101 + "playlistinfo" => handle_playlistinfo(ctx, request, stream).await, 102 + "delete" => handle_delete(ctx, request, stream).await, 103 + "clear" => handle_clear(ctx, request, stream).await, 104 + "move" => handle_move(ctx, request, stream).await, 105 + "list album" => handle_list_album(ctx, request, stream).await, 106 + "list artist" => handle_list_artist(ctx, request, stream).await, 107 + "list title" => handle_list_title(ctx, request, stream).await, 108 + "update" => handle_rescan(ctx, request, stream).await, 109 + "search" => handle_search(ctx, request, stream).await, 110 + "rescan" => handle_rescan(ctx, request, stream).await, 111 + "status" => handle_status(ctx, request, stream).await, 112 + "currentsong" => handle_currentsong(ctx, request, stream).await, 113 + "config" => handle_config(ctx, request, stream).await, 114 + "tagtypes " => handle_tagtypes(ctx, request, stream).await, 115 + "tagtypes clear" => handle_tagtypes_clear(ctx, request, stream).await, 116 + "tagtypes enable" => handle_tagtypes_enable(ctx, request, stream).await, 117 + "stats" => handle_stats(ctx, request, stream).await, 118 + "plchanges" => handle_playlistinfo(ctx, request, stream).await, 119 + "outputs" => handle_outputs(ctx, request, stream).await, 120 + "decoders" => handle_decoders(ctx, request, stream).await, 121 + "lsinfo" => handle_lsinfo(ctx, request, stream).await, 122 + "listall" => handle_listall(ctx, request, stream).await, 123 + "listallinfo" => handle_listallinfo(ctx, request, stream).await, 124 + "listfiles" => handle_listfiles(ctx, request, stream).await, 125 + "find artist" => handle_find_artist(ctx, request, stream).await, 126 + "find album" => handle_find_album(ctx, request, stream).await, 127 + "find title" => handle_find_title(ctx, request, stream).await, 128 + _ => { 129 + println!("Unhandled command: {}", request); 130 + if !ctx.batch { 131 + stream 132 + .write_all(b"ACK [5@0] {unhandled} unknown command\n") 133 + .await?; 134 + } 135 + Ok("ACK [5@0] {unhandled} unknown command\n".to_string()) 136 + } 137 + } 138 + }
+212
crates/mpd/src/handlers/browse.rs
··· 1 + use std::fs; 2 + 3 + use crate::{dir::read_dir, Context}; 4 + use anyhow::Error; 5 + use regex::Regex; 6 + use rockbox_library::repo; 7 + use rockbox_settings::get_music_dir; 8 + use tokio::{ 9 + io::{AsyncWriteExt, BufReader}, 10 + net::TcpStream, 11 + }; 12 + 13 + pub async fn handle_lsinfo( 14 + ctx: &mut Context, 15 + request: &str, 16 + stream: &mut BufReader<TcpStream>, 17 + ) -> Result<String, Error> { 18 + repo::track::all(ctx.pool.clone()).await?; 19 + 20 + let request = request.trim(); 21 + let re = Regex::new(r#"^([\w-]+)(?:\s+"([^"]*)")?$"#).unwrap(); 22 + let music_dir = get_music_dir()?; 23 + let path = match re.captures(request).unwrap().get(2) { 24 + Some(x) => x.as_str(), 25 + None => &music_dir, 26 + }; 27 + 28 + let path = match path.starts_with(&music_dir) { 29 + true => path, 30 + false => &format!("{}/{}", music_dir, path), 31 + }; 32 + 33 + // verify if path is a file or directory or doesn't exist 34 + if fs::metadata(path).is_err() { 35 + if !ctx.batch { 36 + stream 37 + .write_all(b"ACK [50@0] {lsinfo} No such file or directory\n") 38 + .await?; 39 + } 40 + return Ok("ACK [50@0] {lsinfo} No such file or directory\n".to_string()); 41 + } 42 + 43 + let mut response: String = "".to_string(); 44 + 45 + if !fs::metadata(path)?.is_dir() { 46 + build_file_metadata(ctx.clone(), path, &mut response, true).await?; 47 + } 48 + 49 + if fs::metadata(path)?.is_dir() { 50 + let files = read_dir(ctx.clone(), path.to_string(), false, true)?; 51 + response.push_str(&files); 52 + response.push_str("OK\n"); 53 + } 54 + 55 + if !ctx.batch { 56 + stream.write_all(response.as_bytes()).await?; 57 + } 58 + 59 + Ok(response) 60 + } 61 + 62 + pub async fn handle_listall( 63 + ctx: &mut Context, 64 + _request: &str, 65 + stream: &mut BufReader<TcpStream>, 66 + ) -> Result<String, Error> { 67 + let mut response: String = "".to_string(); 68 + let music_dir = get_music_dir()?; 69 + 70 + if fs::metadata(&music_dir)?.is_dir() { 71 + let files = read_dir(ctx.clone(), music_dir, true, false)?; 72 + response.push_str(&files); 73 + response.push_str("OK\n"); 74 + } 75 + 76 + if !ctx.batch { 77 + stream.write_all(response.as_bytes()).await?; 78 + } 79 + 80 + Ok(response) 81 + } 82 + 83 + pub async fn handle_listallinfo( 84 + ctx: &mut Context, 85 + _request: &str, 86 + stream: &mut BufReader<TcpStream>, 87 + ) -> Result<String, Error> { 88 + repo::track::all(ctx.pool.clone()).await?; 89 + let music_dir = get_music_dir()?; 90 + let path = music_dir.clone(); 91 + 92 + // verify if path is a file or directory or doesn't exist 93 + if fs::metadata(&path).is_err() { 94 + if !ctx.batch { 95 + stream 96 + .write_all(b"ACK [50@0] {lsinfo} No such file or directory\n") 97 + .await?; 98 + } 99 + return Ok("ACK [50@0] {lsinfo} No such file or directory\n".to_string()); 100 + } 101 + 102 + let mut response: String = "".to_string(); 103 + 104 + let files = read_dir(ctx.clone(), path, true, true)?; 105 + response.push_str(&files); 106 + response.push_str("OK\n"); 107 + 108 + if !ctx.batch { 109 + stream.write_all(response.as_bytes()).await?; 110 + } 111 + 112 + Ok(response) 113 + } 114 + 115 + pub async fn handle_listfiles( 116 + ctx: &mut Context, 117 + request: &str, 118 + stream: &mut BufReader<TcpStream>, 119 + ) -> Result<String, Error> { 120 + let request = request.trim(); 121 + let re = Regex::new(r#"^([\w-]+)(?:\s+"([^"]*)")?$"#).unwrap(); 122 + let music_dir = get_music_dir()?; 123 + let path = match re.captures(request).unwrap().get(2) { 124 + Some(x) => x.as_str(), 125 + None => &music_dir, 126 + }; 127 + 128 + let path = match path.starts_with(&music_dir) { 129 + true => path, 130 + false => &format!("{}/{}", music_dir, path), 131 + }; 132 + 133 + // verify if path is a file or directory or doesn't exist 134 + if fs::metadata(&path).is_err() { 135 + if !ctx.batch { 136 + stream 137 + .write_all(b"ACK [50@0] {lsinfo} No such file or directory\n") 138 + .await?; 139 + } 140 + return Ok("ACK [50@0] {lsinfo} No such file or directory\n".to_string()); 141 + } 142 + 143 + let mut response: String = "".to_string(); 144 + 145 + if !fs::metadata(path)?.is_dir() { 146 + build_file_metadata(ctx.clone(), path, &mut response, false).await?; 147 + } 148 + 149 + if fs::metadata(path)?.is_dir() { 150 + let files = read_dir(ctx.clone(), path.to_string(), false, false)?; 151 + response.push_str(&files); 152 + response.push_str("OK\n"); 153 + } 154 + 155 + if !ctx.batch { 156 + stream.write_all(response.as_bytes()).await?; 157 + } 158 + 159 + Ok(response) 160 + } 161 + 162 + async fn build_file_metadata( 163 + ctx: Context, 164 + path: &str, 165 + response: &mut String, 166 + with_metadata: bool, 167 + ) -> Result<(), Error> { 168 + let kv = ctx.kv.lock().await; 169 + let track = kv.get(path); 170 + let music_dir = get_music_dir()?; 171 + let file = path.replace(&music_dir, ""); 172 + let last_modified = fs::metadata(path)?.modified().unwrap(); 173 + let last_modified = chrono::DateTime::from_timestamp( 174 + last_modified 175 + .duration_since(std::time::UNIX_EPOCH) 176 + .unwrap() 177 + .as_secs() as i64, 178 + 0, 179 + ) 180 + .unwrap(); 181 + let last_modified = last_modified.format("%Y-%m-%dT%H:%M:%SZ").to_string(); 182 + response.push_str(&format!( 183 + "file: {}\nLast-Modified: {}\n", 184 + file, last_modified 185 + )); 186 + 187 + if !with_metadata { 188 + return Ok(()); 189 + } 190 + 191 + if let Some(track) = track { 192 + response.push_str(&format!( 193 + "Title: {}\nArtist: {}\nAlbum: {}\nTime: {}\nDuration: {}\nAlbumArtist: {}\n", 194 + track.title, 195 + track.artist, 196 + track.album, 197 + (track.length / 1000) as u32, 198 + track.length / 1000, 199 + track.album_artist, 200 + )); 201 + if let Some(track_number) = track.track_number { 202 + response.push_str(&format!("Track: {}\n", track_number)); 203 + } 204 + 205 + if let Some(year) = track.year { 206 + response.push_str(&format!("Date: {}\n", year)); 207 + } 208 + } 209 + 210 + response.push_str("OK\n"); 211 + Ok(()) 212 + }
+136 -3
crates/mpd/src/handlers/library.rs
··· 1 + use std::{collections::HashMap, fs}; 2 + 1 3 use anyhow::Error; 4 + use regex::Regex; 5 + use rockbox_library::{entity::track::Track, repo}; 2 6 use rockbox_rpc::api::rockbox::v1alpha1::{ 3 7 GetAlbumsRequest, GetArtistsRequest, GetGlobalSettingsRequest, GetTracksRequest, 4 8 ScanLibraryRequest, SearchRequest, 5 9 }; 10 + use rockbox_settings::get_music_dir; 6 11 use tokio::{ 7 12 io::{AsyncWriteExt, BufReader}, 8 13 net::TcpStream, ··· 75 80 stream: &mut BufReader<TcpStream>, 76 81 ) -> Result<String, Error> { 77 82 let term = request 78 - .replace("\"", "") 83 + .trim_matches('"') 79 84 .replace("search Album", "") 80 85 .replace("search Artist", "") 81 86 .replace("search Title", "") ··· 123 128 let path = request 124 129 .replace("update ", "") 125 130 .replace("rescan ", "") 126 - .replace("\"", ""); 131 + .trim_matches('"') 132 + .to_string(); 127 133 let path = Some(match path.starts_with("/") { 128 134 true => path, 129 135 false => format!("{}/{}", response.music_dir, path), ··· 178 184 stream.write_all(response.as_bytes()).await?; 179 185 } 180 186 181 - Ok("".to_string()) 187 + Ok(response) 182 188 } 183 189 184 190 pub async fn handle_tagtypes_enable( ··· 220 226 221 227 Ok(response) 222 228 } 229 + 230 + pub async fn handle_find_artist( 231 + ctx: &mut Context, 232 + request: &str, 233 + stream: &mut BufReader<TcpStream>, 234 + ) -> Result<String, Error> { 235 + let re = Regex::new(r#"(?i)(artist|album|date)\s+\"([^\"]+)\""#).unwrap(); 236 + let mut fields = HashMap::new(); 237 + 238 + for caps in re.captures_iter(request) { 239 + let key = caps.get(1).map(|m| m.as_str()).unwrap().to_lowercase(); 240 + let value = caps.get(2).map(|m| m.as_str()).unwrap(); 241 + fields.insert(key, value); 242 + } 243 + let artist = fields.get("artist"); 244 + let album = fields.get("album"); 245 + let date = fields.get("date"); 246 + if artist.is_none() { 247 + return Ok("ACK [2@0] {find} missing \"artist\" argument\n".to_string()); 248 + } 249 + 250 + let artist = *artist.unwrap(); 251 + let tracks = match (album, date) { 252 + (Some(album), Some(date)) => { 253 + repo::track::find_by_artist_album_date(ctx.pool.clone(), artist, *album, *date).await? 254 + } 255 + _ => repo::track::find_by_artist(ctx.pool.clone(), artist).await?, 256 + }; 257 + 258 + let mut response: String = "".to_string(); 259 + 260 + build_file_metadata(tracks, &mut response).await?; 261 + 262 + if !ctx.batch { 263 + stream.write_all(response.as_bytes()).await?; 264 + } 265 + 266 + Ok(response) 267 + } 268 + 269 + pub async fn handle_find_album( 270 + ctx: &mut Context, 271 + request: &str, 272 + stream: &mut BufReader<TcpStream>, 273 + ) -> Result<String, Error> { 274 + let arg = request.replace("find album ", "").replace("find Album", ""); 275 + let arg = arg.trim(); 276 + let arg = arg.trim_matches('"'); 277 + let tracks = repo::track::find_by_album(ctx.pool.clone(), arg).await?; 278 + 279 + let mut response: String = "".to_string(); 280 + 281 + build_file_metadata(tracks, &mut response).await?; 282 + 283 + if !ctx.batch { 284 + stream.write_all(response.as_bytes()).await?; 285 + } 286 + 287 + Ok(response) 288 + } 289 + 290 + pub async fn handle_find_title( 291 + ctx: &mut Context, 292 + request: &str, 293 + stream: &mut BufReader<TcpStream>, 294 + ) -> Result<String, Error> { 295 + let arg = request 296 + .replace("find title ", "") 297 + .replace("find Title ", ""); 298 + let arg = arg.trim(); 299 + let arg = arg.trim_matches('"'); 300 + let tracks = repo::track::find_by_title(ctx.pool.clone(), arg).await?; 301 + 302 + let mut response: String = "".to_string(); 303 + 304 + build_file_metadata(tracks, &mut response).await?; 305 + 306 + if !ctx.batch { 307 + stream.write_all(response.as_bytes()).await?; 308 + } 309 + 310 + Ok(response) 311 + } 312 + 313 + async fn build_file_metadata(tracks: Vec<Track>, response: &mut String) -> Result<(), Error> { 314 + let music_dir = get_music_dir()?; 315 + 316 + for track in tracks { 317 + let file = track.path.replace(&music_dir, ""); 318 + let file = file.chars().skip(1).collect::<String>(); 319 + 320 + let last_modified = fs::metadata(track.path)?.modified().unwrap(); 321 + let last_modified = chrono::DateTime::from_timestamp( 322 + last_modified 323 + .duration_since(std::time::UNIX_EPOCH) 324 + .unwrap() 325 + .as_secs() as i64, 326 + 0, 327 + ) 328 + .unwrap(); 329 + let last_modified = last_modified.format("%Y-%m-%dT%H:%M:%SZ").to_string(); 330 + response.push_str(&format!( 331 + "file: {}\nLast-Modified: {}\n", 332 + file, last_modified 333 + )); 334 + 335 + response.push_str(&format!( 336 + "Title: {}\nArtist: {}\nAlbum: {}\nTime: {}\nDuration: {}\nAlbumArtist: {}\n", 337 + track.title, 338 + track.artist, 339 + track.album, 340 + (track.length / 1000) as u32, 341 + track.length / 1000, 342 + track.album_artist, 343 + )); 344 + if let Some(track_number) = track.track_number { 345 + response.push_str(&format!("Track: {}\n", track_number)); 346 + } 347 + 348 + if let Some(year_string) = track.year_string { 349 + response.push_str(&format!("Date: {}\n", year_string)); 350 + } 351 + } 352 + 353 + response.push_str("OK\n"); 354 + Ok(()) 355 + }
+2
crates/mpd/src/handlers/mod.rs
··· 1 1 pub mod batch; 2 + pub mod browse; 2 3 pub mod library; 3 4 pub mod playback; 4 5 pub mod queue; 6 + pub mod system;
+154 -66
crates/mpd/src/handlers/playback.rs
··· 1 1 use anyhow::Error; 2 2 use rockbox_rpc::api::rockbox::v1alpha1::{ 3 - AdjustVolumeRequest, CurrentTrackRequest, GetCurrentRequest, GetGlobalSettingsRequest, 4 - NextRequest, PauseRequest, PlayRequest, PreviousRequest, ResumeRequest, SaveSettingsRequest, 5 - StatusRequest, 3 + AdjustVolumeRequest, NextRequest, PauseRequest, PlayRequest, PreviousRequest, ResumeRequest, 4 + SaveSettingsRequest, StartRequest, 6 5 }; 7 6 use tokio::{ 8 7 io::{AsyncWriteExt, BufReader}, ··· 17 16 stream: &mut BufReader<TcpStream>, 18 17 ) -> Result<String, Error> { 19 18 ctx.playback.resume(ResumeRequest {}).await?; 19 + match ctx.event_sender.send("player".to_string()) { 20 + Ok(_) => {} 21 + Err(_) => {} 22 + } 20 23 21 24 if !ctx.batch { 22 25 stream.write_all(b"OK\n").await?; ··· 27 30 28 31 pub async fn handle_pause( 29 32 ctx: &mut Context, 30 - request: &str, 33 + _request: &str, 31 34 stream: &mut BufReader<TcpStream>, 32 35 ) -> Result<String, Error> { 33 - let arg = request.split_whitespace().nth(1); 34 - match arg { 35 - Some(r#""0""#) => { 36 + let playback_status = ctx.playback_status.lock().await; 37 + let status = playback_status.as_ref().map(|x| x.status); 38 + 39 + match status { 40 + Some(1) => { 41 + ctx.playback.pause(PauseRequest {}).await?; 42 + } 43 + Some(3) => { 36 44 ctx.playback.resume(ResumeRequest {}).await?; 37 - if !ctx.batch { 38 - stream.write_all(b"OK\n").await?; 39 - } 40 - } 41 - Some(r#""1""#) => { 42 - ctx.playback.pause(PauseRequest {}).await?; 43 - if !ctx.batch { 44 - stream.write_all(b"OK\n").await?; 45 - } 46 45 } 47 46 _ => { 48 47 stream 49 - .write_all(b"ACK [2@0] {pause} incorrect arguments\n") 48 + .write_all(b"ACK [2@0] {pause} no song is playing\n") 50 49 .await?; 51 50 } 52 51 } 52 + 53 + match ctx.event_sender.send("player".to_string()) { 54 + Ok(_) => {} 55 + Err(_) => {} 56 + } 57 + 53 58 Ok("OK\n".to_string()) 54 59 } 55 60 ··· 58 63 _request: &str, 59 64 stream: &mut BufReader<TcpStream>, 60 65 ) -> Result<String, Error> { 61 - let response = ctx.playback.status(StatusRequest {}).await?; 62 - let response = response.into_inner(); 63 - match response.status { 64 - 1 => { 66 + let playback_status = ctx.playback_status.lock().await; 67 + let playback_status = playback_status.as_ref().map(|x| x.status); 68 + 69 + match playback_status { 70 + Some(1) => { 65 71 ctx.playback.pause(PauseRequest {}).await?; 66 72 } 67 - 3 => { 73 + Some(3) => { 68 74 ctx.playback.resume(ResumeRequest {}).await?; 69 75 } 70 76 _ => { ··· 84 90 _request: &str, 85 91 stream: &mut BufReader<TcpStream>, 86 92 ) -> Result<String, Error> { 87 - let response = ctx.playback.status(StatusRequest {}).await?; 88 - let response = response.into_inner(); 89 - let status = match response.status { 90 - 1 => "play", 91 - 3 => "pause", 93 + let playback_status = ctx.playback_status.lock().await; 94 + let playback_status = playback_status.as_ref().map(|x| x.status); 95 + 96 + let status = match playback_status { 97 + Some(1) => "play", 98 + Some(3) => "pause", 92 99 _ => "stop", 93 100 }; 94 101 95 - let response = ctx 96 - .settings 97 - .get_global_settings(GetGlobalSettingsRequest {}) 98 - .await?; 99 - let response = response.into_inner(); 100 - let repeat = match response.repeat_mode { 102 + let settings = ctx.current_settings.lock().await; 103 + let repeat = match settings.repeat_mode { 101 104 0 => 0, 102 105 1 => 1, 103 106 2 => 1, 104 107 _ => 0, 105 108 }; 106 109 107 - let random = match response.playlist_shuffle { 110 + let random = match settings.playlist_shuffle { 108 111 true => 1, 109 112 false => 0, 110 113 }; 111 114 112 - let volume = response.volume; 115 + let volume = settings.volume; 113 116 // volume is between -80 db and 0 db 114 117 // we need to convert it to 0-100 115 118 // -80 db is 0 116 119 // 0 db is 100 117 120 let volume = ((volume + 80) * 100 / 80).max(0).min(100); 118 121 119 - let response = ctx.playback.current_track(CurrentTrackRequest {}).await?; 120 - let response = response.into_inner(); 122 + let current_track = ctx.current_track.lock().await; 123 + 124 + if current_track.is_none() { 125 + let response = format!( 126 + "state: {}\nrepeat: {}\nsingle: 0\nrandom: {}\ntime: 0:0\nelapsed: 0\nplaylistlength: 0\nvolume: {}\naudio: 0:16:2\nbitrate: 0\nOK\n", 127 + status, repeat, random, volume, 128 + ); 129 + if !ctx.batch { 130 + stream.write_all(response.as_bytes()).await?; 131 + } 132 + return Ok(response); 133 + } 134 + 135 + let current_track = current_track.as_ref().unwrap(); 121 136 122 137 let time = format!( 123 138 "{}:{}", 124 - (response.elapsed / 1000) as i64, 125 - (response.length / 1000) as i64 139 + (current_track.elapsed / 1000) as i64, 140 + (current_track.length / 1000) as i64 126 141 ); 127 - let elapsed = (response.elapsed / 1000) as i64; 142 + let elapsed = (current_track.elapsed / 1000) as i64; 128 143 129 144 let single = ctx.single.lock().await; 130 145 let single = single.as_str().replace("\"", ""); 131 - let bitrate = response.bitrate; 132 - let audio = format!("{}:16:2", response.frequency); 146 + let bitrate = current_track.bitrate; 147 + let audio = format!("{}:16:2", current_track.frequency); 133 148 134 - let response = ctx.playlist.get_current(GetCurrentRequest {}).await?; 135 - let response = response.into_inner(); 136 - let playlistlength = response.amount; 137 - let song = response.index; 149 + let current_playlist = ctx.current_playlist.lock().await; 150 + if current_playlist.is_none() { 151 + let response = format!( 152 + "state: {}\nrepeat: {}\nsingle: {}\nrandom: {}\ntime: {}\nelapsed: {}\nplaylistlength: 0\nsong: 0\nvolume: {}\naudio: {}\nbitrate: {}\nOK\n", 153 + status, repeat, single, random, time, elapsed, volume, audio, bitrate, 154 + ); 155 + if !ctx.batch { 156 + stream.write_all(response.as_bytes()).await?; 157 + } 158 + return Ok(response); 159 + } 160 + 161 + let current_playlist = current_playlist.as_ref().unwrap(); 162 + let playlistlength = current_playlist.amount; 163 + let song = current_playlist.index; 138 164 139 165 let response = format!( 140 - "state: {}\nrepeat: {}\nsingle: {}\nrandom: {}\ntime: {}\nelapsed: {}\nplaylistlength: {}\nsong: {}\nvolume: {}\naudio: {}\nbitrate: {}\nOK\n", 141 - status, repeat, single, random, time, elapsed, playlistlength, song, volume, audio, bitrate, 166 + "state: {}\nrepeat: {}\nsingle: {}\nrandom: {}\ntime: {}\nelapsed: {}\nplaylist: {}\nplaylistlength: {}\nsong: {}\nsongid: {}\nvolume: {}\naudio: {}\nbitrate: {}\nnextsong: {}\nnextsongid: {}\nOK\n", 167 + status, repeat, single, random, time, elapsed, playlistlength + 1, playlistlength, song, song + 1, volume, audio, bitrate, 168 + song + 1, song + 2, 142 169 ); 143 170 144 171 if !ctx.batch { ··· 153 180 stream: &mut BufReader<TcpStream>, 154 181 ) -> Result<String, Error> { 155 182 ctx.playback.next(NextRequest {}).await?; 183 + match ctx.event_sender.send("player".to_string()) { 184 + Ok(_) => {} 185 + Err(_) => {} 186 + } 156 187 if !ctx.batch { 157 188 stream.write_all(b"OK\n").await?; 158 189 } ··· 165 196 stream: &mut BufReader<TcpStream>, 166 197 ) -> Result<String, Error> { 167 198 ctx.playback.previous(PreviousRequest {}).await?; 199 + match ctx.event_sender.send("player".to_string()) { 200 + Ok(_) => {} 201 + Err(_) => {} 202 + } 168 203 169 204 if !ctx.batch { 170 205 stream.write_all(b"OK\n").await?; ··· 178 213 request: &str, 179 214 stream: &mut BufReader<TcpStream>, 180 215 ) -> Result<String, Error> { 181 - println!("{}", request); 216 + let arg = request.split_whitespace().nth(1); 217 + 218 + if arg.is_none() { 219 + stream 220 + .write_all(b"ACK [2@0] {playid} incorrect arguments\n") 221 + .await?; 222 + return Ok("ACK [2@0] {playid} incorrect arguments\n".to_string()); 223 + } 224 + 225 + let arg = arg.unwrap(); 226 + let arg = arg.trim(); 227 + let arg = arg.trim_matches('"'); 228 + let arg = arg.parse::<i32>(); 229 + 230 + if arg.is_err() { 231 + stream 232 + .write_all(b"ACK [2@0] {playid} incorrect arguments\n") 233 + .await?; 234 + return Ok("ACK [2@0] {playid} incorrect arguments\n".to_string()); 235 + } 236 + 237 + let arg = arg.unwrap(); 238 + 239 + ctx.playlist 240 + .start(StartRequest { 241 + start_index: Some(arg - 1), 242 + ..Default::default() 243 + }) 244 + .await?; 182 245 183 246 if !ctx.batch { 184 247 stream.write_all(b"OK\n").await?; ··· 192 255 request: &str, 193 256 stream: &mut BufReader<TcpStream>, 194 257 ) -> Result<String, Error> { 258 + // TODO: Implement seek 195 259 println!("{}", request); 196 260 197 261 if !ctx.batch { ··· 206 270 request: &str, 207 271 stream: &mut BufReader<TcpStream>, 208 272 ) -> Result<String, Error> { 273 + // TODO: Implement seekid 209 274 println!("{}", request); 210 275 211 276 if !ctx.batch { ··· 231 296 ctx.playback 232 297 .play(PlayRequest { 233 298 elapsed: arg 234 - .map(|x| x.replace("\"", "")) 299 + .map(|x| x.trim_matches('"')) 235 300 .map(|x| x.parse::<i64>().unwrap() * 1000) 236 301 .unwrap_or_default(), 237 302 offset: 0, 238 303 }) 239 304 .await?; 305 + 306 + match ctx.event_sender.send("player".to_string()) { 307 + Ok(_) => {} 308 + Err(_) => {} 309 + } 310 + 240 311 if !ctx.batch { 241 312 stream.write_all(b"OK\n").await?; 242 313 } ··· 319 390 _request: &str, 320 391 stream: &mut BufReader<TcpStream>, 321 392 ) -> Result<String, Error> { 322 - let response = ctx 323 - .settings 324 - .get_global_settings(GetGlobalSettingsRequest {}) 325 - .await?; 326 - let response = response.into_inner(); 327 - let volume = response.volume; 393 + let settings = rockbox_sys::settings::get_global_settings(); 394 + let volume = settings.volume; 328 395 // volume is between -80 db and 0 db 329 396 // we need to convert it to 0-100 330 397 // -80 db is 0 ··· 344 411 request: &str, 345 412 stream: &mut BufReader<TcpStream>, 346 413 ) -> Result<String, Error> { 347 - let response = ctx 348 - .settings 349 - .get_global_settings(GetGlobalSettingsRequest {}) 350 - .await?; 351 - let response = response.into_inner(); 352 - let volume = response.volume as i32; 414 + let settings = rockbox_sys::settings::get_global_settings(); 415 + let volume = settings.volume as i32; 353 416 let arg = request.split_whitespace().nth(1); 354 417 if arg.is_none() { 355 418 if !ctx.batch { ··· 404 467 _request: &str, 405 468 stream: &mut BufReader<TcpStream>, 406 469 ) -> Result<String, Error> { 407 - let response = ctx.playback.current_track(CurrentTrackRequest {}).await?; 408 - let current = response.into_inner(); 409 - let response = ctx.playlist.get_current(GetCurrentRequest {}).await?; 410 - let current_playlist = response.into_inner(); 470 + let current = ctx.current_track.lock().await; 471 + if current.is_none() { 472 + let response = "OK\n".to_string(); 473 + if !ctx.batch { 474 + stream.write_all(response.as_bytes()).await?; 475 + } 476 + return Ok(response); 477 + } 478 + let current = current.as_ref().unwrap(); 479 + let current_playlist = ctx.current_playlist.lock().await; 480 + 481 + if current_playlist.is_none() { 482 + let response = format!( 483 + "file: {}\nTitle: {}\nArtist: {}\nAlbum: {}\nTrack: {}\nDate: {}\nTime: {}\nPos: 0\nOK\n", 484 + current.path, 485 + current.title, 486 + current.artist, 487 + current.album, 488 + current.tracknum, 489 + current.year, 490 + (current.elapsed / 1000) as i64, 491 + ); 492 + if !ctx.batch { 493 + stream.write_all(response.as_bytes()).await?; 494 + } 495 + return Ok(response); 496 + } 497 + 498 + let current_playlist = current_playlist.as_ref().unwrap(); 411 499 let response = format!( 412 500 "file: {}\nTitle: {}\nArtist: {}\nAlbum: {}\nTrack: {}\nDate: {}\nTime: {}\nPos: {}\nOK\n", 413 501 current.path,
+167 -15
crates/mpd/src/handlers/queue.rs
··· 1 1 use std::fs; 2 2 3 - use crate::{Context, PLAYLIST_INSERT_LAST}; 3 + use crate::{consts::PLAYLIST_INSERT_LAST, Context}; 4 4 use anyhow::Error; 5 5 use regex::Regex; 6 6 use rockbox_rpc::api::rockbox::v1alpha1::{ 7 - GetCurrentRequest, GetGlobalSettingsRequest, InsertDirectoryRequest, InsertTracksRequest, 8 - RemoveAllTracksRequest, RemoveTracksRequest, ShufflePlaylistRequest, 7 + GetGlobalSettingsRequest, InsertDirectoryRequest, InsertTracksRequest, RemoveAllTracksRequest, 8 + RemoveTracksRequest, ShufflePlaylistRequest, StartRequest, 9 9 }; 10 10 use tokio::{ 11 11 io::{AsyncWriteExt, BufReader}, ··· 17 17 _request: &str, 18 18 stream: &mut BufReader<TcpStream>, 19 19 ) -> Result<String, Error> { 20 + let mut idle = ctx.idle.lock().await; 21 + *idle = true; 22 + 20 23 ctx.playlist 21 24 .shuffle_playlist(ShufflePlaylistRequest { start_index: 0 }) 22 25 .await?; 23 26 if !ctx.batch { 24 27 stream.write_all(b"OK\n").await?; 25 28 } 29 + 30 + match ctx.event_sender.send("playlist".to_string()) { 31 + Ok(_) => {} 32 + Err(_) => {} 33 + } 34 + 26 35 Ok("OK\n".to_string()) 27 36 } 28 37 ··· 31 40 request: &str, 32 41 stream: &mut BufReader<TcpStream>, 33 42 ) -> Result<String, Error> { 43 + let mut idle = ctx.idle.lock().await; 44 + *idle = true; 45 + 34 46 let response = ctx 35 47 .settings 36 48 .get_global_settings(GetGlobalSettingsRequest {}) ··· 38 50 let response = response.into_inner(); 39 51 let music_dir = response.music_dir; 40 52 53 + let request = request.trim(); 41 54 let re = Regex::new(r#"^(\w+)\s+"([^"]+)"(?:\s+"?(-?\d+)"?)?$"#).unwrap(); 42 55 let captures = re.captures(request); 43 56 if captures.is_none() { ··· 100 113 .await?; 101 114 } 102 115 116 + let current_track = ctx.current_track.lock().await; 117 + 118 + if current_track.is_none() { 119 + ctx.playlist.start(StartRequest::default()).await?; 120 + } 121 + 103 122 if !ctx.batch { 104 123 stream.write_all(b"OK\n").await?; 105 124 } 125 + 126 + match ctx.event_sender.send("playlist".to_string()) { 127 + Ok(_) => {} 128 + Err(_) => {} 129 + } 130 + 131 + Ok("OK\n".to_string()) 132 + } 133 + 134 + pub async fn handle_addid( 135 + ctx: &mut Context, 136 + request: &str, 137 + stream: &mut BufReader<TcpStream>, 138 + ) -> Result<String, Error> { 139 + let mut idle = ctx.idle.lock().await; 140 + *idle = true; 141 + 142 + let response = ctx 143 + .settings 144 + .get_global_settings(GetGlobalSettingsRequest {}) 145 + .await?; 146 + let response = response.into_inner(); 147 + let music_dir = response.music_dir; 148 + 149 + let request = request.trim(); 150 + let re = Regex::new(r#"^(\w+)\s+"([^"]+)"(?:\s+"?(-?\d+)"?)?$"#).unwrap(); 151 + let captures = re.captures(request); 152 + 153 + println!("captures: {:?}", captures); 154 + 155 + if captures.is_none() { 156 + if !ctx.batch { 157 + stream 158 + .write_all(b"ACK [2@0] {add} missing argument\n") 159 + .await?; 160 + } 161 + return Ok("ACK [2@0] {add} missing argument\n".to_string()); 162 + } 163 + let captures = captures.unwrap(); 164 + 165 + let path = captures.get(2).unwrap().as_str().to_string(); 166 + let position = captures 167 + .get(3) 168 + .map(|x| x.as_str().parse::<i32>().unwrap_or(PLAYLIST_INSERT_LAST)) 169 + .unwrap_or(PLAYLIST_INSERT_LAST); 170 + 171 + if path.is_empty() { 172 + if !ctx.batch { 173 + stream 174 + .write_all(b"ACK [2@0] {add} missing argument\n") 175 + .await?; 176 + } 177 + return Ok("ACK [2@0] {add} missing argument\n".to_string()); 178 + } 179 + 180 + let path = match path.starts_with('/') { 181 + true => path, 182 + false => format!("{}/{}", music_dir, path), 183 + }; 184 + 185 + // verify if path is a file or directory or doesn't exist 186 + if fs::metadata(&path).is_err() { 187 + if !ctx.batch { 188 + stream 189 + .write_all(b"ACK [50@0] {add} No such file or directory\n") 190 + .await?; 191 + } 192 + return Ok("ACK [50@0] {add} No such file or directory\n".to_string()); 193 + } 194 + 195 + if fs::metadata(&path)?.is_file() { 196 + ctx.playlist 197 + .insert_tracks(InsertTracksRequest { 198 + tracks: vec![path.clone()], 199 + position, 200 + ..Default::default() 201 + }) 202 + .await?; 203 + } 204 + 205 + if fs::metadata(&path)?.is_dir() { 206 + // return error if directory, invalid for addid 207 + if !ctx.batch { 208 + stream 209 + .write_all(b"ACK [2@0] {addid} invalid argument\n") 210 + .await?; 211 + } 212 + return Ok("ACK [2@0] {addid} invalid argument\n".to_string()); 213 + } 214 + 215 + let current_track = ctx.current_track.lock().await; 216 + if current_track.is_none() { 217 + ctx.playlist.start(StartRequest::default()).await?; 218 + } 219 + 220 + if !ctx.batch { 221 + stream.write_all(b"OK\n").await?; 222 + } 223 + 224 + match ctx.event_sender.send("playlist".to_string()) { 225 + Ok(_) => {} 226 + Err(_) => {} 227 + } 228 + 106 229 Ok("OK\n".to_string()) 107 230 } 108 231 ··· 111 234 _request: &str, 112 235 stream: &mut BufReader<TcpStream>, 113 236 ) -> Result<String, Error> { 114 - let response = ctx.playlist.get_current(GetCurrentRequest {}).await?; 115 - let response = response.into_inner(); 237 + let current_playlist = ctx.current_playlist.lock().await; 238 + 239 + if current_playlist.is_none() { 240 + if !ctx.batch { 241 + stream.write_all(b"OK\n").await?; 242 + } 243 + return Ok("OK\n".to_string()); 244 + } 245 + 116 246 let mut index = -1; 117 - let response = response 247 + let current_playlist = current_playlist.as_ref().unwrap(); 248 + let response = current_playlist 118 249 .tracks 119 250 .iter() 120 251 .map(|x| { 121 252 index += 1; 122 253 format!( 123 - "file: {}\nTitle: {}\nArtist: {}\nAlbum: {}\nTime: {}\nPos: {}\n", 254 + "file: {}\nTitle: {}\nArtist: {}\nAlbum: {}\nTime: {}\nPos: {}\nDisc: {}\nDate: {}\nAlbumArtist: {}\nTrack: {}\nId: {}\n", 124 255 x.path, 125 256 x.title, 126 257 x.artist, 127 258 x.album, 128 259 (x.length / 1000) as u32, 129 260 index, 261 + x.discnum, 262 + x.year_string, 263 + x.album_artist, 264 + x.tracknum, 265 + index + 1 130 266 ) 131 267 }) 132 268 .collect::<String>(); ··· 144 280 request: &str, 145 281 stream: &mut BufReader<TcpStream>, 146 282 ) -> Result<String, Error> { 147 - let request = request.replace("\"", ""); 283 + let mut idle = ctx.idle.lock().await; 284 + *idle = true; 285 + 148 286 let arg = request.split_whitespace().last(); 149 287 if arg.is_none() { 150 288 if !ctx.batch { ··· 154 292 } 155 293 return Ok("ACK [2@0] {delete} missing argument\n".to_string()); 156 294 } 157 - if arg.unwrap().contains(':') { 295 + let arg = arg.unwrap(); 296 + let arg = arg.trim(); 297 + let arg = arg.trim_matches('"'); 298 + if arg.contains(':') { 158 299 // get the range 159 - let range: Vec<i32> = arg 160 - .unwrap() 161 - .split(':') 162 - .map(|x| x.parse::<i32>().unwrap()) 163 - .collect(); 300 + let range: Vec<i32> = arg.split(':').map(|x| x.parse::<i32>().unwrap()).collect(); 164 301 let positions: Vec<i32> = (range[0]..=range[1]).collect(); 165 302 ctx.playlist 166 303 .remove_tracks(RemoveTracksRequest { positions }) ··· 170 307 } 171 308 return Ok("OK\n".to_string()); 172 309 } 173 - let positions = match arg.unwrap().parse::<i32>() { 310 + let positions = match arg.parse::<i32>() { 174 311 Ok(x) => vec![x], 175 312 Err(_) => { 176 313 if !ctx.batch { ··· 187 324 if !ctx.batch { 188 325 stream.write_all(b"OK\n").await?; 189 326 } 327 + 328 + match ctx.event_sender.send("playlist".to_string()) { 329 + Ok(_) => {} 330 + Err(_) => {} 331 + } 332 + 190 333 Ok("OK\n".to_string()) 191 334 } 192 335 ··· 195 338 _request: &str, 196 339 stream: &mut BufReader<TcpStream>, 197 340 ) -> Result<String, Error> { 341 + let mut idle = ctx.idle.lock().await; 342 + *idle = true; 343 + 198 344 ctx.playlist 199 345 .remove_all_tracks(RemoveAllTracksRequest { positions: vec![] }) 200 346 .await?; 201 347 if !ctx.batch { 202 348 stream.write_all(b"OK\n").await?; 203 349 } 350 + 351 + match ctx.event_sender.send("playlist".to_string()) { 352 + Ok(_) => {} 353 + Err(_) => {} 354 + } 355 + 204 356 Ok("OK\n".to_string()) 205 357 } 206 358
+53
crates/mpd/src/handlers/system.rs
··· 1 + use anyhow::Error; 2 + use tokio::{ 3 + io::{AsyncWriteExt, BufReader}, 4 + net::TcpStream, 5 + }; 6 + 7 + use crate::{consts::DECODERS, Context}; 8 + 9 + pub async fn handle_idle( 10 + _ctx: &mut Context, 11 + _request: &str, 12 + _stream: &mut BufReader<TcpStream>, 13 + ) -> Result<String, Error> { 14 + // TODO: Implement idle 15 + /* 16 + let idle = ctx.idle.lock().await; 17 + 18 + if *idle { 19 + stream 20 + .write_all(b"changed: player\nchanged: playlist\nOK\n") 21 + .await?; 22 + return Ok("changed: player\nchanged: playlist\nOK\n".to_string()); 23 + } 24 + */ 25 + Ok("".to_string()) 26 + } 27 + 28 + pub async fn handle_noidle( 29 + ctx: &mut Context, 30 + _request: &str, 31 + stream: &mut BufReader<TcpStream>, 32 + ) -> Result<String, Error> { 33 + ctx.idle_state.send(false)?; 34 + let mut idle = ctx.idle.lock().await; 35 + *idle = false; 36 + 37 + let response = "OK\n".to_string(); 38 + if !ctx.batch { 39 + stream.write_all(response.as_bytes()).await?; 40 + } 41 + Ok(response) 42 + } 43 + 44 + pub async fn handle_decoders( 45 + ctx: &mut Context, 46 + _request: &str, 47 + stream: &mut BufReader<TcpStream>, 48 + ) -> Result<String, Error> { 49 + if !ctx.batch { 50 + stream.write_all(DECODERS.as_bytes()).await?; 51 + } 52 + Ok(DECODERS.to_string()) 53 + }
+53
crates/mpd/src/kv.rs
··· 1 + use std::collections::HashMap; 2 + 3 + use anyhow::Error; 4 + use rockbox_library::{entity, repo}; 5 + use sqlx::{Pool, Sqlite}; 6 + 7 + #[derive(Clone)] 8 + pub struct KV<V> { 9 + store: HashMap<String, V>, 10 + } 11 + 12 + impl<V> KV<V> { 13 + pub fn new() -> Self { 14 + Self { 15 + store: HashMap::new(), 16 + } 17 + } 18 + 19 + pub fn get(&self, key: &str) -> Option<&V> { 20 + self.store.get(key) 21 + } 22 + 23 + pub fn set(&mut self, key: &str, value: V) { 24 + self.store.insert(key.to_string(), value); 25 + } 26 + 27 + pub fn remove(&mut self, key: &str) { 28 + self.store.remove(key); 29 + } 30 + 31 + pub fn keys(&self) -> Vec<String> { 32 + self.store.keys().cloned().collect() 33 + } 34 + 35 + pub fn values(&self) -> Vec<&V> { 36 + self.store.values().collect() 37 + } 38 + 39 + pub fn id(&self, key: &str) -> Option<usize> { 40 + self.keys().iter().position(|x| x == key) 41 + } 42 + } 43 + 44 + pub async fn build_tracks_kv(pool: Pool<Sqlite>) -> Result<KV<entity::track::Track>, Error> { 45 + let tracks = repo::track::all(pool.clone()).await?; 46 + let mut kv = KV::new(); 47 + 48 + for track in tracks { 49 + kv.set(&track.path, track.clone()); 50 + } 51 + 52 + Ok(kv) 53 + }
+223 -14
crates/mpd/src/lib.rs
··· 1 - use std::{env, sync::Arc}; 2 - 3 1 use anyhow::Error; 4 2 use handlers::{ 5 3 batch::{handle_command_list_begin, handle_command_list_ok_begin}, 4 + browse::{handle_listall, handle_listallinfo, handle_listfiles, handle_lsinfo}, 6 5 library::{ 7 - handle_config, handle_list_album, handle_list_artist, handle_list_title, handle_rescan, 8 - handle_search, handle_stats, handle_tagtypes, handle_tagtypes_enable, 6 + handle_config, handle_find_album, handle_find_artist, handle_find_title, handle_list_album, 7 + handle_list_artist, handle_list_title, handle_rescan, handle_search, handle_stats, 8 + handle_tagtypes, handle_tagtypes_enable, 9 9 }, 10 10 playback::{ 11 11 handle_currentsong, handle_getvol, handle_next, handle_outputs, handle_pause, handle_play, ··· 13 13 handle_seekid, handle_setvol, handle_single, handle_status, handle_toggle, 14 14 }, 15 15 queue::{ 16 - handle_add, handle_clear, handle_delete, handle_move, handle_playlistinfo, handle_shuffle, 16 + handle_add, handle_addid, handle_clear, handle_delete, handle_move, handle_playlistinfo, 17 + handle_shuffle, 17 18 }, 19 + system::{handle_decoders, handle_idle, handle_noidle}, 18 20 }; 21 + use kv::{build_tracks_kv, KV}; 22 + use rockbox_graphql::{ 23 + schema::objects::{audio_status::AudioStatus, playlist::Playlist, track::Track}, 24 + simplebroker::SimpleBroker, 25 + }; 26 + use rockbox_library::{create_connection_pool, entity}; 19 27 use rockbox_rpc::api::rockbox::v1alpha1::{ 20 28 library_service_client::LibraryServiceClient, playback_service_client::PlaybackServiceClient, 21 29 playlist_service_client::PlaylistServiceClient, settings_service_client::SettingsServiceClient, 22 - sound_service_client::SoundServiceClient, 30 + sound_service_client::SoundServiceClient, system_service_client::SystemServiceClient, 31 + GetCurrentRequest, GetGlobalStatusRequest, PlaylistResumeRequest, 23 32 }; 33 + use rockbox_sys::types::user_settings::UserSettings; 34 + use sqlx::{Pool, Sqlite}; 35 + use std::{env, sync::Arc, thread, time::Duration}; 24 36 use tokio::{ 25 37 io::{AsyncReadExt, AsyncWriteExt}, 26 38 net::{TcpListener, TcpStream}, 27 - sync::Mutex, 39 + sync::{broadcast, watch, Mutex}, 28 40 }; 41 + use tokio_stream::StreamExt; 29 42 use tonic::transport::Channel; 30 43 31 - pub const PLAYLIST_INSERT_FIRST: i32 = -4; 32 - pub const PLAYLIST_INSERT_LAST: i32 = -3; 33 - 44 + pub mod consts; 45 + pub mod dir; 34 46 pub mod handlers; 47 + pub mod kv; 35 48 36 49 #[derive(Clone)] 37 50 pub struct Context { ··· 40 53 pub settings: SettingsServiceClient<Channel>, 41 54 pub sound: SoundServiceClient<Channel>, 42 55 pub playlist: PlaylistServiceClient<Channel>, 56 + pub system: SystemServiceClient<Channel>, 43 57 pub single: Arc<Mutex<String>>, 44 58 pub batch: bool, 59 + pub idle_state: Arc<watch::Sender<bool>>, 60 + pub idle_cancel: watch::Receiver<bool>, 61 + pub event_sender: broadcast::Sender<String>, 62 + pub current_track: Arc<Mutex<Option<Track>>>, 63 + pub current_playlist: Arc<Mutex<Option<Playlist>>>, 64 + pub playback_status: Arc<Mutex<Option<AudioStatus>>>, 65 + pub pool: Pool<Sqlite>, 66 + pub kv: Arc<Mutex<KV<entity::track::Track>>>, 67 + pub current_settings: Arc<Mutex<UserSettings>>, 68 + pub idle: Arc<Mutex<bool>>, 45 69 } 46 - 47 70 pub struct MpdServer {} 48 71 49 72 impl MpdServer { 50 73 pub async fn start() -> Result<(), Error> { 51 74 let port = env::var("ROCKBOX_MPD_PORT").unwrap_or_else(|_| "6600".to_string()); 52 75 let addr = format!("0.0.0.0:{}", port); 53 - let context = setup_context(false).await?; 76 + let context = setup_context(false, None).await?; 77 + 78 + listen_events(context.clone()); 79 + 80 + thread::sleep(Duration::from_millis(200)); 81 + 82 + restore_playlist(context.clone())?; 54 83 55 84 let listener = TcpListener::bind(&addr).await?; 56 85 ··· 80 109 } 81 110 let request = String::from_utf8_lossy(&buf[..n]); 82 111 let command = parse_command(&request)?; 112 + println!("request: {}", request); 83 113 84 114 match command.as_str() { 85 115 "play" => handle_play(&mut ctx, &request, &mut stream).await?, ··· 99 129 "single" => handle_single(&mut ctx, &request, &mut stream).await?, 100 130 "shuffle" => handle_shuffle(&mut ctx, &request, &mut stream).await?, 101 131 "add" => handle_add(&mut ctx, &request, &mut stream).await?, 132 + "addid" => handle_addid(&mut ctx, &request, &mut stream).await?, 102 133 "playlistinfo" => handle_playlistinfo(&mut ctx, &request, &mut stream).await?, 103 134 "delete" => handle_delete(&mut ctx, &request, &mut stream).await?, 104 135 "clear" => handle_clear(&mut ctx, &request, &mut stream).await?, ··· 118 149 "stats" => handle_stats(&mut ctx, &request, &mut stream).await?, 119 150 "plchanges" => handle_playlistinfo(&mut ctx, &request, &mut stream).await?, 120 151 "outputs" => handle_outputs(&mut ctx, &request, &mut stream).await?, 152 + "idle" => handle_idle(&mut ctx, &request, &mut stream).await?, 153 + "noidle" => handle_noidle(&mut ctx, &request, &mut stream).await?, 154 + "decoders" => handle_decoders(&mut ctx, &request, &mut stream).await?, 155 + "lsinfo" => handle_lsinfo(&mut ctx, &request, &mut stream).await?, 156 + "listall" => handle_listall(&mut ctx, &request, &mut stream).await?, 157 + "listallinfo" => handle_listallinfo(&mut ctx, &request, &mut stream).await?, 158 + "listfiles" => handle_listfiles(&mut ctx, &request, &mut stream).await?, 159 + "find artist" => handle_find_artist(&mut ctx, &request, &mut stream).await?, 160 + "find album" => handle_find_album(&mut ctx, &request, &mut stream).await?, 161 + "find title" => handle_find_title(&mut ctx, &request, &mut stream).await?, 121 162 "command_list_begin" => { 122 163 handle_command_list_begin(&mut ctx, &request, &mut stream).await? 123 164 } ··· 125 166 handle_command_list_ok_begin(&mut ctx, &request, &mut stream).await? 126 167 } 127 168 _ => { 128 - println!("Unhandled command: {}", request); 169 + println!("Unhandled command: {}", command); 170 + println!("Unhandled request: {}", request); 129 171 stream 130 172 .write_all(b"ACK [5@0] {unhandled} unknown command\n") 131 173 .await?; ··· 150 192 return Ok(format!("tagtypes {}", r#type.replace("\"", ""))); 151 193 } 152 194 195 + if command == "find" { 196 + let r#type = request.split_whitespace().nth(1).unwrap_or_default(); 197 + return Ok(format!("find {}", r#type.to_lowercase())); 198 + } 199 + 153 200 Ok(command.to_string()) 154 201 } 155 202 156 - pub async fn setup_context(batch: bool) -> Result<Context, Error> { 203 + pub async fn setup_context(batch: bool, ctx: Option<Context>) -> Result<Context, Error> { 157 204 let port = env::var("ROCKBOX_PORT").unwrap_or_else(|_| "6061".to_string()); 158 205 let host = env::var("ROCKBOX_HOST").unwrap_or_else(|_| "localhost".to_string()); 159 206 let url = format!("tcp://{}:{}", host, port); 160 207 208 + let pool = create_connection_pool().await?; 209 + let kv = Arc::new(Mutex::new(build_tracks_kv(pool.clone()).await?)); 210 + 161 211 let library = LibraryServiceClient::connect(url.clone()).await?; 162 212 let playback = PlaybackServiceClient::connect(url.clone()).await?; 163 213 let settings = SettingsServiceClient::connect(url.clone()).await?; 164 214 let sound = SoundServiceClient::connect(url.clone()).await?; 165 215 let playlist = PlaylistServiceClient::connect(url.clone()).await?; 216 + let system = SystemServiceClient::connect(url.clone()).await?; 217 + 218 + let (event_sender, _) = broadcast::channel(16); 219 + let (idle_state, idle_cancel) = watch::channel(false); 166 220 167 221 Ok(Context { 168 222 library, ··· 170 224 settings, 171 225 sound, 172 226 playlist, 227 + system, 173 228 single: Arc::new(Mutex::new("\"0\"".to_string())), 174 229 batch, 230 + idle_state: match ctx { 231 + Some(ref ctx) => ctx.clone().idle_state, 232 + None => Arc::new(idle_state), 233 + }, 234 + idle_cancel: match ctx { 235 + Some(ref ctx) => ctx.clone().idle_cancel, 236 + None => idle_cancel, 237 + }, 238 + event_sender: match ctx { 239 + Some(ref ctx) => ctx.clone().event_sender, 240 + None => event_sender, 241 + }, 242 + current_track: match ctx { 243 + Some(ref ctx) => ctx.clone().current_track, 244 + None => Arc::new(Mutex::new(None)), 245 + }, 246 + current_playlist: match ctx { 247 + Some(ref ctx) => ctx.clone().current_playlist, 248 + None => Arc::new(Mutex::new(None)), 249 + }, 250 + playback_status: match ctx { 251 + Some(ref ctx) => ctx.clone().playback_status, 252 + None => Arc::new(Mutex::new(None)), 253 + }, 254 + pool, 255 + kv, 256 + current_settings: Arc::new(Mutex::new(rockbox_sys::settings::get_global_settings())), 257 + idle: Arc::new(Mutex::new(false)), 175 258 }) 176 259 } 260 + 261 + pub fn listen_events(ctx: Context) { 262 + let ctx_clone = ctx.clone(); 263 + let another_ctx = ctx.clone(); 264 + let another_cloned_ctx = ctx.clone(); 265 + 266 + thread::spawn(move || { 267 + let rt = tokio::runtime::Runtime::new().unwrap(); 268 + loop { 269 + let mut current_settings = rt.block_on(another_cloned_ctx.current_settings.lock()); 270 + *current_settings = rockbox_sys::settings::get_global_settings(); 271 + drop(current_settings); 272 + thread::sleep(std::time::Duration::from_millis(800)); 273 + } 274 + }); 275 + 276 + thread::spawn(move || { 277 + let mut subscription = SimpleBroker::<Track>::subscribe(); 278 + let rt = tokio::runtime::Runtime::new().unwrap(); 279 + 280 + while let Some(track) = rt.block_on(subscription.next()) { 281 + let mut current_track = rt.block_on(ctx.current_track.lock()); 282 + *current_track = Some(track); 283 + } 284 + }); 285 + 286 + thread::spawn(move || { 287 + let mut subscription = SimpleBroker::<Playlist>::subscribe(); 288 + let rt = tokio::runtime::Runtime::new().unwrap(); 289 + 290 + while let Some(playlist) = rt.block_on(subscription.next()) { 291 + let mut current_playlist = rt.block_on(ctx_clone.current_playlist.lock()); 292 + 293 + // verify if current_playlist index is different from playlist index 294 + if (current_playlist.is_some() 295 + && current_playlist.as_ref().unwrap().index != playlist.index) 296 + || current_playlist.is_none() 297 + { 298 + let ctx = ctx_clone.clone(); 299 + thread::spawn(move || { 300 + thread::sleep(std::time::Duration::from_millis(500)); 301 + let rt = tokio::runtime::Runtime::new().unwrap(); 302 + let mut idle = rt.block_on(ctx.idle.lock()); 303 + *idle = true; 304 + }); 305 + } 306 + 307 + *current_playlist = Some(playlist); 308 + } 309 + }); 310 + 311 + thread::spawn(move || { 312 + let mut subscription = SimpleBroker::<AudioStatus>::subscribe(); 313 + let rt = tokio::runtime::Runtime::new().unwrap(); 314 + 315 + while let Some(status) = rt.block_on(subscription.next()) { 316 + let mut playback_status = rt.block_on(another_ctx.playback_status.lock()); 317 + // verify if playback_status status is different from status status 318 + if playback_status.is_some() 319 + && playback_status.as_ref().unwrap().status != status.status 320 + { 321 + let ctx = another_ctx.clone(); 322 + thread::spawn(move || { 323 + thread::sleep(std::time::Duration::from_millis(500)); 324 + let rt = tokio::runtime::Runtime::new().unwrap(); 325 + let mut idle = rt.block_on(ctx.idle.lock()); 326 + *idle = true; 327 + }); 328 + } 329 + *playback_status = Some(status); 330 + } 331 + }); 332 + } 333 + 334 + pub fn restore_playlist(ctx: Context) -> Result<(), Error> { 335 + let ctx_clone = ctx.clone(); 336 + thread::spawn(move || { 337 + let mut ctx = ctx_clone.clone(); 338 + let rt = tokio::runtime::Runtime::new().unwrap(); 339 + rt.block_on(async { 340 + let response = ctx 341 + .system 342 + .get_global_status(GetGlobalStatusRequest {}) 343 + .await?; 344 + let response = response.into_inner(); 345 + 346 + let playback_status = ctx.playback_status.lock().await; 347 + let mut status = 0; 348 + 349 + if playback_status.is_some() { 350 + status = playback_status.as_ref().unwrap().status; 351 + } 352 + 353 + if response.resume_index > -1 && status != 1 { 354 + ctx.playlist 355 + .playlist_resume(PlaylistResumeRequest {}) 356 + .await?; 357 + let resume_index = response.resume_index; 358 + let resume_elapsed = response.resume_elapsed; 359 + thread::sleep(std::time::Duration::from_millis(500)); 360 + let response = ctx.playlist.get_current(GetCurrentRequest {}).await?; 361 + let response = response.into_inner(); 362 + let mut current_track = ctx.current_track.lock().await; 363 + *current_track = Some(Track { 364 + path: response.tracks[resume_index as usize].path.clone(), 365 + artist: response.tracks[resume_index as usize].artist.clone(), 366 + album: response.tracks[resume_index as usize].album.clone(), 367 + title: response.tracks[resume_index as usize].title.clone(), 368 + album_artist: response.tracks[resume_index as usize].album_artist.clone(), 369 + elapsed: resume_elapsed as u64, 370 + length: response.tracks[resume_index as usize].length, 371 + tracknum: response.tracks[resume_index as usize].tracknum, 372 + year: response.tracks[resume_index as usize].year, 373 + year_string: response.tracks[resume_index as usize].year_string.clone(), 374 + ..Default::default() 375 + }); 376 + } 377 + 378 + Ok::<(), Error>(()) 379 + })?; 380 + 381 + Ok::<(), Error>(()) 382 + }); 383 + 384 + Ok(()) 385 + }
-8
crates/mpd/src/main.rs
··· 1 - use anyhow::Error; 2 - use rockbox_mpd::MpdServer; 3 - 4 - #[tokio::main] 5 - async fn main() -> Result<(), Error> { 6 - MpdServer::start().await?; 7 - Ok(()) 8 - }
+1 -1
webui/rockbox/package.json
··· 1 1 { 2 2 "name": "rockbox", 3 3 "private": true, 4 - "version": "2024.11.10", 4 + "version": "2024.11.13", 5 5 "type": "module", 6 6 "main": "dist-electron/main/index.js", 7 7 "author": "Tsiry Sandratraina <tsiry.sndr@fluentci.io>",