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.

chromecast: return current playback in real time

+755 -142
+6
Cargo.lock
··· 6636 6636 "anyhow", 6637 6637 "async-std", 6638 6638 "futures-util", 6639 + "lazy_static", 6640 + "local-ip-addr", 6639 6641 "md5", 6640 6642 "owo-colors 4.1.0", 6641 6643 "queryst", ··· 6659 6661 "sqlx", 6660 6662 "threadpool", 6661 6663 "tokio", 6664 + "url", 6662 6665 "urlencoding", 6663 6666 ] 6664 6667 ··· 6676 6679 version = "0.1.0" 6677 6680 dependencies = [ 6678 6681 "anyhow", 6682 + "reqwest", 6683 + "rockbox-traits", 6679 6684 "serde", 6685 + "url", 6680 6686 ] 6681 6687 6682 6688 [[package]]
+32 -62
crates/chromecast/src/lib.rs
··· 12 12 use async_trait::async_trait; 13 13 use chromecast::{ 14 14 channels::{ 15 - media::{Image, Media, Metadata, MusicTrackMediaMetadata, StatusEntry, StreamType}, 15 + media::{ 16 + Image, Media, Metadata, MusicTrackMediaMetadata, PlayerState, StatusEntry, StreamType, 17 + }, 16 18 receiver::CastDeviceApp, 17 19 }, 18 20 CastDevice, 19 21 }; 20 - use rockbox_traits::types::track::{Album, Track}; 21 - use rockbox_traits::types::{playback::Playback, track::Artist}; 22 + use rockbox_traits::types::playback::Playback; 23 + use rockbox_traits::types::track::Track; 22 24 use rockbox_traits::Player; 23 25 use rockbox_types::device::Device; 24 26 use tokio::sync::mpsc; ··· 233 235 stream_type: StreamType::None, 234 236 metadata: Some(Metadata::MusicTrack(MusicTrackMediaMetadata { 235 237 title: Some(track.title.clone()), 236 - artist: Some(track.artists.first().unwrap().name.clone()), 237 - album_name: Some(track.album.as_ref().unwrap().title.clone()), 238 - album_artist: Some(track.artists.first().unwrap().name.clone()), 238 + artist: Some(track.artist.clone()), 239 + album_name: Some(track.album.clone()), 240 + album_artist: track.album_artist.clone(), 239 241 track_number: track.track_number, 240 242 disc_number: Some(track.disc_number), 241 - images: match &track.album.as_ref().unwrap().cover { 243 + images: match &track.album_cover { 242 244 Some(cover) => vec![Image { 243 245 url: cover.clone(), 244 246 dimensions: None, ··· 248 250 release_date: None, 249 251 composer: None, 250 252 })), 251 - duration: None, 253 + duration: track.duration, 252 254 }) 253 255 .collect::<Vec<Media>>(); 254 256 ··· 478 480 .to_string(), 479 481 uri: media.content_id.clone(), 480 482 title: metadata.title.clone().unwrap(), 481 - artists: vec![Artist { 482 - id: format!("{:x}", md5::compute(metadata.artist.clone().unwrap())), 483 - name: metadata.artist.clone().unwrap(), 484 - ..Default::default() 485 - }], 486 - album: Some(Album { 487 - id: cover 488 - .clone() 489 - .map(|x| { 490 - x.split("/") 491 - .last() 492 - .map(|x| x.split(".").next().unwrap()) 493 - .unwrap() 494 - .to_string() 495 - }) 496 - .unwrap_or_default(), 497 - title: metadata.album_name.clone().unwrap(), 498 - cover, 499 - ..Default::default() 500 - }), 483 + artist: metadata.artist.clone().unwrap(), 484 + album: metadata.album_name.clone().unwrap(), 501 485 track_number: metadata.track_number, 502 486 disc_number: metadata.disc_number.unwrap_or(0), 503 487 duration: media.duration, 488 + album_cover: cover, 504 489 ..Default::default() 505 490 }, 506 491 item.item_id, ··· 521 506 .to_string(), 522 507 uri: media.content_id.clone(), 523 508 title: metadata.title.clone().unwrap(), 524 - artists: vec![Artist { 525 - id: format!("{:x}", md5::compute(metadata.artist.clone().unwrap())), 526 - name: metadata.artist.clone().unwrap(), 527 - ..Default::default() 528 - }], 529 - album: Some(Album { 530 - id: cover 531 - .clone() 532 - .map(|x| { 533 - x.split("/") 534 - .last() 535 - .map(|x| x.split(".").next().unwrap()) 536 - .unwrap() 537 - .to_string() 538 - }) 539 - .unwrap_or_default(), 540 - title: metadata.album_name.clone().unwrap(), 541 - cover, 542 - ..Default::default() 543 - }), 509 + artist: metadata.artist.clone().unwrap(), 510 + album: metadata.album_name.clone().unwrap(), 544 511 track_number: metadata.track_number, 545 512 disc_number: metadata.disc_number.unwrap_or(0), 546 513 duration: media.duration, 514 + album_cover: cover, 547 515 ..Default::default() 548 516 }; 549 517 return Ok(Playback { ··· 553 521 .current_time 554 522 .map(|x| (x * 1000.0) as u32) 555 523 .unwrap_or(0), 556 - is_playing: true, 524 + is_playing: status.player_state.to_string() == "PLAYING", 557 525 items, 558 526 current_item_id: status.current_item_id, 559 527 }); ··· 572 540 }), 573 541 index: 0, 574 542 position_ms: status.current_time.map(|x| x as u32).unwrap_or(0), 575 - is_playing: true, 543 + is_playing: status.player_state.to_string() == "PLAYING", 576 544 current_item_id: status.current_item_id, 577 545 items, 578 546 }); ··· 582 550 let app_to_manage = CastDeviceApp::from_str(DEFAULT_APP_ID).unwrap(); 583 551 self.cast_device 584 552 .connection 585 - .connect(DEFAULT_DESTINATION_ID.to_string()) 586 - .unwrap(); 587 - self.cast_device.heartbeat.ping().unwrap(); 553 + .connect(DEFAULT_DESTINATION_ID.to_string())?; 554 + self.cast_device.heartbeat.ping()?; 588 555 589 - let status = self.cast_device.receiver.get_status().unwrap(); 556 + let status = self.cast_device.receiver.get_status()?; 590 557 591 558 let app = status 592 559 .applications ··· 597 564 Some(app) => { 598 565 self.cast_device 599 566 .connection 600 - .connect(app.transport_id.as_str()) 601 - .unwrap(); 567 + .connect(app.transport_id.as_str())?; 602 568 603 569 let status = self 604 570 .cast_device 605 571 .media 606 - .get_status(app.transport_id.as_str(), None) 607 - .unwrap(); 572 + .get_status(app.transport_id.as_str(), None)?; 573 + 574 + if status.entries.is_empty() { 575 + return Err(Error::msg("No media session running")); 576 + } 577 + 608 578 let status = status.entries.first().unwrap(); 609 579 let media_session_id = status.media_session_id; 610 580 let transport_id = app.transport_id.as_str(); ··· 665 635 stream_type: StreamType::Buffered, 666 636 metadata: Some(Metadata::MusicTrack(MusicTrackMediaMetadata { 667 637 title: Some(track.title.clone()), 668 - artist: Some(track.artists.first().unwrap().name.clone()), 669 - album_name: Some(track.album.as_ref().unwrap().title.clone()), 670 - album_artist: Some(track.artists.first().unwrap().name.clone()), 638 + artist: Some(track.artist.clone()), 639 + album_name: Some(track.album.clone()), 640 + album_artist: track.album_artist, 671 641 track_number: track.track_number, 672 642 disc_number: Some(track.disc_number), 673 - images: match &track.album.as_ref().unwrap().cover { 643 + images: match &track.album_cover { 674 644 Some(cover) => vec![Image { 675 645 url: cover.clone(), 676 646 dimensions: None,
+27
crates/graphql/src/schema/mod.rs
··· 43 43 44 44 #[derive(MergedSubscription, Default)] 45 45 pub struct Subscription(PlaybackSubscription, PlaylistSubscription); 46 + 47 + #[macro_export] 48 + macro_rules! check_and_load_player { 49 + ($client:expr, $tracks:expr, $shuffle:expr) => { 50 + let response = $client 51 + .get(&format!("{}/player", rockbox_url())) 52 + .send() 53 + .await?; 54 + let player = response.json::<Device>().await?; 55 + 56 + // connected to a player 57 + if !player.host.is_empty() && player.port != 0 { 58 + let client = reqwest::Client::new(); 59 + let body = serde_json::json!({ 60 + "tracks": $tracks, 61 + "shuffle": $shuffle, 62 + }); 63 + 64 + client 65 + .put(&format!("{}/player/load", rockbox_url())) 66 + .json(&body) 67 + .send() 68 + .await?; 69 + return Ok(0); 70 + } 71 + }; 72 + }
+1 -1
crates/graphql/src/schema/objects/track.rs
··· 7 7 use tantivy::schema::*; 8 8 use tantivy::TantivyDocument; 9 9 10 - #[derive(Default, Clone, Serialize, Deserialize)] 10 + #[derive(Default, Debug, Clone, Serialize, Deserialize)] 11 11 pub struct Track { 12 12 pub id: Option<String>, 13 13 pub title: String,
+33 -24
crates/graphql/src/schema/playback.rs
··· 3 3 sync::{mpsc::Sender, Arc, Mutex}, 4 4 }; 5 5 6 - use crate::{read_files, schema::objects, AUDIO_EXTENSIONS}; 6 + use crate::{check_and_load_player, read_files, schema::objects, AUDIO_EXTENSIONS}; 7 7 use async_graphql::*; 8 8 use futures_util::Stream; 9 9 use rockbox_library::repo; ··· 11 11 events::RockboxCommand, 12 12 types::{audio_status::AudioStatus, file_position::FilePosition, mp3_entry::Mp3Entry}, 13 13 }; 14 + use rockbox_types::device::Device; 14 15 use sqlx::{Pool, Sqlite}; 15 16 16 17 use crate::{rockbox_url, schema::objects::track::Track, simplebroker::SimpleBroker}; ··· 91 92 } 92 93 93 94 async fn pause(&self, ctx: &Context<'_>) -> Result<i32, Error> { 94 - ctx.data::<Arc<Mutex<Sender<RockboxCommand>>>>() 95 - .unwrap() 96 - .lock() 97 - .unwrap() 98 - .send(RockboxCommand::Pause)?; 95 + let client = ctx.data::<reqwest::Client>().unwrap(); 96 + let url = format!("{}/player/pause", rockbox_url()); 97 + client.put(&url).send().await?; 99 98 Ok(0) 100 99 } 101 100 102 101 async fn resume(&self, ctx: &Context<'_>) -> Result<i32, Error> { 103 - ctx.data::<Arc<Mutex<Sender<RockboxCommand>>>>() 104 - .unwrap() 105 - .lock() 106 - .unwrap() 107 - .send(RockboxCommand::Resume)?; 102 + let client = ctx.data::<reqwest::Client>().unwrap(); 103 + let url = format!("{}/player/resume", rockbox_url()); 104 + client.put(&url).send().await?; 108 105 Ok(0) 109 106 } 110 107 111 108 async fn next(&self, ctx: &Context<'_>) -> Result<i32, Error> { 112 - ctx.data::<Arc<Mutex<Sender<RockboxCommand>>>>() 113 - .unwrap() 114 - .lock() 115 - .unwrap() 116 - .send(RockboxCommand::Next)?; 109 + let client = ctx.data::<reqwest::Client>().unwrap(); 110 + let url = format!("{}/player/next", rockbox_url()); 111 + client.put(&url).send().await?; 117 112 Ok(0) 118 113 } 119 114 120 115 async fn previous(&self, ctx: &Context<'_>) -> Result<i32, Error> { 121 - ctx.data::<Arc<Mutex<Sender<RockboxCommand>>>>() 122 - .unwrap() 123 - .lock() 124 - .unwrap() 125 - .send(RockboxCommand::Prev)?; 116 + let client = ctx.data::<reqwest::Client>().unwrap(); 117 + let url = format!("{}/player/previous", rockbox_url()); 118 + client.put(&url).send().await?; 126 119 Ok(0) 127 120 } 128 121 ··· 163 156 let pool = ctx.data::<Pool<Sqlite>>()?; 164 157 let tracks = repo::album_tracks::find_by_album(pool.clone(), &album_id).await?; 165 158 let client = ctx.data::<reqwest::Client>().unwrap(); 159 + let tracks = tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(); 166 160 let body = serde_json::json!({ 167 - "tracks": tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(), 161 + "tracks": tracks, 168 162 }); 163 + 164 + check_and_load_player!(client, tracks, shuffle.unwrap_or_default()); 169 165 170 166 let url = format!("{}/playlists", rockbox_url()); 171 167 client.post(&url).json(&body).send().await?; ··· 195 191 let pool = ctx.data::<Pool<Sqlite>>()?; 196 192 let client = ctx.data::<reqwest::Client>().unwrap(); 197 193 let tracks = repo::artist_tracks::find_by_artist(pool.clone(), &artist_id).await?; 194 + let tracks = tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(); 198 195 let body = serde_json::json!({ 199 - "tracks": tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(), 196 + "tracks": tracks, 200 197 }); 201 198 199 + check_and_load_player!(client, tracks, shuffle.unwrap_or_default()); 200 + 202 201 let url = format!("{}/playlists", rockbox_url()); 203 202 client.post(&url).json(&body).send().await?; 204 203 ··· 272 271 "tracks": tracks, 273 272 }); 274 273 274 + check_and_load_player!(client, tracks, shuffle.unwrap_or_default()); 275 + 275 276 let url = format!("{}/playlists", rockbox_url()); 276 277 client.post(&url).json(&body).send().await?; 277 278 ··· 295 296 let path = path.replace("file://", ""); 296 297 297 298 let body = serde_json::json!({ 298 - "tracks": vec![path], 299 + "tracks": vec![path.clone()], 299 300 }); 300 301 302 + check_and_load_player!(client, vec![path], false); 303 + 304 + let client = reqwest::Client::new(); 305 + 301 306 let url = format!("{}/playlists", rockbox_url()); 302 307 client.post(&url).json(&body).send().await?; 303 308 ··· 325 330 let body = serde_json::json!({ 326 331 "tracks": tracks, 327 332 }); 333 + 334 + check_and_load_player!(client, tracks, shuffle.unwrap_or_default()); 328 335 329 336 let url = format!("{}/playlists", rockbox_url()); 330 337 client.post(&url).json(&body).send().await?; ··· 361 368 let body = serde_json::json!({ 362 369 "tracks": tracks, 363 370 }); 371 + 372 + check_and_load_player!(client, tracks, shuffle.unwrap_or_default()); 364 373 365 374 let url = format!("{}/playlists", rockbox_url()); 366 375 client.post(&url).json(&body).send().await?;
+31
crates/rpc/src/lib.rs
··· 1212 1212 Ok(result) 1213 1213 }) 1214 1214 } 1215 + 1216 + #[macro_export] 1217 + macro_rules! check_and_load_player { 1218 + ($response:expr, $tracks:expr, $shuffle:expr) => { 1219 + let client = reqwest::Client::new(); 1220 + let response = client 1221 + .get(format!("{}/player", rockbox_url())) 1222 + .send() 1223 + .await 1224 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 1225 + let player = response 1226 + .json::<rockbox_types::device::Device>() 1227 + .await 1228 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 1229 + 1230 + if player.host.is_empty() && player.port == 0 { 1231 + let client = reqwest::Client::new(); 1232 + let body = serde_json::json!({ 1233 + "tracks": $tracks, 1234 + "shuffle": $shuffle, 1235 + }); 1236 + client 1237 + .put(&format!("{}/player/", rockbox_url())) 1238 + .json(&body) 1239 + .send() 1240 + .await 1241 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 1242 + return Ok(tonic::Response::new($response)); 1243 + } 1244 + }; 1245 + }
+47 -25
crates/rpc/src/playback.rs
··· 5 5 6 6 use crate::{ 7 7 api::rockbox::v1alpha1::{playback_service_server::PlaybackService, *}, 8 - read_files, rockbox_url, AUDIO_EXTENSIONS, 8 + check_and_load_player, read_files, rockbox_url, AUDIO_EXTENSIONS, 9 9 }; 10 10 use rockbox_library::repo; 11 11 use rockbox_sys::{self as rb, events::RockboxCommand, types::audio_status::AudioStatus}; ··· 50 50 &self, 51 51 _request: tonic::Request<PauseRequest>, 52 52 ) -> Result<tonic::Response<PauseResponse>, tonic::Status> { 53 - self.cmd_tx 54 - .lock() 55 - .unwrap() 56 - .send(RockboxCommand::Pause) 57 - .map_err(|_| tonic::Status::internal("Failed to send command"))?; 53 + self.client 54 + .put(&format!("{}/player/pause", rockbox_url())) 55 + .send() 56 + .await 57 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 58 58 Ok(tonic::Response::new(PauseResponse::default())) 59 59 } 60 60 ··· 62 62 &self, 63 63 _request: tonic::Request<ResumeRequest>, 64 64 ) -> Result<tonic::Response<ResumeResponse>, tonic::Status> { 65 - self.cmd_tx 66 - .lock() 67 - .unwrap() 68 - .send(RockboxCommand::Resume) 69 - .map_err(|_| tonic::Status::internal("Failed to send command"))?; 65 + self.client 66 + .put(&format!("{}/player/resume", rockbox_url())) 67 + .send() 68 + .await 69 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 70 70 Ok(tonic::Response::new(ResumeResponse::default())) 71 71 } 72 72 ··· 74 74 &self, 75 75 _request: tonic::Request<NextRequest>, 76 76 ) -> Result<tonic::Response<NextResponse>, tonic::Status> { 77 - self.cmd_tx 78 - .lock() 79 - .unwrap() 80 - .send(RockboxCommand::Next) 81 - .map_err(|_| tonic::Status::internal("Failed to send command"))?; 77 + self.client 78 + .put(&format!("{}/player/next", rockbox_url())) 79 + .send() 80 + .await 81 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 82 82 Ok(tonic::Response::new(NextResponse::default())) 83 83 } 84 84 ··· 86 86 &self, 87 87 _request: tonic::Request<PreviousRequest>, 88 88 ) -> Result<tonic::Response<PreviousResponse>, tonic::Status> { 89 - self.cmd_tx 90 - .lock() 91 - .unwrap() 92 - .send(RockboxCommand::Prev) 93 - .map_err(|_| tonic::Status::internal("Failed to send command"))?; 89 + self.client 90 + .put(&format!("{}/player/previous", rockbox_url())) 91 + .send() 92 + .await 93 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 94 94 Ok(tonic::Response::new(PreviousResponse::default())) 95 95 } 96 96 ··· 209 209 let tracks = repo::album_tracks::find_by_album(self.pool.clone(), &album_id) 210 210 .await 211 211 .map_err(|e| tonic::Status::internal(e.to_string()))?; 212 + let tracks = tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(); 212 213 let body = serde_json::json!({ 213 - "tracks": tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(), 214 + "tracks": tracks, 214 215 }); 216 + 217 + let response = PlayAlbumResponse::default(); 218 + check_and_load_player!(response, tracks, shuffle.unwrap_or_default()); 215 219 216 220 let url = format!("{}/playlists", rockbox_url()); 217 221 self.client ··· 255 259 let tracks = repo::artist_tracks::find_by_artist(self.pool.clone(), &artist_id) 256 260 .await 257 261 .map_err(|e| tonic::Status::internal(e.to_string()))?; 262 + let tracks = tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(); 258 263 let body = serde_json::json!({ 259 - "tracks": tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(), 264 + "tracks": tracks, 260 265 }); 266 + 267 + let response = PlayArtistTracksResponse::default(); 268 + check_and_load_player!(response, tracks, shuffle.unwrap_or_default()); 261 269 262 270 let url = format!("{}/playlists", rockbox_url()); 263 271 self.client ··· 350 358 "tracks": tracks, 351 359 }); 352 360 361 + let response = PlayDirectoryResponse::default(); 362 + check_and_load_player!(response, tracks, shuffle.unwrap_or_default()); 363 + 353 364 let url = format!("{}/playlists", rockbox_url()); 354 365 self.client 355 366 .post(&url) ··· 393 404 "tracks": tracks, 394 405 }); 395 406 407 + let response = PlayTrackResponse::default(); 408 + check_and_load_player!(response, tracks, false); 409 + 396 410 let url = format!("{}/playlists", rockbox_url()); 397 411 self.client 398 412 .post(&url) ··· 422 436 let tracks = repo::favourites::all_tracks(self.pool.clone()) 423 437 .await 424 438 .map_err(|e| tonic::Status::internal(e.to_string()))?; 439 + let tracks = tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(); 425 440 let body = serde_json::json!({ 426 - "tracks": tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(), 441 + "tracks": tracks, 427 442 }); 428 443 444 + let response = PlayLikedTracksResponse::default(); 445 + check_and_load_player!(response, tracks, shuffle.unwrap_or_default()); 446 + 429 447 let url = format!("{}/playlists", rockbox_url()); 430 448 self.client 431 449 .post(&url) ··· 467 485 let tracks = repo::track::all(self.pool.clone()) 468 486 .await 469 487 .map_err(|e| tonic::Status::internal(e.to_string()))?; 488 + let tracks = tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(); 470 489 let body = serde_json::json!({ 471 - "tracks": tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(), 490 + "tracks": tracks, 472 491 }); 492 + 493 + let response = PlayAllTracksResponse::default(); 494 + check_and_load_player!(response, tracks, shuffle.unwrap_or_default()); 473 495 474 496 let url = format!("{}/playlists", rockbox_url()); 475 497 self.client
+3
crates/server/Cargo.toml
··· 10 10 anyhow = "1.0.89" 11 11 async-std = {version = "1.13.0", features = ["unstable"]} 12 12 futures-util = "0.3.31" 13 + lazy_static = "1.5.0" 14 + local-ip-addr = "0.1.1" 13 15 md5 = "0.7.0" 14 16 owo-colors = "4.0.0" 15 17 queryst = "3.0.0" ··· 33 35 sqlx = {version = "0.8.2", features = ["runtime-tokio", "tls-rustls", "sqlite", "chrono", "derive", "macros"]} 34 36 threadpool = "1.8.1" 35 37 tokio = {version = "1.36.0", features = ["full"]} 38 + url = "2.3.1" 36 39 urlencoding = "2.1.3"
+9 -1
crates/server/src/handlers/devices.rs
··· 1 1 use anyhow::Error; 2 2 use rockbox_chromecast::Chromecast; 3 3 4 - use crate::http::{Context, Request, Response}; 4 + use crate::{ 5 + http::{Context, Request, Response}, 6 + GLOBAL_MUTEX, 7 + }; 5 8 6 9 pub async fn connect(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 7 10 let id = &req.params[0]; ··· 10 13 let devices = ctx.devices.lock().unwrap(); 11 14 let device = devices.iter().find(|d| d.id == *id); 12 15 if let Some(device) = device { 16 + let mut mutex = GLOBAL_MUTEX.lock().unwrap(); 17 + *mutex = 1; 13 18 *player = Chromecast::connect(device.clone())?; 14 19 *current_device = Some(device.clone()); 15 20 res.set_status(200); ··· 24 29 let mut player = ctx.player.lock().unwrap(); 25 30 let mut current_device = ctx.current_device.lock().unwrap(); 26 31 if let Some(player) = player.as_mut() { 32 + player.stop().await?; 27 33 player.disconnect().await?; 28 34 } 35 + let mut mutex = GLOBAL_MUTEX.lock().unwrap(); 36 + *mutex = 0; 29 37 *player = None; 30 38 *current_device = None; 31 39 res.set_status(200);
+2
crates/server/src/handlers/mod.rs
··· 35 35 async_handler!(artists, get_artist_albums); 36 36 async_handler!(artists, get_artist_tracks); 37 37 async_handler!(browse, get_tree_entries); 38 + async_handler!(player, load); 38 39 async_handler!(player, play); 39 40 async_handler!(player, pause); 40 41 async_handler!(player, resume); ··· 48 49 async_handler!(player, stop); 49 50 async_handler!(player, get_file_position); 50 51 async_handler!(player, adjust_volume); 52 + async_handler!(player, get_current_player); 51 53 async_handler!(playlists, create_playlist); 52 54 async_handler!(playlists, start_playlist); 53 55 async_handler!(playlists, shuffle_playlist);
+177 -19
crates/server/src/handlers/player.rs
··· 1 + use std::env; 2 + 1 3 use crate::http::{Context, Request, Response}; 2 4 use anyhow::Error; 3 - use rockbox_sys as rb; 4 - use rockbox_types::NewVolume; 5 + use local_ip_addr::get_local_ip_address; 6 + use rand::seq::SliceRandom; 7 + use rockbox_sys::{ 8 + self as rb, 9 + types::{audio_status::AudioStatus, mp3_entry::Mp3Entry}, 10 + }; 11 + use rockbox_traits::types::track::Track; 12 + use rockbox_types::{device::Device, LoadTracks, NewVolume}; 13 + 14 + pub async fn load(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 15 + let mut player = ctx.player.lock().unwrap(); 16 + if player.is_none() { 17 + res.set_status(404); 18 + return Ok(()); 19 + } 5 20 6 - pub async fn play(_ctx: &Context, req: &Request, _res: &mut Response) -> Result<(), Error> { 21 + let player = player.as_deref_mut().unwrap(); 22 + 23 + let req_body = req.body.as_ref().unwrap(); 24 + let request: LoadTracks = serde_json::from_str(&req_body)?; 25 + 26 + let rockbox_addr = env::var("ROCKBOX_ADDR").unwrap_or_else(|_| get_local_ip_address().unwrap()); 27 + let rockbox_port = env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or_else(|_| "6062".to_string()); 28 + let kv = ctx.kv.lock().unwrap(); 29 + let mut tracks = request 30 + .tracks 31 + .iter() 32 + .filter(|t| kv.get(*t).is_some()) 33 + .map(|t| { 34 + let track = kv.get(t).unwrap(); 35 + Track { 36 + id: track.id.clone(), 37 + title: track.title.clone(), 38 + artist: track.artist.clone(), 39 + album: track.album.clone(), 40 + album_artist: Some(track.album_artist.clone()), 41 + artist_id: Some(track.artist_id.clone()), 42 + album_id: Some(track.album_id.clone()), 43 + album_cover: track.album_art.clone().map(|cover| { 44 + format!("http://{}:{}/covers/{}", rockbox_addr, rockbox_port, cover) 45 + }), 46 + track_number: track.track_number, 47 + path: track.path.clone(), 48 + uri: format!( 49 + "http://{}:{}/tracks/{}", 50 + rockbox_addr, rockbox_port, track.id 51 + ), 52 + disc_number: track.disc_number, 53 + duration: Some(track.length as f32 / 1000.0), 54 + ..Default::default() 55 + } 56 + }) 57 + .collect::<Vec<Track>>(); 58 + 59 + if Some(true) == request.shuffle { 60 + tracks.shuffle(&mut rand::thread_rng()); 61 + } 62 + 63 + player.load_tracks(tracks, None).await?; 64 + 65 + res.set_status(200); 66 + 67 + Ok(()) 68 + } 69 + 70 + pub async fn play(ctx: &Context, req: &Request, _res: &mut Response) -> Result<(), Error> { 7 71 let elapsed = match req.query_params.get("elapsed") { 8 72 Some(elapsed) => elapsed.as_str().unwrap_or("0").parse().unwrap_or(0), 9 73 None => 0, ··· 12 76 Some(offset) => offset.as_str().unwrap_or("0").parse().unwrap_or(0), 13 77 None => 0, 14 78 }; 15 - rb::playback::play(elapsed, offset); 79 + let player = ctx.player.lock().unwrap(); 80 + 81 + if player.is_none() { 82 + rb::playback::play(elapsed, offset); 83 + } 84 + 16 85 Ok(()) 17 86 } 18 87 19 - pub async fn pause(_ctx: &Context, _req: &Request, _res: &mut Response) -> Result<(), Error> { 20 - rb::playback::pause(); 88 + pub async fn pause(ctx: &Context, _req: &Request, _res: &mut Response) -> Result<(), Error> { 89 + let player = ctx.player.lock().unwrap(); 90 + 91 + match player.as_deref() { 92 + Some(player) => { 93 + player.pause().await?; 94 + } 95 + None => { 96 + rb::playback::pause(); 97 + } 98 + } 99 + 21 100 Ok(()) 22 101 } 23 102 ··· 30 109 Ok(()) 31 110 } 32 111 33 - pub async fn status(_ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { 112 + pub async fn status(ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { 113 + let mut player = ctx.player.lock().unwrap(); 114 + 115 + if let Some(player) = player.as_deref_mut() { 116 + let current_playback = player.get_current_playback().await?; 117 + res.json(&AudioStatus { 118 + status: match current_playback.is_playing { 119 + true => 1, 120 + false => 0, 121 + }, 122 + }); 123 + return Ok(()); 124 + } 125 + 34 126 let status = rb::playback::status(); 35 127 res.json(&status); 36 128 Ok(()) 37 129 } 38 130 39 - pub async fn current_track( 40 - _ctx: &Context, 41 - _req: &Request, 42 - res: &mut Response, 43 - ) -> Result<(), Error> { 131 + pub async fn current_track(ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { 132 + let mut player = ctx.player.lock().unwrap(); 133 + 134 + if let Some(player) = player.as_deref_mut() { 135 + let current_playback = player.get_current_playback().await?; 136 + let track: Option<Mp3Entry> = current_playback.current_track.map(|t| t.into()); 137 + let track = track.map(|mut t| { 138 + t.elapsed = current_playback.position_ms as u64; 139 + t 140 + }); 141 + res.json(&track); 142 + return Ok(()); 143 + } 144 + 44 145 let track = rb::playback::current_track(); 45 146 res.json(&track); 46 147 Ok(()) 47 148 } 48 149 49 - pub async fn next_track(_ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { 150 + pub async fn next_track(ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { 151 + let player = ctx.player.lock().unwrap(); 152 + 153 + if let Some(_player) = player.as_deref() { 154 + return Ok(()); 155 + } 156 + 50 157 let track = rb::playback::next_track(); 51 158 res.json(&track); 52 159 Ok(()) ··· 61 168 Ok(()) 62 169 } 63 170 64 - pub async fn resume(_ctx: &Context, _req: &Request, _res: &mut Response) -> Result<(), Error> { 65 - rb::playback::resume(); 171 + pub async fn resume(ctx: &Context, _req: &Request, _res: &mut Response) -> Result<(), Error> { 172 + let player = ctx.player.lock().unwrap(); 173 + 174 + match player.as_deref() { 175 + Some(player) => { 176 + player.play().await?; 177 + } 178 + None => { 179 + rb::playback::resume(); 180 + } 181 + } 182 + 66 183 Ok(()) 67 184 } 68 185 69 - pub async fn next(_ctx: &Context, _req: &Request, _res: &mut Response) -> Result<(), Error> { 70 - rb::playback::next(); 186 + pub async fn next(ctx: &Context, _req: &Request, _res: &mut Response) -> Result<(), Error> { 187 + let player = ctx.player.lock().unwrap(); 188 + 189 + match player.as_deref() { 190 + Some(player) => { 191 + player.next().await?; 192 + } 193 + None => { 194 + rb::playback::next(); 195 + } 196 + } 197 + 71 198 Ok(()) 72 199 } 73 200 74 - pub async fn previous(_ctx: &Context, _req: &Request, _res: &mut Response) -> Result<(), Error> { 75 - rb::playback::prev(); 201 + pub async fn previous(ctx: &Context, _req: &Request, _res: &mut Response) -> Result<(), Error> { 202 + let player = ctx.player.lock().unwrap(); 203 + 204 + match player.as_deref() { 205 + Some(player) => { 206 + player.previous().await?; 207 + } 208 + None => { 209 + rb::playback::prev(); 210 + } 211 + } 212 + 76 213 Ok(()) 77 214 } 78 215 ··· 99 236 res.json(&new_volume); 100 237 Ok(()) 101 238 } 239 + 240 + pub async fn get_current_player( 241 + ctx: &Context, 242 + _req: &Request, 243 + res: &mut Response, 244 + ) -> Result<(), Error> { 245 + let device = ctx.current_device.lock().unwrap(); 246 + 247 + if let Some(device) = device.as_ref() { 248 + res.json(device); 249 + return Ok(()); 250 + } 251 + 252 + res.json(&Device { 253 + name: "Rockbox (Default Player)".to_string(), 254 + app: "default".to_string(), 255 + service: "rockbox".to_string(), 256 + ..Default::default() 257 + }); 258 + Ok(()) 259 + }
+13 -1
crates/server/src/http.rs
··· 1 1 use anyhow::Error; 2 2 use owo_colors::OwoColorize; 3 + use rockbox_library::entity::track::Track; 3 4 use rockbox_search::{create_indexes, Indexes}; 4 5 use rockbox_sys::{ 5 6 self as rb, ··· 20 21 }; 21 22 use threadpool::ThreadPool; 22 23 23 - use crate::scan::scan_chromecast_devices; 24 + use crate::{ 25 + kv::{build_tracks_kv, KV}, 26 + player_events::listen_for_playback_changes, 27 + scan::scan_chromecast_devices, 28 + }; 24 29 25 30 type Handler = fn(&Context, &Request, &mut Response) -> Result<(), Error>; 26 31 ··· 32 37 pub devices: Arc<Mutex<Vec<Device>>>, 33 38 pub current_device: Arc<Mutex<Option<Device>>>, 34 39 pub player: Arc<Mutex<Option<Box<dyn Player + Send>>>>, 40 + pub kv: Arc<Mutex<KV<Track>>>, 35 41 } 36 42 37 43 #[derive(Debug)] ··· 248 254 let devices = Arc::new(Mutex::new(Vec::new())); 249 255 let current_device = Arc::new(Mutex::new(None)); 250 256 let player = Arc::new(Mutex::new(None)); 257 + let kv = Arc::new(Mutex::new(rt.block_on(build_tracks_kv(db_pool.clone()))?)); 251 258 252 259 // Start scanning for devices 253 260 scan_chromecast_devices(devices.clone()); 261 + listen_for_playback_changes(player.clone(), db_pool.clone()); 254 262 255 263 let indexes = create_indexes()?; 256 264 ··· 270 278 let cloned_devices = devices.clone(); 271 279 let cloned_current_device = current_device.clone(); 272 280 let cloned_player = player.clone(); 281 + let cloned_kv = kv.clone(); 273 282 pool.execute(move || { 274 283 let mut buf_reader = BufReader::new(&stream); 275 284 let mut request = String::new(); ··· 339 348 cloned_devices, 340 349 cloned_current_device, 341 350 cloned_player, 351 + cloned_kv, 342 352 ); 343 353 } 344 354 ··· 386 396 devices: Arc<Mutex<Vec<Device>>>, 387 397 current_device: Arc<Mutex<Option<Device>>>, 388 398 player: Arc<Mutex<Option<Box<dyn Player + Send>>>>, 399 + kv: Arc<Mutex<KV<Track>>>, 389 400 ) { 390 401 println!("{} {}", method.bright_cyan(), path); 391 402 match self.router.route(method, path) { ··· 399 410 devices, 400 411 current_device, 401 412 player, 413 + kv, 402 414 }; 403 415 let request = Request { 404 416 method: method.to_string(),
+53
crates/server/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 + }
+19
crates/server/src/lib.rs
··· 1 1 use handlers::*; 2 2 3 3 use http::RockboxHttpServer; 4 + use lazy_static::lazy_static; 4 5 use rockbox_graphql::{ 5 6 schema::objects::{self, audio_status::AudioStatus, track::Track}, 6 7 simplebroker::SimpleBroker, ··· 21 22 pub mod cache; 22 23 pub mod handlers; 23 24 pub mod http; 25 + pub mod kv; 26 + pub mod player_events; 24 27 pub mod scan; 25 28 26 29 pub const AUDIO_EXTENSIONS: [&str; 17] = [ ··· 28 31 "spx", "sid", "ape", "wma", 29 32 ]; 30 33 34 + lazy_static! { 35 + pub static ref GLOBAL_MUTEX: Mutex<i32> = Mutex::new(0); 36 + } 37 + 31 38 #[no_mangle] 32 39 pub extern "C" fn debugfn(args: *const c_char, value: c_int) { 33 40 let c_str = unsafe { std::ffi::CStr::from_ptr(args) }; ··· 57 64 58 65 app.get("/browse/tree-entries", get_tree_entries); 59 66 67 + app.get("/player", get_current_player); 68 + app.put("/player/load", load); 60 69 app.put("/player/play", play); 61 70 app.put("/player/pause", pause); 62 71 app.put("/player/resume", resume); ··· 247 256 let mut metadata_cache: HashMap<String, Mp3Entry> = HashMap::new(); 248 257 249 258 loop { 259 + let mutex = GLOBAL_MUTEX.lock().unwrap(); 260 + if *mutex == 1 { 261 + drop(mutex); 262 + thread::sleep(std::time::Duration::from_millis(100)); 263 + rb::system::sleep(rb::HZ); 264 + continue; 265 + } 266 + 267 + drop(mutex); 268 + 250 269 let playback_status: AudioStatus = rb::playback::status().into(); 251 270 SimpleBroker::publish(playback_status); 252 271 match rb::playback::current_track() {
+93
crates/server/src/player_events.rs
··· 1 + use std::{ 2 + env, 3 + sync::{Arc, Mutex}, 4 + thread, 5 + }; 6 + 7 + use rockbox_graphql::{ 8 + schema::objects::{audio_status::AudioStatus, track::Track}, 9 + simplebroker::SimpleBroker, 10 + }; 11 + use rockbox_library::repo; 12 + use rockbox_traits::Player; 13 + use sqlx::{Pool, Sqlite}; 14 + use url::Url; 15 + 16 + pub fn listen_for_playback_changes( 17 + player: Arc<Mutex<Option<Box<dyn Player + Send>>>>, 18 + pool: Pool<Sqlite>, 19 + ) { 20 + let cloned_player = player.clone(); 21 + thread::spawn(move || { 22 + let rt = tokio::runtime::Runtime::new().unwrap(); 23 + let client = reqwest::blocking::Client::new(); 24 + loop { 25 + let mut player = cloned_player.lock().unwrap(); 26 + 27 + if let Some(player) = player.as_deref_mut() { 28 + if let Ok(current_playback) = rt.block_on(player.get_current_playback()) { 29 + if let Some(current_track) = current_playback.current_track { 30 + if let Ok(Some(metadata)) = 31 + rt.block_on(repo::track::find(pool.clone(), &current_track.id)) 32 + { 33 + let album_art = match current_track.album_cover { 34 + Some(cover) => { 35 + let url = Url::parse(&cover).unwrap(); 36 + let path = url.path(); 37 + match client 38 + .get(&format!( 39 + "{}:{}{}", 40 + "http://localhost", 41 + env::var("ROCKBOX_GRAPHQL_PORT") 42 + .unwrap_or("6062".to_string()), 43 + path 44 + )) 45 + .send() 46 + { 47 + Ok(response) => match response.status() { 48 + reqwest::StatusCode::OK => Some(format!( 49 + "http://localhost:{}{}", 50 + env::var("ROCKBOX_GRAPHQL_PORT") 51 + .unwrap_or("6062".to_string()), 52 + path 53 + )), 54 + _ => Some(cover), 55 + }, 56 + Err(_) => Some(cover), 57 + } 58 + } 59 + None => None, 60 + }; 61 + 62 + let mut track: Track = Default::default(); 63 + track.id = Some(metadata.id); 64 + track.title = metadata.title; 65 + track.artist = metadata.artist; 66 + track.album = metadata.album; 67 + track.length = metadata.length as u64; 68 + track.album_art = album_art; 69 + track.album_id = Some(metadata.album_id); 70 + track.artist_id = Some(metadata.artist_id); 71 + track.elapsed = current_playback.position_ms as u64; 72 + track.path = metadata.path; 73 + track.tracknum = 74 + metadata.track_number.map(|n| n as i32).unwrap_or_default(); 75 + track.discnum = metadata.disc_number as i32; 76 + SimpleBroker::publish(track); 77 + SimpleBroker::publish(AudioStatus { 78 + status: match current_playback.is_playing { 79 + true => 1, 80 + false => 3, 81 + }, 82 + }); 83 + } 84 + } 85 + } 86 + } 87 + 88 + drop(player); 89 + 90 + thread::sleep(std::time::Duration::from_millis(500)); 91 + } 92 + }); 93 + }
+5
crates/settings/src/lib.rs
··· 12 12 } 13 13 }; 14 14 15 + if new_settings.is_none() { 16 + // disable sleep timer 17 + rb::system::set_sleeptimer_duration(0); 18 + } 19 + 15 20 if let Some(music_dir) = settings.clone().music_dir { 16 21 if let Ok(_) = std::fs::metadata(&music_dir) { 17 22 std::env::set_var(
+3
crates/sys/Cargo.toml
··· 5 5 6 6 [dependencies] 7 7 anyhow = "1.0.89" 8 + reqwest = {version = "0.12.7", features = ["rustls-tls", "json"], default-features = false} 9 + rockbox-traits = {path = "../traits"} 8 10 serde = {version = "1.0.210", features = ["derive"]} 11 + url = "2.3.1"
+1 -1
crates/sys/src/lib.rs
··· 1302 1302 fn semaphore_wait(); 1303 1303 fn semaphore_release(); 1304 1304 fn reset_poweroff_timer(); 1305 - fn set_sleeptimer_duration(); 1305 + fn set_sleeptimer_duration(minutes: c_int); 1306 1306 fn get_sleep_timer(); 1307 1307 1308 1308 // Menu
+2 -2
crates/sys/src/system.rs
··· 115 115 } 116 116 } 117 117 118 - pub fn set_sleeptimer_duration() { 118 + pub fn set_sleeptimer_duration(minutes: i32) { 119 119 unsafe { 120 - crate::set_sleeptimer_duration(); 120 + crate::set_sleeptimer_duration(minutes); 121 121 } 122 122 } 123 123
+46
crates/sys/src/types/mp3_entry.rs
··· 1 + use std::env; 2 + 1 3 use crate::cast_ptr; 2 4 use crate::get_string_from_ptr; 5 + use rockbox_traits::types::track::Track; 3 6 use serde::{Deserialize, Serialize}; 7 + use url::Url; 4 8 5 9 #[derive(Debug, Default, Serialize, Deserialize, Clone)] 6 10 pub struct Mp3Entry { ··· 134 138 } 135 139 } 136 140 } 141 + 142 + impl From<Track> for Mp3Entry { 143 + fn from(track: Track) -> Self { 144 + let client = reqwest::blocking::Client::new(); 145 + let album_art = match track.album_cover { 146 + Some(cover) => { 147 + let url = Url::parse(&cover).unwrap(); 148 + let path = url.path(); 149 + match client 150 + .get(&format!( 151 + "{}:{}{}", 152 + "http://localhost", 153 + env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or("6062".to_string()), 154 + path 155 + )) 156 + .send() 157 + { 158 + Ok(response) => match response.status() { 159 + reqwest::StatusCode::OK => Some(format!( 160 + "http://localhost:{}{}", 161 + env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or("6062".to_string()), 162 + path 163 + )), 164 + _ => Some(cover), 165 + }, 166 + Err(_) => Some(cover), 167 + } 168 + } 169 + None => None, 170 + }; 171 + Self { 172 + title: track.title, 173 + artist: track.artist, 174 + album: track.album, 175 + albumartist: track.album_artist.unwrap_or_default(), 176 + path: track.path, 177 + album_art, 178 + length: (track.duration.unwrap_or_default() as u64) * 1000 as u64, 179 + ..Default::default() 180 + } 181 + } 182 + }
+7 -2
crates/traits/src/types/track.rs
··· 15 15 pub struct Track { 16 16 pub id: String, 17 17 pub uri: String, 18 + pub path: String, 18 19 pub title: String, 19 - pub artists: Vec<Artist>, 20 - pub album: Option<Album>, 20 + pub artist: String, 21 + pub album: String, 21 22 pub track_number: Option<u32>, 22 23 pub disc_number: u32, 23 24 pub duration: Option<f32>, 25 + pub album_artist: Option<String>, 26 + pub album_cover: Option<String>, 27 + pub album_id: Option<String>, 28 + pub artist_id: Option<String>, 24 29 }
+8
crates/types/src/lib.rs
··· 22 22 } 23 23 24 24 #[derive(Debug, Serialize, Deserialize)] 25 + pub struct LoadTracks { 26 + pub tracks: Vec<String>, 27 + pub directory: Option<String>, 28 + pub album_id: Option<String>, 29 + pub shuffle: Option<bool>, 30 + } 31 + 32 + #[derive(Debug, Serialize, Deserialize)] 25 33 pub struct NewVolume { 26 34 pub steps: i32, 27 35 }
+2 -2
firmware/powermgmt.c
··· 993 993 } 994 994 else { 995 995 DEBUGF("Sleep timer timeout. Shutting off...\n"); 996 - sys_poweroff(); 996 + // sys_poweroff(); 997 997 } 998 998 } 999 999 } ··· 1049 1049 && TIME_AFTER(tick, storage_last_disk_activity() + timeout) 1050 1050 #endif 1051 1051 ) { 1052 - sys_poweroff(); 1052 + // sys_poweroff(); 1053 1053 } 1054 1054 } else 1055 1055 handle_sleep_timer();
+115
webui/chromecast/src/Assets/rockbox-clef.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + xmlns:dc="http://purl.org/dc/elements/1.1/" 6 + xmlns:cc="http://creativecommons.org/ns#" 7 + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 + xmlns:svg="http://www.w3.org/2000/svg" 9 + xmlns="http://www.w3.org/2000/svg" 10 + xmlns:xlink="http://www.w3.org/1999/xlink" 11 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 12 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 13 + version="1.1" 14 + width="120" 15 + height="120" 16 + viewBox="0 0 3386.6665 3386.6666" 17 + id="svg2" 18 + xml:space="preserve" 19 + style="fill-rule:evenodd" 20 + inkscape:version="0.48.1 r9760" 21 + sodipodi:docname="rockbox-clef.svg"><metadata 22 + id="metadata18"><rdf:RDF><cc:Work 23 + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type 24 + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><sodipodi:namedview 25 + pagecolor="#ffffff" 26 + bordercolor="#666666" 27 + borderopacity="1" 28 + objecttolerance="10" 29 + gridtolerance="10" 30 + guidetolerance="10" 31 + inkscape:pageopacity="0" 32 + inkscape:pageshadow="2" 33 + inkscape:window-width="864" 34 + inkscape:window-height="720" 35 + id="namedview16" 36 + showgrid="true" 37 + inkscape:zoom="4" 38 + inkscape:cx="82.5729" 39 + inkscape:cy="69.674808" 40 + inkscape:window-x="0" 41 + inkscape:window-y="24" 42 + inkscape:window-maximized="0" 43 + inkscape:current-layer="Ebene_x0020_1"><inkscape:grid 44 + type="xygrid" 45 + id="grid2993" 46 + empspacing="5" 47 + visible="true" 48 + enabled="true" 49 + snapvisiblegridlinesonly="true" /></sodipodi:namedview><defs 50 + id="defs38"><linearGradient 51 + id="linearGradient3657"><stop 52 + id="stop3659" 53 + style="stop-color:#aa8800;stop-opacity:1" 54 + offset="0" /><stop 55 + id="stop3661" 56 + style="stop-color:#aa8800;stop-opacity:0" 57 + offset="1" /></linearGradient><linearGradient 58 + x1="-89.260162" 59 + y1="-2.1270833" 60 + x2="-14.333748" 61 + y2="85.830009" 62 + id="linearGradient3663" 63 + xlink:href="#linearGradient3657" 64 + gradientUnits="userSpaceOnUse" /></defs> 65 + <g 66 + transform="matrix(0.90063697,0,0,0.88724946,748.25202,-1317.6084)" 67 + id="Ebene_x0020_1"> 68 + <defs 69 + id="defs5"> 70 + <linearGradient 71 + x1="17608" 72 + y1="4190.54" 73 + x2="17715.699" 74 + y2="4801.2798" 75 + id="id0" 76 + gradientUnits="userSpaceOnUse"> 77 + <stop 78 + id="stop8" 79 + style="stop-color:#a67d00;stop-opacity:1" 80 + offset="0" /> 81 + 82 + <stop 83 + id="stop12" 84 + style="stop-color:#ffffff;stop-opacity:1" 85 + offset="1" /> 86 + </linearGradient> 87 + </defs> 88 + 89 + 90 + 91 + 92 + 93 + <rect 94 + width="3133.5845" 95 + height="3180.8667" 96 + ry="302.44305" 97 + x="-517.4447" 98 + y="1803.135" 99 + id="rect3694" 100 + style="fill:#ffc001;fill-opacity:1;fill-rule:evenodd;stroke:none" /><path 101 + d="m 1133.6395,3200.4269 c 11.2559,18.2986 22.432,36.6011 33.5473,54.9487 60.3406,-16.5847 123.9814,-25.3702 188.4648,-26.059 126.1999,-1.316 246.2441,48.6846 317.2687,132.1614 64.2026,75.4539 101.1079,162.6951 106.6733,252.1193 2.3212,37.3762 -5.6502,74.466 -23.3869,108.7376 -43.5526,84.192 -117.9946,155.791 -214.1337,205.9842 45.7615,89.9291 90.0403,180.2716 132.8495,271.0265 81.8832,173.6387 132.6702,354.6655 150.8198,537.8489 4.676,47.2371 -2.5858,94.4734 -21.3927,138.9539 -19.2847,45.5742 -58.1673,84.1121 -110.1785,109.1541 -37.7378,18.1621 -83.751,23.2435 -127.4177,14.0995 -43.6802,-9.1436 -81.2498,-31.7365 -104.0476,-62.5664 -26.5203,-35.904 -42.2855,-75.8627 -46.0345,-116.7994 -2.1173,-23.0223 9.3653,-45.2202 31.1441,-60.2062 27.4567,-18.8993 62.7411,-29.5144 99.7917,-30.07 24.3266,-0.3368 47.3936,9.6188 60.3568,26.0881 12.9472,16.4494 13.7396,36.8489 2.1096,53.3337 -14.3314,20.3045 -37.0946,36.0653 -64.6429,44.7438 l 11.8035,16.1533 c 5.7557,9.2105 16.8132,15.7847 29.6363,17.5515 12.8112,1.7777 25.7362,-1.4481 34.5823,-8.6399 43.6788,-34.3792 67.7315,-81.3042 66.8875,-130.5556 -3.562,-84.7728 -16.3171,-169.3555 -38.1543,-252.8527 -55.7305,-177.8644 -127.5412,-352.777 -214.8999,-523.3512 -16.3737,5.1045 -33.0607,9.6178 -49.9908,13.5678 -49.5652,11.5132 -99.9157,21.0976 -150.8427,28.7217 l -100.813,-814.0936 z m 248.7771,672.5156 c -50.5142,19.7875 -104.412,34.1161 -160.2241,42.5737 l -50.7294,-409.6546 c 72.2514,121.1726 142.3288,243.0983 210.2123,365.7262 0.2567,0.458 0.4978,0.8959 0.7412,1.3547 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0,0 m -150.076,-508.6879 c 17.5147,-4.8294 35.3415,-9.0782 53.3625,-12.729 56.5362,-11.4397 117.3362,-3.637 167.586,21.5055 45.7565,22.8959 80.8463,56.9138 99.7551,96.6234 42.1044,88.5079 47.194,184.4321 14.5179,273.6257 -6.8456,18.7164 -19.8851,35.5583 -37.8052,48.8537 -13.7547,10.2054 -28.1066,19.8974 -43.0038,29.0629 -80.5314,-153.6773 -165.3681,-306.0385 -254.4125,-456.9422 m -10.1481,551.2616 c -14.298,2.1559 -28.7086,3.9408 -43.196,5.3209 -157.9011,15.1267 -314.30239,-47.8917 -393.33817,-158.4708 -140.27886,-196.2782 -185.31029,-423.8857 -126.59762,-639.7364 20.62766,-75.767 42.25356,-151.3353 64.91887,-226.6963 6.26181,-20.8439 12.63094,-41.6829 19.09409,-62.5163 104.89649,154.512 206.20813,310.449 303.84503,467.7319 -104.88444,52.3821 -185.1784,129.7162 -229.58648,221.109 -7.33244,15.1065 -1.5786,32.4892 14.38626,43.3632 15.95163,10.8746 38.6339,12.8768 56.5858,5.0072 15.19318,-10.1136 29.0455,-21.3616 41.40015,-33.6098 49.89688,-51.7003 111.96607,-95.3657 183.00107,-128.7738 19.7486,32.4884 39.3155,65.0179 58.7576,97.6166 l -37.8236,-305.4347 c -92.6263,-150.6417 -189.46344,-299.7421 -290.47048,-447.1875 -16.73566,-24.4224 -33.73799,-48.7363 -51.00315,-72.9107 12.53936,-37.7911 25.36472,-75.5343 38.46801,-113.1874 18.72476,-53.759 31.27196,-108.8276 37.52831,-164.6129 7.04724,-63.0773 -7.52517,-127.0079 -42.2481,-185.2058 -79.1109,-132.6193 -213.39469,-241.1725 -380.18823,-307.3322 -73.37553,-29.1121 -155.31083,-41.0734 -235.85269,-34.4583 -67.48248,5.551 -116.971987,50.5243 -114.340324,103.8724 2.381016,48.4159 22.574124,95.696 58.086904,136.1456 32.88527,37.4341 67.82132,73.7652 104.71321,108.8721 113.82257,108.3303 214.95374,224.3585 302.12809,346.5898 17.13041,24.0557 34.19842,48.1462 51.17499,72.252 -15.99656,48.3246 -31.52841,96.7397 -46.59165,145.277 -69.85813,225.1541 -104.21032,456.9898 -102.24126,690.3515 0.92822,110.1406 26.98496,219.8501 76.97168,323.9736 68.55723,142.8242 245.98138,237.2059 438.57406,233.2775 86.18513,-1.7571 171.82603,-8.9862 256.10363,-21.6231 l -12.26,-99.0043" 102 + id="path24" 103 + style="fill:#000000;fill-opacity:1;fill-rule:evenodd" 104 + inkscape:connector-curvature="0" /> 105 + 106 + 107 + <path 108 + d="M 659.33533,2503.3767 C 573.5524,2394.5355 482.17708,2288.3563 385.43749,2185.1728 c -30.76545,-32.8099 -59.12968,-66.9092 -84.95666,-102.1687 -16.86386,-22.98 -28.28033,-48.07 -33.73308,-74.0078 -1.1599,-5.5966 0.71447,-11.2481 5.21013,-15.5497 4.45467,-4.3102 11.12647,-6.8716 18.3068,-7.0503 34.58075,-0.8971 69.10856,5.1026 100.42147,17.453 134.57561,53.071 237.34107,146.2148 284.58433,257.9109 13.77151,32.583 18.33147,66.9284 13.33714,100.5928 -7.04096,47.4029 -16.80734,94.4462 -29.27229,141.0237 l 0,0" 109 + id="path30" 110 + style="fill:#ffc000" 111 + inkscape:connector-curvature="0" /> 112 + 113 + 114 + </g> 115 + </svg>
webui/chromecast/src/Assets/rockbox-icon.png

This is a binary file and will not be displayed.

+20 -2
webui/chromecast/src/Components/Header/Header.jsx
··· 1 1 import styled from "@emotion/styled"; 2 + import Rockbox from "../../Assets/rockbox-icon.png"; 2 3 3 4 const Container = styled.div` 4 5 height: 70px; ··· 7 8 padding-right: 5vw; 8 9 display: flex; 9 10 align-items: center; 11 + margin-top: 20px; 10 12 `; 11 13 12 - const Logo = styled.div` 14 + const LogoText = styled.div` 13 15 font-family: RockfordSansBold; 16 + margin-left: 10px; 17 + `; 18 + 19 + const Logo = styled.img` 20 + height: 40px; 21 + width: 40px; 22 + `; 23 + 24 + const Row = styled.div` 25 + display: flex; 26 + flex-direction: row; 27 + align-items: center; 28 + height: 70px; 14 29 `; 15 30 16 31 const Header = () => { 17 32 return ( 18 33 <Container> 19 - <Logo>Music Player</Logo> 34 + <Row> 35 + <Logo src={Rockbox} /> 36 + <LogoText>Rockbox</LogoText> 37 + </Row> 20 38 </Container> 21 39 ); 22 40 };