A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 395 lines 14 kB view raw
1use crate::api::rockbox::v1alpha1::browse_service_client::BrowseServiceClient; 2use crate::api::rockbox::v1alpha1::playback_service_client::PlaybackServiceClient; 3use crate::api::rockbox::v1alpha1::playlist_service_client::PlaylistServiceClient; 4use crate::api::rockbox::v1alpha1::{ 5 InsertDirectoryRequest, InsertTracksRequest, PlayDirectoryRequest, PlayTrackRequest, 6 TreeGetEntriesRequest, TreeGetEntriesResponse, 7}; 8use crate::constants::*; 9use crate::state::AppState; 10use adw::prelude::*; 11use adw::subclass::prelude::*; 12use anyhow::Error; 13use glib::subclass; 14use gtk::glib; 15use gtk::{CompositeTemplate, Image, Label, ListBox, MenuButton}; 16use std::cell::{Cell, RefCell}; 17use std::{env, thread}; 18 19mod imp { 20 21 use super::*; 22 23 #[derive(Debug, Default, CompositeTemplate)] 24 #[template(resource = "/io/github/tsirysndr/Rockbox/gtk/file.ui")] 25 pub struct File { 26 #[template_child] 27 pub file_icon: TemplateChild<Image>, 28 #[template_child] 29 pub file_name: TemplateChild<Label>, 30 #[template_child] 31 pub row: TemplateChild<gtk::Box>, 32 #[template_child] 33 pub file_menu: TemplateChild<MenuButton>, 34 #[template_child] 35 pub directory_menu: TemplateChild<MenuButton>, 36 37 pub files: RefCell<Option<ListBox>>, 38 pub go_back_button: RefCell<Option<gtk::Button>>, 39 pub path: RefCell<String>, 40 pub is_dir: Cell<bool>, 41 pub state: glib::WeakRef<AppState>, 42 } 43 44 #[glib::object_subclass] 45 impl ObjectSubclass for File { 46 const NAME: &'static str = "File"; 47 type ParentType = gtk::Box; 48 type Type = super::File; 49 50 fn class_init(klass: &mut Self::Class) { 51 Self::bind_template(klass); 52 53 klass.install_action("app.dir.play-next", None, move |file, _action, _target| { 54 file.play_next(); 55 }); 56 57 klass.install_action("app.dir.play-last", None, move |file, _action, _target| { 58 file.play_last(); 59 }); 60 61 klass.install_action( 62 "app.dir.add-shuffled", 63 None, 64 move |file, _action, _target| { 65 file.add_shuffled(); 66 }, 67 ); 68 69 klass.install_action( 70 "app.dir.play-last-shuffled", 71 None, 72 move |file, _action, _target| { 73 file.play_last_shuffled(); 74 }, 75 ); 76 77 klass.install_action( 78 "app.dir.play-shuffled", 79 None, 80 move |file, _action, _target| { 81 file.play(true); 82 }, 83 ); 84 85 klass.install_action("app.dir.play", None, move |file, _action, _target| { 86 file.play(false); 87 }); 88 } 89 90 fn instance_init(obj: &subclass::InitializingObject<Self>) { 91 obj.init_template(); 92 } 93 } 94 95 impl ObjectImpl for File { 96 fn constructed(&self) { 97 self.parent_constructed(); 98 99 let self_weak = self.downgrade(); 100 let click = gtk::GestureClick::new(); 101 click.connect_released(move |_, _, _, _| { 102 if let Some(self_) = self_weak.upgrade() { 103 let path = self_.path.borrow(); 104 let path = path.clone(); 105 let obj = self_.obj(); 106 107 if !self_.is_dir.get() { 108 return; 109 } 110 111 let state = self_.state.upgrade().unwrap(); 112 state.set_current_path(path.clone().as_str()); 113 114 obj.load_files(Some(path)); 115 let go_back_button = self_.go_back_button.borrow(); 116 if let Some(go_back_button) = go_back_button.as_ref() { 117 go_back_button.set_visible(true); 118 } 119 } 120 }); 121 122 self.row.add_controller(click); 123 124 let self_weak = self.downgrade(); 125 let gesture = gtk::GestureClick::new(); 126 let is_dir = self.is_dir.get(); 127 gesture.connect_pressed(move |gestrure, n_press, _, _| { 128 if n_press == 2 && !is_dir { 129 if let Some(self_) = self_weak.upgrade() { 130 let obj = self_.obj(); 131 obj.play(false); 132 } 133 } 134 }); 135 } 136 } 137 138 impl WidgetImpl for File {} 139 impl BoxImpl for File {} 140 141 impl File { 142 pub fn set_files(&self, files: ListBox) { 143 self.files.replace(Some(files)); 144 } 145 146 pub fn set_go_back_button(&self, go_back_button: Option<gtk::Button>) { 147 *self.go_back_button.borrow_mut() = go_back_button; 148 } 149 150 pub fn set_path(&self, path: String) { 151 *self.path.borrow_mut() = path; 152 } 153 154 pub fn set_is_dir(&self, is_dir: bool) { 155 self.is_dir.set(is_dir); 156 match is_dir { 157 true => self.file_icon.set_icon_name(Some("directory-symbolic")), 158 false => self.file_icon.set_icon_name(Some("music-alt-symbolic")), 159 }; 160 } 161 } 162} 163 164glib::wrapper! { 165 pub struct File(ObjectSubclass<imp::File>) 166 @extends gtk::Widget, gtk::Box; 167} 168 169#[gtk::template_callbacks] 170impl File { 171 pub fn new() -> Self { 172 glib::Object::new() 173 } 174 175 pub fn load_files(&self, path: Option<String>) { 176 let rt = tokio::runtime::Runtime::new().unwrap(); 177 let response_ = rt.block_on(async { 178 let url = build_url(); 179 let mut client = BrowseServiceClient::connect(url).await?; 180 let response = client 181 .tree_get_entries(TreeGetEntriesRequest { path: path.clone() }) 182 .await? 183 .into_inner(); 184 Ok::<TreeGetEntriesResponse, Error>(response) 185 }); 186 187 if let Ok(response) = response_ { 188 let files = self.imp().files.borrow(); 189 let files_ref = files.as_ref(); 190 let files_ref = files_ref.unwrap(); 191 192 while let Some(file) = files_ref.first_child() { 193 files_ref.remove(&file); 194 } 195 196 let state = self.imp().state.upgrade().unwrap(); 197 198 for entry in response.entries { 199 let file = File::new(); 200 let filename = entry.name.split("/").last().unwrap(); 201 file.imp().set_files(files_ref.clone()); 202 file.imp() 203 .set_go_back_button(self.imp().go_back_button.borrow().clone()); 204 file.imp().file_name.set_text(filename); 205 file.imp().state.set(Some(&state)); 206 file.imp().set_path(entry.name.clone()); 207 file.imp().set_is_dir(entry.attr == 16); 208 209 match entry.attr == 16 { 210 true => { 211 file.imp().file_menu.set_visible(false); 212 file.imp().directory_menu.set_visible(true); 213 } 214 false => { 215 file.imp().file_menu.set_visible(true); 216 file.imp().directory_menu.set_visible(false); 217 } 218 } 219 220 files_ref.append(&file); 221 } 222 } 223 } 224 225 pub fn play_next(&self) { 226 let path = self.imp().path.borrow(); 227 let path = path.clone(); 228 let is_dir = self.imp().is_dir.get(); 229 thread::spawn(move || { 230 let rt = tokio::runtime::Runtime::new().unwrap(); 231 let url = build_url(); 232 let _ = rt.block_on(async { 233 let mut client = PlaylistServiceClient::connect(url).await?; 234 match is_dir { 235 true => { 236 client 237 .insert_directory(InsertDirectoryRequest { 238 directory: path, 239 position: PLAYLIST_INSERT_FIRST, 240 ..Default::default() 241 }) 242 .await?; 243 } 244 false => { 245 client 246 .insert_tracks(InsertTracksRequest { 247 tracks: vec![path], 248 position: PLAYLIST_INSERT_FIRST, 249 ..Default::default() 250 }) 251 .await?; 252 } 253 } 254 Ok::<(), Error>(()) 255 }); 256 }); 257 } 258 259 pub fn play_last(&self) { 260 let path = self.imp().path.borrow(); 261 let path = path.clone(); 262 let is_dir = self.imp().is_dir.get(); 263 thread::spawn(move || { 264 let rt = tokio::runtime::Runtime::new().unwrap(); 265 let url = build_url(); 266 let _ = rt.block_on(async { 267 let mut client = PlaylistServiceClient::connect(url).await?; 268 match is_dir { 269 true => { 270 client 271 .insert_directory(InsertDirectoryRequest { 272 directory: path, 273 position: PLAYLIST_INSERT_LAST, 274 ..Default::default() 275 }) 276 .await?; 277 } 278 false => { 279 client 280 .insert_tracks(InsertTracksRequest { 281 tracks: vec![path], 282 position: PLAYLIST_INSERT_LAST, 283 ..Default::default() 284 }) 285 .await?; 286 } 287 } 288 Ok::<(), Error>(()) 289 }); 290 }); 291 } 292 293 pub fn add_shuffled(&self) { 294 let path = self.imp().path.borrow(); 295 let path = path.clone(); 296 let is_dir = self.imp().is_dir.get(); 297 thread::spawn(move || { 298 let rt = tokio::runtime::Runtime::new().unwrap(); 299 let url = build_url(); 300 let _ = rt.block_on(async { 301 let mut client = PlaylistServiceClient::connect(url).await?; 302 match is_dir { 303 true => { 304 client 305 .insert_directory(InsertDirectoryRequest { 306 directory: path, 307 position: PLAYLIST_INSERT_SHUFFLED, 308 ..Default::default() 309 }) 310 .await?; 311 } 312 false => { 313 client 314 .insert_tracks(InsertTracksRequest { 315 tracks: vec![path], 316 position: PLAYLIST_INSERT_SHUFFLED, 317 ..Default::default() 318 }) 319 .await?; 320 } 321 } 322 Ok::<(), Error>(()) 323 }); 324 }); 325 } 326 327 pub fn play_last_shuffled(&self) { 328 let path = self.imp().path.borrow(); 329 let path = path.clone(); 330 let is_dir = self.imp().is_dir.get(); 331 thread::spawn(move || { 332 let rt = tokio::runtime::Runtime::new().unwrap(); 333 let url = build_url(); 334 let _ = rt.block_on(async { 335 let mut client = PlaylistServiceClient::connect(url).await?; 336 match is_dir { 337 true => { 338 client 339 .insert_directory(InsertDirectoryRequest { 340 directory: path, 341 position: PLAYLIST_INSERT_LAST_SHUFFLED, 342 ..Default::default() 343 }) 344 .await?; 345 } 346 false => { 347 client 348 .insert_tracks(InsertTracksRequest { 349 tracks: vec![path], 350 position: PLAYLIST_INSERT_LAST_SHUFFLED, 351 ..Default::default() 352 }) 353 .await?; 354 } 355 } 356 Ok::<(), Error>(()) 357 }); 358 }); 359 } 360 361 pub fn play(&self, shuffle: bool) { 362 let path = self.imp().path.borrow(); 363 let path = path.clone(); 364 let is_dir = self.imp().is_dir.get(); 365 thread::spawn(move || { 366 let rt = tokio::runtime::Runtime::new().unwrap(); 367 let url = build_url(); 368 let _ = rt.block_on(async { 369 let mut client = PlaybackServiceClient::connect(url).await?; 370 match is_dir { 371 true => { 372 client 373 .play_directory(PlayDirectoryRequest { 374 path, 375 shuffle: Some(shuffle), 376 recurse: Some(false), 377 ..Default::default() 378 }) 379 .await?; 380 } 381 false => { 382 client.play_track(PlayTrackRequest { path }).await?; 383 } 384 } 385 Ok::<(), Error>(()) 386 }); 387 }); 388 } 389} 390 391fn build_url() -> String { 392 let host = env::var("ROCKBOX_HOST").unwrap_or("localhost".to_string()); 393 let port = env::var("ROCKBOX_PORT").unwrap_or("6061".to_string()); 394 format!("tcp://{}:{}", host, port) 395}