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 #60 from tsirysndr/feat/mpd

add basic mpd protocol support

authored by

Tsiry Sandratraina and committed by
GitHub
13e3b25a 8ca28cbc

+1322 -57
+18 -6
Cargo.lock
··· 6071 6071 6072 6072 [[package]] 6073 6073 name = "regex" 6074 - version = "1.10.6" 6074 + version = "1.11.1" 6075 6075 source = "registry+https://github.com/rust-lang/crates.io-index" 6076 - checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 6076 + checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 6077 6077 dependencies = [ 6078 6078 "aho-corasick", 6079 6079 "memchr", ··· 6083 6083 6084 6084 [[package]] 6085 6085 name = "regex-automata" 6086 - version = "0.4.7" 6086 + version = "0.4.8" 6087 6087 source = "registry+https://github.com/rust-lang/crates.io-index" 6088 - checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 6088 + checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 6089 6089 dependencies = [ 6090 6090 "aho-corasick", 6091 6091 "memchr", ··· 6100 6100 6101 6101 [[package]] 6102 6102 name = "regex-syntax" 6103 - version = "0.8.4" 6103 + version = "0.8.5" 6104 6104 source = "registry+https://github.com/rust-lang/crates.io-index" 6105 - checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 6105 + checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 6106 6106 6107 6107 [[package]] 6108 6108 name = "reqwest" ··· 6295 6295 ] 6296 6296 6297 6297 [[package]] 6298 + name = "rockbox-mpd" 6299 + version = "0.1.0" 6300 + dependencies = [ 6301 + "anyhow", 6302 + "regex", 6303 + "rockbox-rpc", 6304 + "tokio", 6305 + "tonic", 6306 + ] 6307 + 6308 + [[package]] 6298 6309 name = "rockbox-mpris" 6299 6310 version = "0.1.0" 6300 6311 dependencies = [ ··· 6356 6367 "reqwest", 6357 6368 "rockbox-graphql", 6358 6369 "rockbox-library", 6370 + "rockbox-mpd", 6359 6371 "rockbox-mpris", 6360 6372 "rockbox-rpc", 6361 6373 "rockbox-search",
+1
Dockerfile
··· 73 73 EXPOSE 6061 74 74 EXPOSE 6062 75 75 EXPOSE 6063 76 + EXPOSE 6600 76 77 77 78 CMD ["rockboxd"]
+9 -4
README.md
··· 19 19 A modern take on the [Rockbox](https://www.rockbox.org) open-source firmware with enhancements in Zig and Rust. This project offers: 20 20 21 21 - gRPC & GraphQL APIs for seamless interaction and control 22 + - [MPD](https://mpd.readthedocs.io/en/stable/protocol.html) server for compatibility with existing clients 23 + - [MPRIS](https://specifications.freedesktop.org/mpris-spec/) support for desktop integration 22 24 - TypeScript support for building powerful extensions 23 25 24 26 Take advantage of modern tooling while preserving the core functionality of Rockbox. ··· 35 37 docker run \ 36 38 --device /dev/snd \ 37 39 --privileged \ 38 - -p 6061:6061 -p 6062:6062 -p 6063:6063 \ 40 + -p 6061:6061 \ 41 + -p 6062:6062 \ 42 + -p 6063:6063 \ 43 + -p 6600:6600 \ 39 44 -v $HOME/Music:/root/Music \ 40 45 tsiry/rockbox:latest 41 46 ``` ··· 61 66 62 67 ## 📦 Downloads 63 68 64 - - `Linux`: intel: [rockbox_2024.11.7_x86_64-linux.tar.gz](https://github.com/tsirysndr/rockbox-zig/releases/download/2024.11.7/rockbox_2024.11.7_x86_64-linux.tar.gz) arm64: [rockbox_2024.11.7_aarch64-linux.tar.gz](https://github.com/tsirysndr/rockbox-zig/releases/download/2024.11.7/rockbox_2024.11.7_aarch64-linux.tar.gz) 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) 65 70 66 71 67 72 ## ✨ Features ··· 83 88 - [ ] Stream to Chromecast 84 89 - [ ] Stream to Kodi 85 90 - [ ] TuneIn Radio 86 - - [ ] MPD Server 91 + - [x] MPD Server 87 92 - [x] MPRIS 88 - - [ ] Upnp Player 93 + - [ ] UPnP/DLNA 89 94 - [ ] Airplay 90 95 - [ ] TypeScript ([Deno](https://deno.com)) API (for writing plugins) 91 96 - [ ] Wasm extensions
+11
crates/mpd/Cargo.toml
··· 1 + [package] 2 + edition = "2021" 3 + name = "rockbox-mpd" 4 + version = "0.1.0" 5 + 6 + [dependencies] 7 + anyhow = "1.0.93" 8 + regex = "1.11.1" 9 + rockbox-rpc = {path = "../rpc"} 10 + tokio = {version = "1.36.0", features = ["full"]} 11 + tonic = "0.12.2"
+162
crates/mpd/src/handlers/batch.rs
··· 1 + use anyhow::Error; 2 + use tokio::{ 3 + io::{AsyncWriteExt, BufReader}, 4 + net::TcpStream, 5 + }; 6 + 7 + use crate::{parse_command, setup_context, Context}; 8 + 9 + use super::{ 10 + 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, 14 + }, 15 + playback::{ 16 + handle_currentsong, handle_getvol, handle_next, handle_pause, handle_play, handle_playid, 17 + handle_previous, handle_random, handle_repeat, handle_seek, handle_seekcur, handle_seekid, 18 + handle_setvol, handle_single, handle_status, handle_toggle, 19 + }, 20 + queue::{ 21 + handle_add, handle_clear, handle_delete, handle_move, handle_playlistinfo, handle_shuffle, 22 + }, 23 + }; 24 + 25 + pub async fn handle_command_list_begin( 26 + _ctx: &mut Context, 27 + request: &str, 28 + stream: &mut BufReader<TcpStream>, 29 + ) -> Result<String, Error> { 30 + let mut ctx = setup_context(true).await?; 31 + 32 + let commands: Vec<&str> = request 33 + .split("\n") 34 + .filter(|x| !vec!["command_list_begin", "command_list_end", ""].contains(x)) 35 + .collect(); 36 + 37 + let mut response = String::new(); 38 + for request in commands { 39 + 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 + _ => { 77 + println!("Unhandled command: {}", request); 78 + if !ctx.batch { 79 + stream 80 + .write_all(b"ACK [5@0] {unhandled} unknown command\n") 81 + .await?; 82 + } 83 + "ACK [5@0] {unhandled} unknown command\n".to_string() 84 + } 85 + }); 86 + } 87 + 88 + stream.write_all(response.as_bytes()).await?; 89 + 90 + Ok(response) 91 + } 92 + 93 + pub async fn handle_command_list_ok_begin( 94 + _ctx: &mut Context, 95 + request: &str, 96 + stream: &mut BufReader<TcpStream>, 97 + ) -> Result<String, Error> { 98 + let mut ctx = setup_context(true).await?; 99 + 100 + let commands: Vec<&str> = request 101 + .split("\n") 102 + .filter(|x| !vec!["command_list_ok_begin", "command_list_end", ""].contains(x)) 103 + .collect(); 104 + 105 + let mut response = String::new(); 106 + 107 + for request in commands { 108 + let command = parse_command(&request)?; 109 + 110 + response.push_str(&match command.as_str() { 111 + "play" => handle_play(&mut ctx, &request, stream).await?, 112 + "pause" => handle_pause(&mut ctx, &request, stream).await?, 113 + "toggle" => handle_toggle(&mut ctx, &request, stream).await?, 114 + "next" => handle_next(&mut ctx, &request, stream).await?, 115 + "previous" => handle_previous(&mut ctx, &request, stream).await?, 116 + "playid" => handle_playid(&mut ctx, &request, stream).await?, 117 + "seek" => handle_seek(&mut ctx, &request, stream).await?, 118 + "seekid" => handle_seekid(&mut ctx, &request, stream).await?, 119 + "seekcur" => handle_seekcur(&mut ctx, &request, stream).await?, 120 + "random" => handle_random(&mut ctx, &request, stream).await?, 121 + "repeat" => handle_repeat(&mut ctx, &request, stream).await?, 122 + "getvol" => handle_getvol(&mut ctx, &request, stream).await?, 123 + "setvol" => handle_setvol(&mut ctx, &request, stream).await?, 124 + "volume" => handle_setvol(&mut ctx, &request, stream).await?, 125 + "single" => handle_single(&mut ctx, &request, stream).await?, 126 + "shuffle" => handle_shuffle(&mut ctx, &request, stream).await?, 127 + "add" => handle_add(&mut ctx, &request, stream).await?, 128 + "playlistinfo" => handle_playlistinfo(&mut ctx, &request, stream).await?, 129 + "delete" => handle_delete(&mut ctx, &request, stream).await?, 130 + "clear" => handle_clear(&mut ctx, &request, stream).await?, 131 + "move" => handle_move(&mut ctx, &request, stream).await?, 132 + "list album" => handle_list_album(&mut ctx, &request, stream).await?, 133 + "list artist" => handle_list_artist(&mut ctx, &request, stream).await?, 134 + "list title" => handle_list_title(&mut ctx, &request, stream).await?, 135 + "update" => handle_rescan(&mut ctx, &request, stream).await?, 136 + "search" => handle_search(&mut ctx, &request, stream).await?, 137 + "rescan" => handle_rescan(&mut ctx, &request, stream).await?, 138 + "status" => handle_status(&mut ctx, &request, stream).await?, 139 + "currentsong" => handle_currentsong(&mut ctx, &request, stream).await?, 140 + "config" => handle_config(&mut ctx, &request, stream).await?, 141 + "tagtypes " => handle_tagtypes(&mut ctx, &request, stream).await?, 142 + "tagtypes clear" => handle_tagtypes_clear(&mut ctx, &request, stream).await?, 143 + "tagtypes enable" => handle_tagtypes_enable(&mut ctx, &request, stream).await?, 144 + "stats" => handle_stats(&mut ctx, &request, stream).await?, 145 + "plchanges" => handle_playlistinfo(&mut ctx, &request, stream).await?, 146 + _ => { 147 + println!("Unhandled command: {}", request); 148 + if !ctx.batch { 149 + stream 150 + .write_all(b"ACK [5@0] {unhandled} unknown command\n") 151 + .await?; 152 + } 153 + "ACK [5@0] {unhandled} unknown command\n".to_string() 154 + } 155 + }); 156 + } 157 + 158 + let mut response = response.replace("OK\n", "list_OK\n"); 159 + response.push_str("OK\n"); 160 + stream.write_all(response.as_bytes()).await?; 161 + Ok(response) 162 + }
+222
crates/mpd/src/handlers/library.rs
··· 1 + use anyhow::Error; 2 + use rockbox_rpc::api::rockbox::v1alpha1::{ 3 + GetAlbumsRequest, GetArtistsRequest, GetGlobalSettingsRequest, GetTracksRequest, 4 + ScanLibraryRequest, SearchRequest, 5 + }; 6 + use tokio::{ 7 + io::{AsyncWriteExt, BufReader}, 8 + net::TcpStream, 9 + }; 10 + 11 + use crate::Context; 12 + 13 + pub async fn handle_list_album( 14 + ctx: &mut Context, 15 + _request: &str, 16 + stream: &mut BufReader<TcpStream>, 17 + ) -> Result<String, Error> { 18 + let response = ctx.library.get_albums(GetAlbumsRequest {}).await?; 19 + let response = response.into_inner(); 20 + let response = response 21 + .albums 22 + .iter() 23 + .map(|x| format!("Album: {}\n", x.title)) 24 + .collect::<String>(); 25 + let response = format!("{}OK\n", response); 26 + 27 + if !ctx.batch { 28 + stream.write_all(response.as_bytes()).await?; 29 + } 30 + 31 + Ok(response) 32 + } 33 + 34 + pub async fn handle_list_artist( 35 + ctx: &mut Context, 36 + _request: &str, 37 + stream: &mut BufReader<TcpStream>, 38 + ) -> Result<String, Error> { 39 + let response = ctx.library.get_artists(GetArtistsRequest {}).await?; 40 + let response = response.into_inner(); 41 + let response = response 42 + .artists 43 + .iter() 44 + .map(|x| format!("Artist: {}\n", x.name)) 45 + .collect::<String>(); 46 + let response = format!("{}OK\n", response); 47 + if !ctx.batch { 48 + stream.write_all(response.as_bytes()).await?; 49 + } 50 + Ok(response) 51 + } 52 + 53 + pub async fn handle_list_title( 54 + ctx: &mut Context, 55 + _request: &str, 56 + stream: &mut BufReader<TcpStream>, 57 + ) -> Result<String, Error> { 58 + let response = ctx.library.get_tracks(GetTracksRequest {}).await?; 59 + let response = response.into_inner(); 60 + let response = response 61 + .tracks 62 + .iter() 63 + .map(|x| format!("Title: {}\n", x.title)) 64 + .collect::<String>(); 65 + let response = format!("{}OK\n", response); 66 + if !ctx.batch { 67 + stream.write_all(response.as_bytes()).await?; 68 + } 69 + Ok(response) 70 + } 71 + 72 + pub async fn handle_search( 73 + ctx: &mut Context, 74 + request: &str, 75 + stream: &mut BufReader<TcpStream>, 76 + ) -> Result<String, Error> { 77 + let term = request 78 + .replace("\"", "") 79 + .replace("search Album", "") 80 + .replace("search Artist", "") 81 + .replace("search Title", "") 82 + .replace("search album", "") 83 + .replace("search artist", "") 84 + .replace("search title", "") 85 + .trim() 86 + .to_string(); 87 + let response = ctx.library.search(SearchRequest { term }).await?; 88 + let response = response.into_inner(); 89 + 90 + let response = response 91 + .tracks 92 + .iter() 93 + .map(|x| { 94 + format!( 95 + "file: {}\nArtist: {}\nAlbum: {}\nTitle: {}\nTrack: {}\nTime: {}\nDuration: {}\n", 96 + x.path, 97 + x.artist, 98 + x.album, 99 + x.title, 100 + x.track_number, 101 + (x.length / 1000) as u32, 102 + x.length / 1000 103 + ) 104 + }) 105 + .collect::<String>(); 106 + let response = format!("{}OK\n", response); 107 + if !ctx.batch { 108 + stream.write_all(response.as_bytes()).await?; 109 + } 110 + Ok(response) 111 + } 112 + 113 + pub async fn handle_rescan( 114 + ctx: &mut Context, 115 + request: &str, 116 + stream: &mut BufReader<TcpStream>, 117 + ) -> Result<String, Error> { 118 + let response = ctx 119 + .settings 120 + .get_global_settings(GetGlobalSettingsRequest {}) 121 + .await?; 122 + let response = response.into_inner(); 123 + let path = request 124 + .replace("update ", "") 125 + .replace("rescan ", "") 126 + .replace("\"", ""); 127 + let path = Some(match path.starts_with("/") { 128 + true => path, 129 + false => format!("{}/{}", response.music_dir, path), 130 + }); 131 + ctx.library 132 + .scan_library(ScanLibraryRequest { path }) 133 + .await?; 134 + 135 + if !ctx.batch { 136 + stream.write_all(b"OK\n").await?; 137 + } 138 + Ok("OK\n".to_string()) 139 + } 140 + 141 + pub async fn handle_config( 142 + ctx: &mut Context, 143 + _request: &str, 144 + stream: &mut BufReader<TcpStream>, 145 + ) -> Result<String, Error> { 146 + let response = "ACK [4@0] {config} Command only permitted to local clients"; 147 + if !ctx.batch { 148 + stream.write_all(response.as_bytes()).await?; 149 + } 150 + 151 + Ok(response.to_string()) 152 + } 153 + 154 + pub async fn handle_tagtypes( 155 + ctx: &mut Context, 156 + _request: &str, 157 + stream: &mut BufReader<TcpStream>, 158 + ) -> Result<String, Error> { 159 + let response = format!( 160 + "Tagtype: Artist\nTagtype: Album\nTagtype: Title\nTagtype: Track\nTagtype: Date\nOK\n" 161 + ); 162 + 163 + if !ctx.batch { 164 + stream.write_all(response.as_bytes()).await?; 165 + } 166 + 167 + Ok(response) 168 + } 169 + 170 + pub async fn handle_tagtypes_clear( 171 + ctx: &mut Context, 172 + _request: &str, 173 + stream: &mut BufReader<TcpStream>, 174 + ) -> Result<String, Error> { 175 + let response = format!("OK\n"); 176 + 177 + if !ctx.batch { 178 + stream.write_all(response.as_bytes()).await?; 179 + } 180 + 181 + Ok("".to_string()) 182 + } 183 + 184 + pub async fn handle_tagtypes_enable( 185 + ctx: &mut Context, 186 + _request: &str, 187 + stream: &mut BufReader<TcpStream>, 188 + ) -> Result<String, Error> { 189 + let response = format!("OK\n"); 190 + 191 + if !ctx.batch { 192 + stream.write_all(response.as_bytes()).await?; 193 + } 194 + 195 + Ok("".to_string()) 196 + } 197 + 198 + pub async fn handle_stats( 199 + ctx: &mut Context, 200 + _request: &str, 201 + stream: &mut BufReader<TcpStream>, 202 + ) -> Result<String, Error> { 203 + let response = ctx.library.get_albums(GetAlbumsRequest {}).await?; 204 + let response = response.into_inner(); 205 + let albums = response.albums.len(); 206 + let response = ctx.library.get_artists(GetArtistsRequest {}).await?; 207 + let response = response.into_inner(); 208 + let artists = response.artists.len(); 209 + let response = ctx.library.get_tracks(GetTracksRequest {}).await?; 210 + let response = response.into_inner(); 211 + let tracks = response.tracks.len(); 212 + let response = format!( 213 + "artists: {}\nalbums: {}\nsongs: {}\nOK\n", 214 + artists, albums, tracks 215 + ); 216 + 217 + if !ctx.batch { 218 + stream.write_all(response.as_bytes()).await?; 219 + } 220 + 221 + Ok(response) 222 + }
+4
crates/mpd/src/handlers/mod.rs
··· 1 + pub mod batch; 2 + pub mod library; 3 + pub mod playback; 4 + pub mod queue;
+426
crates/mpd/src/handlers/playback.rs
··· 1 + use anyhow::Error; 2 + use rockbox_rpc::api::rockbox::v1alpha1::{ 3 + AdjustVolumeRequest, CurrentTrackRequest, GetCurrentRequest, GetGlobalSettingsRequest, 4 + NextRequest, PauseRequest, PlayRequest, PreviousRequest, ResumeRequest, SaveSettingsRequest, 5 + StatusRequest, 6 + }; 7 + use tokio::{ 8 + io::{AsyncWriteExt, BufReader}, 9 + net::TcpStream, 10 + }; 11 + 12 + use crate::Context; 13 + 14 + pub async fn handle_play( 15 + ctx: &mut Context, 16 + _request: &str, 17 + stream: &mut BufReader<TcpStream>, 18 + ) -> Result<String, Error> { 19 + ctx.playback.resume(ResumeRequest {}).await?; 20 + 21 + if !ctx.batch { 22 + stream.write_all(b"OK\n").await?; 23 + } 24 + 25 + Ok("OK\n".to_string()) 26 + } 27 + 28 + pub async fn handle_pause( 29 + ctx: &mut Context, 30 + request: &str, 31 + stream: &mut BufReader<TcpStream>, 32 + ) -> Result<String, Error> { 33 + let arg = request.split_whitespace().nth(1); 34 + match arg { 35 + Some(r#""0""#) => { 36 + 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 + } 47 + _ => { 48 + stream 49 + .write_all(b"ACK [2@0] {pause} incorrect arguments\n") 50 + .await?; 51 + } 52 + } 53 + Ok("OK\n".to_string()) 54 + } 55 + 56 + pub async fn handle_toggle( 57 + ctx: &mut Context, 58 + _request: &str, 59 + stream: &mut BufReader<TcpStream>, 60 + ) -> Result<String, Error> { 61 + let response = ctx.playback.status(StatusRequest {}).await?; 62 + let response = response.into_inner(); 63 + match response.status { 64 + 1 => { 65 + ctx.playback.pause(PauseRequest {}).await?; 66 + } 67 + 3 => { 68 + ctx.playback.resume(ResumeRequest {}).await?; 69 + } 70 + _ => { 71 + stream 72 + .write_all(b"ACK [2@0] {toggle} no song is playing\n") 73 + .await?; 74 + } 75 + } 76 + if !ctx.batch { 77 + stream.write_all(b"OK\n").await?; 78 + } 79 + Ok("OK\n".to_string()) 80 + } 81 + 82 + pub async fn handle_status( 83 + ctx: &mut Context, 84 + _request: &str, 85 + stream: &mut BufReader<TcpStream>, 86 + ) -> 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", 92 + _ => "stop", 93 + }; 94 + 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 { 101 + 0 => 0, 102 + 1 => 1, 103 + 2 => 1, 104 + _ => 0, 105 + }; 106 + 107 + let random = match response.playlist_shuffle { 108 + true => 1, 109 + false => 0, 110 + }; 111 + 112 + let volume = response.volume; 113 + // volume is between -80 db and 0 db 114 + // we need to convert it to 0-100 115 + // -80 db is 0 116 + // 0 db is 100 117 + let volume = ((volume + 80) * 100 / 80).max(0).min(100); 118 + 119 + let response = ctx.playback.current_track(CurrentTrackRequest {}).await?; 120 + let response = response.into_inner(); 121 + 122 + let time = format!( 123 + "{}:{}", 124 + (response.elapsed / 1000) as i64, 125 + (response.length / 1000) as i64 126 + ); 127 + let elapsed = (response.elapsed / 1000) as i64; 128 + 129 + let single = ctx.single.lock().await; 130 + let single = single.as_str().replace("\"", ""); 131 + let bitrate = response.bitrate; 132 + let audio = format!("{}:16:2", response.frequency); 133 + 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; 138 + 139 + 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, 142 + ); 143 + 144 + if !ctx.batch { 145 + stream.write_all(response.as_bytes()).await?; 146 + } 147 + Ok(response) 148 + } 149 + 150 + pub async fn handle_next( 151 + ctx: &mut Context, 152 + _request: &str, 153 + stream: &mut BufReader<TcpStream>, 154 + ) -> Result<String, Error> { 155 + ctx.playback.next(NextRequest {}).await?; 156 + if !ctx.batch { 157 + stream.write_all(b"OK\n").await?; 158 + } 159 + Ok("OK\n".to_string()) 160 + } 161 + 162 + pub async fn handle_previous( 163 + ctx: &mut Context, 164 + _request: &str, 165 + stream: &mut BufReader<TcpStream>, 166 + ) -> Result<String, Error> { 167 + ctx.playback.previous(PreviousRequest {}).await?; 168 + 169 + if !ctx.batch { 170 + stream.write_all(b"OK\n").await?; 171 + } 172 + 173 + Ok("OK\n".to_string()) 174 + } 175 + 176 + pub async fn handle_playid( 177 + ctx: &mut Context, 178 + request: &str, 179 + stream: &mut BufReader<TcpStream>, 180 + ) -> Result<String, Error> { 181 + println!("{}", request); 182 + 183 + if !ctx.batch { 184 + stream.write_all(b"OK\n").await?; 185 + } 186 + 187 + Ok("OK\n".to_string()) 188 + } 189 + 190 + pub async fn handle_seek( 191 + ctx: &mut Context, 192 + request: &str, 193 + stream: &mut BufReader<TcpStream>, 194 + ) -> Result<String, Error> { 195 + println!("{}", request); 196 + 197 + if !ctx.batch { 198 + stream.write_all(b"OK\n").await?; 199 + } 200 + 201 + Ok("OK\n".to_string()) 202 + } 203 + 204 + pub async fn handle_seekid( 205 + ctx: &mut Context, 206 + request: &str, 207 + stream: &mut BufReader<TcpStream>, 208 + ) -> Result<String, Error> { 209 + println!("{}", request); 210 + 211 + if !ctx.batch { 212 + stream.write_all(b"OK\n").await?; 213 + } 214 + 215 + Ok("OK\n".to_string()) 216 + } 217 + 218 + pub async fn handle_seekcur( 219 + ctx: &mut Context, 220 + request: &str, 221 + stream: &mut BufReader<TcpStream>, 222 + ) -> Result<String, Error> { 223 + let arg = request.split_whitespace().nth(1); 224 + if arg.is_none() { 225 + stream 226 + .write_all(b"ACK [2@0] {seekcur} incorrect arguments\n") 227 + .await?; 228 + return Ok("ACK [2@0] {seekcur} incorrect arguments\n".to_string()); 229 + } 230 + 231 + ctx.playback 232 + .play(PlayRequest { 233 + elapsed: arg 234 + .map(|x| x.replace("\"", "")) 235 + .map(|x| x.parse::<i64>().unwrap() * 1000) 236 + .unwrap_or_default(), 237 + offset: 0, 238 + }) 239 + .await?; 240 + if !ctx.batch { 241 + stream.write_all(b"OK\n").await?; 242 + } 243 + Ok("OK\n".to_string()) 244 + } 245 + 246 + pub async fn handle_random( 247 + ctx: &mut Context, 248 + request: &str, 249 + stream: &mut BufReader<TcpStream>, 250 + ) -> Result<String, Error> { 251 + let arg = request.split_whitespace().nth(1); 252 + if arg.is_none() { 253 + if !ctx.batch { 254 + stream 255 + .write_all(b"ACK [2@0] {random} incorrect arguments\n") 256 + .await?; 257 + } 258 + return Ok("ACK [2@0] {random} incorrect arguments\n".to_string()); 259 + } 260 + 261 + ctx.settings 262 + .save_settings(SaveSettingsRequest { 263 + playlist_shuffle: Some(arg.unwrap() == r#""1""#), 264 + ..Default::default() 265 + }) 266 + .await?; 267 + if !ctx.batch { 268 + stream.write_all(b"OK\n").await?; 269 + } 270 + Ok("OK\n".to_string()) 271 + } 272 + 273 + pub async fn handle_repeat( 274 + ctx: &mut Context, 275 + request: &str, 276 + stream: &mut BufReader<TcpStream>, 277 + ) -> Result<String, Error> { 278 + let arg = request.split_whitespace().nth(1); 279 + if arg.is_none() { 280 + if !ctx.batch { 281 + stream 282 + .write_all(b"ACK [2@0] {repeat} incorrect arguments\n") 283 + .await?; 284 + } 285 + return Ok("ACK [2@0] {repeat} incorrect arguments\n".to_string()); 286 + } 287 + 288 + let single = ctx.single.lock().await; 289 + 290 + let repeat_mode = match arg.unwrap() { 291 + r#""0""# => Some(0), 292 + r#""1""# => match single.as_str() { 293 + r#""1""# => Some(2), 294 + _ => Some(1), 295 + }, 296 + _ => { 297 + if !ctx.batch { 298 + stream 299 + .write_all(b"ACK [2@0] {repeat} incorrect arguments\n") 300 + .await?; 301 + } 302 + return Ok("ACK [2@0] {repeat} incorrect arguments\n".to_string()); 303 + } 304 + }; 305 + ctx.settings 306 + .save_settings(SaveSettingsRequest { 307 + repeat_mode, 308 + ..Default::default() 309 + }) 310 + .await?; 311 + if !ctx.batch { 312 + stream.write_all(b"OK\n").await?; 313 + } 314 + Ok("OK\n".to_string()) 315 + } 316 + 317 + pub async fn handle_getvol( 318 + ctx: &mut Context, 319 + _request: &str, 320 + stream: &mut BufReader<TcpStream>, 321 + ) -> 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; 328 + // volume is between -80 db and 0 db 329 + // we need to convert it to 0-100 330 + // -80 db is 0 331 + // 0 db is 100 332 + let volume = ((volume + 80) * 100 / 80).max(0).min(100); 333 + let response = format!("volume: {}\nOK\n", volume); 334 + 335 + if !ctx.batch { 336 + stream.write_all(response.as_bytes()).await?; 337 + } 338 + 339 + Ok(response) 340 + } 341 + 342 + pub async fn handle_setvol( 343 + ctx: &mut Context, 344 + request: &str, 345 + stream: &mut BufReader<TcpStream>, 346 + ) -> 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; 353 + let arg = request.split_whitespace().nth(1); 354 + if arg.is_none() { 355 + if !ctx.batch { 356 + stream 357 + .write_all(b"ACK [2@0] {setvol} incorrect arguments\n") 358 + .await?; 359 + } 360 + return Ok("ACK [2@0] {setvol} incorrect arguments\n".to_string()); 361 + } 362 + 363 + let new_volume = arg.unwrap().replace("\"", "").parse::<i64>().unwrap(); 364 + // volume is between 0 and 100 365 + // we need to convert it to -80 db to 0 db 366 + // 0 is -80 db 367 + // 100 is 0 db 368 + let new_volume = ((new_volume * 80 / 100) - 80) as i32; 369 + let steps = new_volume - volume; 370 + ctx.sound 371 + .adjust_volume(AdjustVolumeRequest { steps }) 372 + .await?; 373 + if !ctx.batch { 374 + stream.write_all(b"OK\n").await?; 375 + } 376 + Ok("OK\n".to_string()) 377 + } 378 + 379 + pub async fn handle_single( 380 + ctx: &mut Context, 381 + request: &str, 382 + stream: &mut BufReader<TcpStream>, 383 + ) -> Result<String, Error> { 384 + let arg = request.split_whitespace().nth(1); 385 + if arg.is_none() { 386 + if !ctx.batch { 387 + stream 388 + .write_all(b"ACK [2@0] {single} incorrect arguments\n") 389 + .await?; 390 + } 391 + return Ok("ACK [2@0] {single} incorrect arguments\n".to_string()); 392 + } 393 + 394 + let mut single = ctx.single.lock().await; 395 + *single = arg.unwrap().to_string(); 396 + if !ctx.batch { 397 + stream.write_all(b"OK\n").await?; 398 + } 399 + Ok("OK\n".to_string()) 400 + } 401 + 402 + pub async fn handle_currentsong( 403 + ctx: &mut Context, 404 + _request: &str, 405 + stream: &mut BufReader<TcpStream>, 406 + ) -> 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(); 411 + let response = format!( 412 + "file: {}\nTitle: {}\nArtist: {}\nAlbum: {}\nTrack: {}\nDate: {}\nTime: {}\nPos: {}\nOK\n", 413 + current.path, 414 + current.title, 415 + current.artist, 416 + current.album, 417 + current.tracknum, 418 + current.year, 419 + (current.elapsed / 1000) as i64, 420 + current_playlist.index, 421 + ); 422 + if !ctx.batch { 423 + stream.write_all(response.as_bytes()).await?; 424 + } 425 + Ok(response) 426 + }
+217
crates/mpd/src/handlers/queue.rs
··· 1 + use std::fs; 2 + 3 + use crate::{Context, PLAYLIST_INSERT_LAST}; 4 + use anyhow::Error; 5 + use regex::Regex; 6 + use rockbox_rpc::api::rockbox::v1alpha1::{ 7 + GetCurrentRequest, GetGlobalSettingsRequest, InsertDirectoryRequest, InsertTracksRequest, 8 + RemoveAllTracksRequest, RemoveTracksRequest, ShufflePlaylistRequest, 9 + }; 10 + use tokio::{ 11 + io::{AsyncWriteExt, BufReader}, 12 + net::TcpStream, 13 + }; 14 + 15 + pub async fn handle_shuffle( 16 + ctx: &mut Context, 17 + _request: &str, 18 + stream: &mut BufReader<TcpStream>, 19 + ) -> Result<String, Error> { 20 + ctx.playlist 21 + .shuffle_playlist(ShufflePlaylistRequest { start_index: 0 }) 22 + .await?; 23 + if !ctx.batch { 24 + stream.write_all(b"OK\n").await?; 25 + } 26 + Ok("OK\n".to_string()) 27 + } 28 + 29 + pub async fn handle_add( 30 + ctx: &mut Context, 31 + request: &str, 32 + stream: &mut BufReader<TcpStream>, 33 + ) -> Result<String, Error> { 34 + let response = ctx 35 + .settings 36 + .get_global_settings(GetGlobalSettingsRequest {}) 37 + .await?; 38 + let response = response.into_inner(); 39 + let music_dir = response.music_dir; 40 + 41 + let re = Regex::new(r#"^(\w+)\s+"([^"]+)"(?:\s+"?(-?\d+)"?)?$"#).unwrap(); 42 + let captures = re.captures(request); 43 + if captures.is_none() { 44 + if !ctx.batch { 45 + stream 46 + .write_all(b"ACK [2@0] {add} missing argument\n") 47 + .await?; 48 + } 49 + return Ok("ACK [2@0] {add} missing argument\n".to_string()); 50 + } 51 + let captures = captures.unwrap(); 52 + 53 + let path = captures.get(2).unwrap().as_str().to_string(); 54 + let position = captures 55 + .get(3) 56 + .map(|x| x.as_str().parse::<i32>().unwrap_or(PLAYLIST_INSERT_LAST)) 57 + .unwrap_or(PLAYLIST_INSERT_LAST); 58 + 59 + if path.is_empty() { 60 + if !ctx.batch { 61 + stream 62 + .write_all(b"ACK [2@0] {add} missing argument\n") 63 + .await?; 64 + } 65 + return Ok("ACK [2@0] {add} missing argument\n".to_string()); 66 + } 67 + 68 + let path = match path.starts_with('/') { 69 + true => path, 70 + false => format!("{}/{}", music_dir, path), 71 + }; 72 + 73 + // verify if path is a file or directory or doesn't exist 74 + if fs::metadata(&path).is_err() { 75 + if !ctx.batch { 76 + stream 77 + .write_all(b"ACK [50@0] {add} No such file or directory\n") 78 + .await?; 79 + } 80 + return Ok("ACK [50@0] {add} No such file or directory\n".to_string()); 81 + } 82 + 83 + if fs::metadata(&path)?.is_file() { 84 + ctx.playlist 85 + .insert_tracks(InsertTracksRequest { 86 + tracks: vec![path.clone()], 87 + position, 88 + ..Default::default() 89 + }) 90 + .await?; 91 + } 92 + 93 + if fs::metadata(&path)?.is_dir() { 94 + ctx.playlist 95 + .insert_directory(InsertDirectoryRequest { 96 + directory: path, 97 + position, 98 + ..Default::default() 99 + }) 100 + .await?; 101 + } 102 + 103 + if !ctx.batch { 104 + stream.write_all(b"OK\n").await?; 105 + } 106 + Ok("OK\n".to_string()) 107 + } 108 + 109 + pub async fn handle_playlistinfo( 110 + ctx: &mut Context, 111 + _request: &str, 112 + stream: &mut BufReader<TcpStream>, 113 + ) -> Result<String, Error> { 114 + let response = ctx.playlist.get_current(GetCurrentRequest {}).await?; 115 + let response = response.into_inner(); 116 + let mut index = -1; 117 + let response = response 118 + .tracks 119 + .iter() 120 + .map(|x| { 121 + index += 1; 122 + format!( 123 + "file: {}\nTitle: {}\nArtist: {}\nAlbum: {}\nTime: {}\nPos: {}\n", 124 + x.path, 125 + x.title, 126 + x.artist, 127 + x.album, 128 + (x.length / 1000) as u32, 129 + index, 130 + ) 131 + }) 132 + .collect::<String>(); 133 + let response = format!("{}OK\n", response); 134 + 135 + if !ctx.batch { 136 + stream.write_all(response.as_bytes()).await?; 137 + } 138 + 139 + Ok(response) 140 + } 141 + 142 + pub async fn handle_delete( 143 + ctx: &mut Context, 144 + request: &str, 145 + stream: &mut BufReader<TcpStream>, 146 + ) -> Result<String, Error> { 147 + let request = request.replace("\"", ""); 148 + let arg = request.split_whitespace().last(); 149 + if arg.is_none() { 150 + if !ctx.batch { 151 + stream 152 + .write_all(b"ACK [2@0] {delete} missing argument\n") 153 + .await?; 154 + } 155 + return Ok("ACK [2@0] {delete} missing argument\n".to_string()); 156 + } 157 + if arg.unwrap().contains(':') { 158 + // get the range 159 + let range: Vec<i32> = arg 160 + .unwrap() 161 + .split(':') 162 + .map(|x| x.parse::<i32>().unwrap()) 163 + .collect(); 164 + let positions: Vec<i32> = (range[0]..=range[1]).collect(); 165 + ctx.playlist 166 + .remove_tracks(RemoveTracksRequest { positions }) 167 + .await?; 168 + if !ctx.batch { 169 + stream.write_all(b"OK\n").await?; 170 + } 171 + return Ok("OK\n".to_string()); 172 + } 173 + let positions = match arg.unwrap().parse::<i32>() { 174 + Ok(x) => vec![x], 175 + Err(_) => { 176 + if !ctx.batch { 177 + stream 178 + .write_all(b"ACK [2@0] {delete} invalid argument\n") 179 + .await?; 180 + } 181 + return Ok("ACK [2@0] {delete} invalid argument\n".to_string()); 182 + } 183 + }; 184 + ctx.playlist 185 + .remove_tracks(RemoveTracksRequest { positions }) 186 + .await?; 187 + if !ctx.batch { 188 + stream.write_all(b"OK\n").await?; 189 + } 190 + Ok("OK\n".to_string()) 191 + } 192 + 193 + pub async fn handle_clear( 194 + ctx: &mut Context, 195 + _request: &str, 196 + stream: &mut BufReader<TcpStream>, 197 + ) -> Result<String, Error> { 198 + ctx.playlist 199 + .remove_all_tracks(RemoveAllTracksRequest { positions: vec![] }) 200 + .await?; 201 + if !ctx.batch { 202 + stream.write_all(b"OK\n").await?; 203 + } 204 + Ok("OK\n".to_string()) 205 + } 206 + 207 + pub async fn handle_move( 208 + ctx: &mut Context, 209 + request: &str, 210 + stream: &mut BufReader<TcpStream>, 211 + ) -> Result<String, Error> { 212 + println!("{}", request); 213 + if !ctx.batch { 214 + stream.write_all(b"OK\n").await?; 215 + } 216 + Ok("OK\n".to_string()) 217 + }
+175
crates/mpd/src/lib.rs
··· 1 + use std::{env, sync::Arc}; 2 + 3 + use anyhow::Error; 4 + use handlers::{ 5 + batch::{handle_command_list_begin, handle_command_list_ok_begin}, 6 + 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, 9 + }, 10 + playback::{ 11 + handle_currentsong, handle_getvol, handle_next, handle_pause, handle_play, handle_playid, 12 + handle_previous, handle_random, handle_repeat, handle_seek, handle_seekcur, handle_seekid, 13 + handle_setvol, handle_single, handle_status, handle_toggle, 14 + }, 15 + queue::{ 16 + handle_add, handle_clear, handle_delete, handle_move, handle_playlistinfo, handle_shuffle, 17 + }, 18 + }; 19 + use rockbox_rpc::api::rockbox::v1alpha1::{ 20 + library_service_client::LibraryServiceClient, playback_service_client::PlaybackServiceClient, 21 + playlist_service_client::PlaylistServiceClient, settings_service_client::SettingsServiceClient, 22 + sound_service_client::SoundServiceClient, 23 + }; 24 + use tokio::{ 25 + io::{AsyncReadExt, AsyncWriteExt}, 26 + net::{TcpListener, TcpStream}, 27 + sync::Mutex, 28 + }; 29 + use tonic::transport::Channel; 30 + 31 + pub const PLAYLIST_INSERT_FIRST: i32 = -4; 32 + pub const PLAYLIST_INSERT_LAST: i32 = -3; 33 + 34 + pub mod handlers; 35 + 36 + #[derive(Clone)] 37 + pub struct Context { 38 + pub library: LibraryServiceClient<Channel>, 39 + pub playback: PlaybackServiceClient<Channel>, 40 + pub settings: SettingsServiceClient<Channel>, 41 + pub sound: SoundServiceClient<Channel>, 42 + pub playlist: PlaylistServiceClient<Channel>, 43 + pub single: Arc<Mutex<String>>, 44 + pub batch: bool, 45 + } 46 + 47 + pub struct MpdServer {} 48 + 49 + impl MpdServer { 50 + pub async fn start() -> Result<(), Error> { 51 + let port = env::var("ROCKBOX_MPD_PORT").unwrap_or_else(|_| "6600".to_string()); 52 + let addr = format!("0.0.0.0:{}", port); 53 + let context = setup_context(false).await?; 54 + 55 + let listener = TcpListener::bind(&addr).await?; 56 + 57 + loop { 58 + let (stream, _) = listener.accept().await?; 59 + let context = context.clone(); 60 + tokio::spawn(async move { 61 + match handle_client(context, stream).await { 62 + Ok(_) => {} 63 + Err(e) => { 64 + eprintln!("Error: {}", e); 65 + } 66 + } 67 + }); 68 + } 69 + } 70 + } 71 + 72 + pub async fn handle_client(mut ctx: Context, stream: TcpStream) -> Result<(), Error> { 73 + let mut buf = [0; 4096]; 74 + let mut stream = tokio::io::BufReader::new(stream); 75 + stream.write_all(b"OK MPD 0.23.15\n").await?; 76 + 77 + while let Ok(n) = stream.read(&mut buf).await { 78 + if n == 0 { 79 + break; 80 + } 81 + let request = String::from_utf8_lossy(&buf[..n]); 82 + let command = parse_command(&request)?; 83 + 84 + match command.as_str() { 85 + "play" => handle_play(&mut ctx, &request, &mut stream).await?, 86 + "pause" => handle_pause(&mut ctx, &request, &mut stream).await?, 87 + "toggle" => handle_toggle(&mut ctx, &request, &mut stream).await?, 88 + "next" => handle_next(&mut ctx, &request, &mut stream).await?, 89 + "previous" => handle_previous(&mut ctx, &request, &mut stream).await?, 90 + "playid" => handle_playid(&mut ctx, &request, &mut stream).await?, 91 + "seek" => handle_seek(&mut ctx, &request, &mut stream).await?, 92 + "seekid" => handle_seekid(&mut ctx, &request, &mut stream).await?, 93 + "seekcur" => handle_seekcur(&mut ctx, &request, &mut stream).await?, 94 + "random" => handle_random(&mut ctx, &request, &mut stream).await?, 95 + "repeat" => handle_repeat(&mut ctx, &request, &mut stream).await?, 96 + "getvol" => handle_getvol(&mut ctx, &request, &mut stream).await?, 97 + "setvol" => handle_setvol(&mut ctx, &request, &mut stream).await?, 98 + "volume" => handle_setvol(&mut ctx, &request, &mut stream).await?, 99 + "single" => handle_single(&mut ctx, &request, &mut stream).await?, 100 + "shuffle" => handle_shuffle(&mut ctx, &request, &mut stream).await?, 101 + "add" => handle_add(&mut ctx, &request, &mut stream).await?, 102 + "playlistinfo" => handle_playlistinfo(&mut ctx, &request, &mut stream).await?, 103 + "delete" => handle_delete(&mut ctx, &request, &mut stream).await?, 104 + "clear" => handle_clear(&mut ctx, &request, &mut stream).await?, 105 + "move" => handle_move(&mut ctx, &request, &mut stream).await?, 106 + "list album" => handle_list_album(&mut ctx, &request, &mut stream).await?, 107 + "list artist" => handle_list_artist(&mut ctx, &request, &mut stream).await?, 108 + "list title" => handle_list_title(&mut ctx, &request, &mut stream).await?, 109 + "update" => handle_rescan(&mut ctx, &request, &mut stream).await?, 110 + "search" => handle_search(&mut ctx, &request, &mut stream).await?, 111 + "rescan" => handle_rescan(&mut ctx, &request, &mut stream).await?, 112 + "status" => handle_status(&mut ctx, &request, &mut stream).await?, 113 + "currentsong" => handle_currentsong(&mut ctx, &request, &mut stream).await?, 114 + "config" => handle_config(&mut ctx, &request, &mut stream).await?, 115 + "tagtypes " => handle_tagtypes(&mut ctx, &request, &mut stream).await?, 116 + "tagtypes clear" => handle_clear(&mut ctx, &request, &mut stream).await?, 117 + "tagtypes enable" => handle_tagtypes_enable(&mut ctx, &request, &mut stream).await?, 118 + "stats" => handle_stats(&mut ctx, &request, &mut stream).await?, 119 + "plchanges" => handle_playlistinfo(&mut ctx, &request, &mut stream).await?, 120 + "command_list_begin" => { 121 + handle_command_list_begin(&mut ctx, &request, &mut stream).await? 122 + } 123 + "command_list_ok_begin" => { 124 + handle_command_list_ok_begin(&mut ctx, &request, &mut stream).await? 125 + } 126 + _ => { 127 + println!("Unhandled command: {}", request); 128 + stream 129 + .write_all(b"ACK [5@0] {unhandled} unknown command\n") 130 + .await?; 131 + "ACK [5@0] {unhandled} unknown command\n".to_string() 132 + } 133 + }; 134 + } 135 + Ok(()) 136 + } 137 + 138 + fn parse_command(request: &str) -> Result<String, Error> { 139 + let command = request.split_whitespace().next().unwrap_or_default(); 140 + 141 + if command == "list" { 142 + // should parse the next word, and return "list album" or "list artist" or "list title" 143 + let r#type = request.split_whitespace().nth(1).unwrap_or_default(); 144 + return Ok(format!("list {}", r#type.to_lowercase())); 145 + } 146 + 147 + if command == "tagtypes" { 148 + let r#type = request.split_whitespace().nth(1).unwrap_or_default(); 149 + return Ok(format!("tagtypes {}", r#type.replace("\"", ""))); 150 + } 151 + 152 + Ok(command.to_string()) 153 + } 154 + 155 + pub async fn setup_context(batch: bool) -> Result<Context, Error> { 156 + let port = env::var("ROCKBOX_PORT").unwrap_or_else(|_| "6061".to_string()); 157 + let host = env::var("ROCKBOX_HOST").unwrap_or_else(|_| "localhost".to_string()); 158 + let url = format!("tcp://{}:{}", host, port); 159 + 160 + let library = LibraryServiceClient::connect(url.clone()).await?; 161 + let playback = PlaybackServiceClient::connect(url.clone()).await?; 162 + let settings = SettingsServiceClient::connect(url.clone()).await?; 163 + let sound = SoundServiceClient::connect(url.clone()).await?; 164 + let playlist = PlaylistServiceClient::connect(url.clone()).await?; 165 + 166 + Ok(Context { 167 + library, 168 + playback, 169 + settings, 170 + sound, 171 + playlist, 172 + single: Arc::new(Mutex::new("\"0\"".to_string())), 173 + batch, 174 + }) 175 + }
+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
crates/rpc/src/playback.rs
··· 1 1 use std::{ 2 2 fs, 3 3 sync::{mpsc::Sender, Arc, Mutex}, 4 - thread, 5 4 }; 6 5 7 6 use crate::{
+1
crates/server/Cargo.toml
··· 16 16 reqwest = {version = "0.12.7", features = ["blocking", "rustls-tls"], default-features = false} 17 17 rockbox-graphql = {path = "../graphql"} 18 18 rockbox-library = {path = "../library"} 19 + rockbox-mpd = {path = "../mpd"} 19 20 rockbox-mpris = {path = "../mpris"} 20 21 rockbox-rpc = {path = "../rpc"} 21 22 rockbox-search = {path = "../search"}
+14
crates/server/src/lib.rs
··· 6 6 simplebroker::SimpleBroker, 7 7 }; 8 8 use rockbox_library::repo; 9 + use rockbox_mpd::MpdServer; 9 10 use rockbox_mpris::MprisServer; 10 11 use rockbox_sys::events::RockboxCommand; 11 12 use rockbox_sys::{self as rb, types::mp3_entry::Mp3Entry}; ··· 212 213 } 213 214 }, 214 215 ); 216 + 217 + thread::spawn(move || { 218 + let runtime = tokio::runtime::Builder::new_current_thread() 219 + .enable_all() 220 + .build() 221 + .unwrap(); 222 + match runtime.block_on(MpdServer::start()) { 223 + Ok(_) => {} 224 + Err(e) => { 225 + eprintln!("Error starting mpd server: {}", e); 226 + } 227 + } 228 + }); 215 229 } 216 230 217 231 #[no_mangle]
+1 -1
webui/rockbox/package.json
··· 1 1 { 2 2 "name": "rockbox", 3 3 "private": true, 4 - "version": "2024.11.7", 4 + "version": "2024.11.10", 5 5 "type": "module", 6 6 "main": "dist-electron/main/index.js", 7 7 "author": "Tsiry Sandratraina <tsiry.sndr@fluentci.io>",
+3
webui/rockbox/src/Components/ControlBar/ControlBarWithData.tsx
··· 25 25 import { useResumePlaylist } from "../../Hooks/useResumePlaylist"; 26 26 import { likesState } from "../Likes/LikesState"; 27 27 import { settingsState } from "../Settings/SettingsState"; 28 + import { useSettings } from "../../Hooks/useSettings"; 28 29 29 30 const ControlBarWithData: FC = () => { 30 31 const [{ nowPlaying, locked, resumeIndex }, setControlBarState] = ··· 57 58 useGetLikedAlbumsQuery({ 58 59 fetchPolicy: "network-only", 59 60 }); 61 + 62 + useSettings(); 60 63 61 64 useEffect(() => { 62 65 if (
+3 -45
webui/rockbox/src/Components/Settings/SettingsWithData.tsx
··· 1 - import { FC, useEffect } from "react"; 1 + import { FC } from "react"; 2 2 import Settings from "./Settings"; 3 - import { useGetGlobalSettingsQuery } from "../../Hooks/GraphQL"; 4 - import { useRecoilState } from "recoil"; 5 - import { settingsState } from "./SettingsState"; 3 + import { useSettings } from "../../Hooks/useSettings"; 6 4 7 5 const SettingsWithData: FC = () => { 8 - const [, setSettings] = useRecoilState(settingsState); 9 - const { data, loading } = useGetGlobalSettingsQuery(); 10 - 11 - useEffect(() => { 12 - if (!data || loading) { 13 - return; 14 - } 15 - setSettings((state) => ({ 16 - ...state, 17 - eqEnabled: data.globalSettings.eqEnabled, 18 - eqBandSettings: data.globalSettings.eqBandSettings, 19 - volume: data.globalSettings.volume, 20 - bass: data.globalSettings.bass, 21 - bassCutoff: data.globalSettings.bassCutoff, 22 - treble: data.globalSettings.treble, 23 - trebleCutoff: data.globalSettings.trebleCutoff, 24 - playlistShuffle: data.globalSettings.playlistShuffle, 25 - repeatMode: data.globalSettings.repeatMode, 26 - replaygainSettings: data.globalSettings.replaygainSettings, 27 - playerName: data.globalSettings.playerName, 28 - partyMode: data.globalSettings.partyMode, 29 - ditheringEnabled: data.globalSettings.ditheringEnabled, 30 - channelConfig: data.globalSettings.channelConfig, 31 - balance: data.globalSettings.balance, 32 - fadeOnStop: data.globalSettings.fadeOnStop, 33 - crossfade: data.globalSettings.crossfade, 34 - crossfadeFadeInDelay: data.globalSettings.crossfadeFadeInDelay, 35 - crossfadeFadeInDuration: data.globalSettings.crossfadeFadeInDuration, 36 - crossfadeFadeOutDelay: data.globalSettings.crossfadeFadeOutDelay, 37 - crossfadeFadeOutDuration: data.globalSettings.crossfadeFadeOutDuration, 38 - crossfadeFadeOutMixmode: data.globalSettings.crossfadeFadeOutMixmode, 39 - stereoWidth: data.globalSettings.stereoWidth, 40 - stereoswMode: data.globalSettings.stereoswMode, 41 - surroundEnabled: data.globalSettings.surroundEnabled, 42 - surroundBalance: data.globalSettings.surroundBalance, 43 - surroundFx1: data.globalSettings.surroundFx1, 44 - surroundFx2: data.globalSettings.surroundFx2, 45 - })); 46 - // eslint-disable-next-line react-hooks/exhaustive-deps 47 - }, [data, loading]); 48 - 6 + useSettings(); 49 7 return <Settings />; 50 8 }; 51 9
+47
webui/rockbox/src/Hooks/useSettings.tsx
··· 1 + import { useRecoilState } from "recoil"; 2 + import { settingsState } from "../Components/Settings/SettingsState"; 3 + import { useGetGlobalSettingsQuery } from "./GraphQL"; 4 + import { useEffect } from "react"; 5 + 6 + export const useSettings = () => { 7 + const [, setSettings] = useRecoilState(settingsState); 8 + const { data, loading } = useGetGlobalSettingsQuery(); 9 + 10 + useEffect(() => { 11 + if (!data || loading) { 12 + return; 13 + } 14 + setSettings((state) => ({ 15 + ...state, 16 + eqEnabled: data.globalSettings.eqEnabled, 17 + eqBandSettings: data.globalSettings.eqBandSettings, 18 + volume: data.globalSettings.volume, 19 + bass: data.globalSettings.bass, 20 + bassCutoff: data.globalSettings.bassCutoff, 21 + treble: data.globalSettings.treble, 22 + trebleCutoff: data.globalSettings.trebleCutoff, 23 + playlistShuffle: data.globalSettings.playlistShuffle, 24 + repeatMode: data.globalSettings.repeatMode, 25 + replaygainSettings: data.globalSettings.replaygainSettings, 26 + playerName: data.globalSettings.playerName, 27 + partyMode: data.globalSettings.partyMode, 28 + ditheringEnabled: data.globalSettings.ditheringEnabled, 29 + channelConfig: data.globalSettings.channelConfig, 30 + balance: data.globalSettings.balance, 31 + fadeOnStop: data.globalSettings.fadeOnStop, 32 + crossfade: data.globalSettings.crossfade, 33 + crossfadeFadeInDelay: data.globalSettings.crossfadeFadeInDelay, 34 + crossfadeFadeInDuration: data.globalSettings.crossfadeFadeInDuration, 35 + crossfadeFadeOutDelay: data.globalSettings.crossfadeFadeOutDelay, 36 + crossfadeFadeOutDuration: data.globalSettings.crossfadeFadeOutDuration, 37 + crossfadeFadeOutMixmode: data.globalSettings.crossfadeFadeOutMixmode, 38 + stereoWidth: data.globalSettings.stereoWidth, 39 + stereoswMode: data.globalSettings.stereoswMode, 40 + surroundEnabled: data.globalSettings.surroundEnabled, 41 + surroundBalance: data.globalSettings.surroundBalance, 42 + surroundFx1: data.globalSettings.surroundFx1, 43 + surroundFx2: data.globalSettings.surroundFx2, 44 + })); 45 + // eslint-disable-next-line react-hooks/exhaustive-deps 46 + }, [data, loading]); 47 + };