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 #148 from tsirysndr/feat/playlists

Add saved and smart playlists support

authored by

Tsiry Sandratraina and committed by
GitHub
463876ba db5c8ef6

+10903 -64
+52 -28
Cargo.lock
··· 417 417 checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 418 418 419 419 [[package]] 420 - name = "android-tzdata" 421 - version = "0.1.1" 422 - source = "registry+https://github.com/rust-lang/crates.io-index" 423 - checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 424 - 425 - [[package]] 426 420 name = "android_system_properties" 427 421 version = "0.1.5" 428 422 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1110 1104 "addr2line", 1111 1105 "cfg-if 1.0.0", 1112 1106 "libc", 1113 - "miniz_oxide 0.8.0", 1107 + "miniz_oxide 0.8.9", 1114 1108 "object", 1115 1109 "rustc-demangle", 1116 1110 "windows-targets 0.52.6", ··· 1428 1422 1429 1423 [[package]] 1430 1424 name = "bumpalo" 1431 - version = "3.16.0" 1425 + version = "3.20.2" 1432 1426 source = "registry+https://github.com/rust-lang/crates.io-index" 1433 - checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 1427 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 1434 1428 dependencies = [ 1435 1429 "allocator-api2", 1436 1430 ] ··· 1661 1655 1662 1656 [[package]] 1663 1657 name = "chrono" 1664 - version = "0.4.38" 1658 + version = "0.4.44" 1665 1659 source = "registry+https://github.com/rust-lang/crates.io-index" 1666 - checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 1660 + checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" 1667 1661 dependencies = [ 1668 - "android-tzdata", 1669 1662 "iana-time-zone", 1670 1663 "js-sys", 1671 1664 "num-traits", 1672 1665 "serde", 1673 1666 "wasm-bindgen", 1674 - "windows-targets 0.52.6", 1667 + "windows-link", 1675 1668 ] 1676 1669 1677 1670 [[package]] ··· 4407 4400 "bit_field", 4408 4401 "half", 4409 4402 "lebe", 4410 - "miniz_oxide 0.8.0", 4403 + "miniz_oxide 0.8.9", 4411 4404 "rayon-core", 4412 4405 "smallvec", 4413 4406 "zune-inflate", ··· 4587 4580 4588 4581 [[package]] 4589 4582 name = "flate2" 4590 - version = "1.0.35" 4583 + version = "1.1.9" 4591 4584 source = "registry+https://github.com/rust-lang/crates.io-index" 4592 - checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 4585 + checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" 4593 4586 dependencies = [ 4594 4587 "crc32fast", 4595 4588 "libz-sys", 4596 - "miniz_oxide 0.8.0", 4589 + "miniz_oxide 0.8.9", 4597 4590 ] 4598 4591 4599 4592 [[package]] ··· 6759 6752 6760 6753 [[package]] 6761 6754 name = "log" 6762 - version = "0.4.22" 6755 + version = "0.4.29" 6763 6756 source = "registry+https://github.com/rust-lang/crates.io-index" 6764 - checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 6757 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 6765 6758 dependencies = [ 6766 - "serde", 6759 + "serde_core", 6767 6760 "value-bag", 6768 6761 ] 6769 6762 ··· 7003 6996 7004 6997 [[package]] 7005 6998 name = "miniz_oxide" 7006 - version = "0.8.0" 6999 + version = "0.8.9" 7007 7000 source = "registry+https://github.com/rust-lang/crates.io-index" 7008 - checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 7001 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 7009 7002 dependencies = [ 7010 7003 "adler2", 7004 + "simd-adler32", 7011 7005 ] 7012 7006 7013 7007 [[package]] ··· 9159 9153 "dirs 6.0.0", 9160 9154 "libc", 9161 9155 "owo-colors 4.1.0", 9156 + "reqwest", 9162 9157 "rockbox-airplay", 9163 9158 "rockbox-library", 9159 + "rockbox-playlists", 9164 9160 "rockbox-rocksky", 9165 9161 "rockbox-settings", 9166 9162 "rockbox-slim", ··· 9203 9199 "owo-colors 4.1.0", 9204 9200 "reqwest", 9205 9201 "rockbox-library", 9202 + "rockbox-playlists", 9206 9203 "rockbox-rocksky", 9207 9204 "rockbox-sys", 9208 9205 "rockbox-types", ··· 9304 9301 ] 9305 9302 9306 9303 [[package]] 9304 + name = "rockbox-playlists" 9305 + version = "0.1.0" 9306 + dependencies = [ 9307 + "anyhow", 9308 + "chrono", 9309 + "rand 0.8.5", 9310 + "serde", 9311 + "serde_json", 9312 + "sqlx", 9313 + "tokio", 9314 + "tracing", 9315 + "uuid", 9316 + ] 9317 + 9318 + [[package]] 9307 9319 name = "rockbox-rocksky" 9308 9320 version = "0.1.0" 9309 9321 dependencies = [ ··· 9344 9356 "reqwest", 9345 9357 "rockbox-graphql", 9346 9358 "rockbox-library", 9359 + "rockbox-playlists", 9347 9360 "rockbox-rocksky", 9348 9361 "rockbox-sys", 9349 9362 "rockbox-types", ··· 9381 9394 "rockbox-mpd", 9382 9395 "rockbox-mpris", 9383 9396 "rockbox-network", 9397 + "rockbox-playlists", 9384 9398 "rockbox-rocksky", 9385 9399 "rockbox-rpc", 9386 9400 "rockbox-settings", ··· 9951 9965 9952 9966 [[package]] 9953 9967 name = "serde" 9954 - version = "1.0.216" 9968 + version = "1.0.228" 9955 9969 source = "registry+https://github.com/rust-lang/crates.io-index" 9956 - checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 9970 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 9957 9971 dependencies = [ 9972 + "serde_core", 9958 9973 "serde_derive", 9959 9974 ] 9960 9975 ··· 9989 10004 ] 9990 10005 9991 10006 [[package]] 10007 + name = "serde_core" 10008 + version = "1.0.228" 10009 + source = "registry+https://github.com/rust-lang/crates.io-index" 10010 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 10011 + dependencies = [ 10012 + "serde_derive", 10013 + ] 10014 + 10015 + [[package]] 9992 10016 name = "serde_derive" 9993 - version = "1.0.216" 10017 + version = "1.0.228" 9994 10018 source = "registry+https://github.com/rust-lang/crates.io-index" 9995 - checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 10019 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 9996 10020 dependencies = [ 9997 10021 "proc-macro2", 9998 10022 "quote", ··· 12411 12435 12412 12436 [[package]] 12413 12437 name = "value-bag" 12414 - version = "1.10.0" 12438 + version = "1.12.0" 12415 12439 source = "registry+https://github.com/rust-lang/crates.io-index" 12416 - checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" 12440 + checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" 12417 12441 12418 12442 [[package]] 12419 12443 name = "value-trait"
+17
cli/src/api/rockbox.v1alpha1.rs
··· 516 516 pub term: ::prost::alloc::string::String, 517 517 } 518 518 #[derive(Clone, PartialEq, ::prost::Message)] 519 + pub struct SearchPlaylist { 520 + #[prost(string, tag = "1")] 521 + pub id: ::prost::alloc::string::String, 522 + #[prost(string, tag = "2")] 523 + pub name: ::prost::alloc::string::String, 524 + #[prost(string, optional, tag = "3")] 525 + pub description: ::core::option::Option<::prost::alloc::string::String>, 526 + #[prost(string, optional, tag = "4")] 527 + pub image: ::core::option::Option<::prost::alloc::string::String>, 528 + #[prost(bool, tag = "5")] 529 + pub is_smart: bool, 530 + #[prost(int64, tag = "6")] 531 + pub track_count: i64, 532 + } 533 + #[derive(Clone, PartialEq, ::prost::Message)] 519 534 pub struct SearchResponse { 520 535 #[prost(message, repeated, tag = "1")] 521 536 pub tracks: ::prost::alloc::vec::Vec<Track>, ··· 523 538 pub albums: ::prost::alloc::vec::Vec<Album>, 524 539 #[prost(message, repeated, tag = "3")] 525 540 pub artists: ::prost::alloc::vec::Vec<Artist>, 541 + #[prost(message, repeated, tag = "4")] 542 + pub playlists: ::prost::alloc::vec::Vec<SearchPlaylist>, 526 543 } 527 544 /// Generated client implementations. 528 545 pub mod library_service_client {
cli/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+2
crates/cli/Cargo.toml
··· 12 12 rockbox-slim = {path = "../slim"} 13 13 clap = "4.5.16" 14 14 owo-colors = "4.1.0" 15 + reqwest = { workspace = true, features = ["rustls-tls", "json"] } 15 16 rockbox-library = {path = "../library"} 17 + rockbox-playlists = {path = "../playlists"} 16 18 rockbox-typesense = {path = "../typesense"} 17 19 rockbox-settings = {path = "../settings"} 18 20 rockbox-rocksky = {path = "../rocksky"}
+56 -1
crates/cli/src/lib.rs
··· 7 7 use rockbox_airplay::_link_airplay as _; 8 8 use rockbox_library::audio_scan::{save_audio_metadata, scan_audio_files}; 9 9 use rockbox_library::{create_connection_pool, repo}; 10 + use rockbox_playlists::PlaylistStore; 10 11 #[allow(unused_imports)] 11 12 use rockbox_slim::_link_slim as _; 12 13 use rockbox_typesense::client::*; ··· 22 23 23 24 /// PID of the spawned typesense-server child, or -1 if not yet started. 24 25 static TYPESENSE_PID: AtomicI32 = AtomicI32::new(-1); 26 + 27 + /// Poll the Typesense health endpoint until it responds, giving the server 28 + /// time to start before any collection or indexing calls are made. 29 + async fn wait_for_typesense() { 30 + let port = std::env::var("RB_TYPESENSE_PORT").unwrap_or_else(|_| "8109".to_string()); 31 + let url = format!("http://localhost:{}/health", port); 32 + let client = reqwest::Client::new(); 33 + for attempt in 1..=30 { 34 + match client.get(&url).send().await { 35 + Ok(r) if r.status().is_success() => { 36 + info!("Typesense ready after {} attempt(s)", attempt); 37 + return; 38 + } 39 + _ => { 40 + tokio::time::sleep(Duration::from_secs(1)).await; 41 + } 42 + } 43 + } 44 + warn!("Typesense did not become ready in time; proceeding anyway"); 45 + } 25 46 26 47 /// SIGTERM/SIGINT handler: kill the typesense child then _exit immediately. 27 48 /// ··· 142 163 insert_artists(artists.into_iter().map(Artist::from).collect()).await?; 143 164 insert_albums(albums.into_iter().map(Album::from).collect()).await?; 144 165 } 166 + 167 + // Always sync playlists on startup so the collection exists even when the 168 + // library scan was skipped. Wait for Typesense to be ready first since it 169 + // may still be starting when tracks are already indexed (no scan delay). 170 + wait_for_typesense().await; 171 + create_playlists_collection().await?; 172 + let playlist_store = PlaylistStore::new(pool.clone()); 173 + let saved = playlist_store.list().await.unwrap_or_default(); 174 + let smart = playlist_store 175 + .list_smart_playlists() 176 + .await 177 + .unwrap_or_default(); 178 + let ts_playlists: Vec<Playlist> = saved 179 + .into_iter() 180 + .map(|p| Playlist { 181 + id: p.id, 182 + name: p.name, 183 + description: p.description, 184 + image: p.image, 185 + is_smart: false, 186 + track_count: p.track_count, 187 + }) 188 + .chain(smart.into_iter().map(|p| Playlist { 189 + id: p.id, 190 + name: p.name, 191 + description: p.description, 192 + image: p.image, 193 + is_smart: true, 194 + track_count: 0, 195 + })) 196 + .collect(); 197 + if !ts_playlists.is_empty() { 198 + insert_playlists(ts_playlists).await?; 199 + } 145 200 Ok::<(), Error>(()) 146 201 }) 147 202 .unwrap(); ··· 150 205 sleep(Duration::from_secs(5)); 151 206 match rockbox_rocksky::register_rockbox() { 152 207 Ok(_) => info!("Successfully registered Rockbox with Rocksky server"), 153 - Err(e) => error!("Failed to register Rockbox with Rocksky server: {}", e), 208 + Err(e) => tracing::debug!("Failed to register Rockbox with Rocksky server: {}", e), 154 209 }; 155 210 }); 156 211
+1
crates/graphql/Cargo.toml
··· 20 20 owo-colors = "4.1.0" 21 21 reqwest = {version = "0.12.5", features = ["rustls-tls", "json"], default-features = false} 22 22 rockbox-library = {path = "../library"} 23 + rockbox-playlists = {path = "../playlists"} 23 24 rockbox-rocksky = {path = "../rocksky"} 24 25 rockbox-typesense = {path = "../typesense"} 25 26 rockbox-sys = {path = "../sys"}
+8
crates/graphql/src/schema/mod.rs
··· 4 4 use library::{LibraryMutation, LibraryQuery}; 5 5 use playback::{PlaybackMutation, PlaybackQuery, PlaybackSubscription}; 6 6 use playlist::{PlaylistMutation, PlaylistQuery, PlaylistSubscription}; 7 + use saved_playlist::{SavedPlaylistMutation, SavedPlaylistQuery}; 7 8 use settings::{SettingsMutation, SettingsQuery}; 9 + use smart_playlist::{SmartPlaylistMutation, SmartPlaylistQuery}; 8 10 use sound::{SoundMutation, SoundQuery}; 9 11 use system::SystemQuery; 10 12 ··· 15 17 pub mod objects; 16 18 pub mod playback; 17 19 pub mod playlist; 20 + pub mod saved_playlist; 18 21 pub mod settings; 22 + pub mod smart_playlist; 19 23 pub mod sound; 20 24 pub mod system; 21 25 ··· 26 30 LibraryQuery, 27 31 PlaybackQuery, 28 32 PlaylistQuery, 33 + SavedPlaylistQuery, 34 + SmartPlaylistQuery, 29 35 SoundQuery, 30 36 SettingsQuery, 31 37 SystemQuery, ··· 36 42 DeviceMutation, 37 43 PlaybackMutation, 38 44 PlaylistMutation, 45 + SavedPlaylistMutation, 46 + SmartPlaylistMutation, 39 47 SoundMutation, 40 48 LibraryMutation, 41 49 SettingsMutation,
+2
crates/graphql/src/schema/objects/mod.rs
··· 8 8 pub mod new_global_settings; 9 9 pub mod playlist; 10 10 pub mod replaygain_settings; 11 + pub mod saved_playlist; 11 12 pub mod search; 12 13 pub mod settings_list; 14 + pub mod smart_playlist; 13 15 pub mod system_status; 14 16 pub mod track; 15 17 pub mod user_settings;
+92
crates/graphql/src/schema/objects/saved_playlist.rs
··· 1 + use async_graphql::*; 2 + use serde::Serialize; 3 + 4 + #[derive(Default, Clone, Serialize)] 5 + pub struct SavedPlaylistFolder { 6 + pub id: String, 7 + pub name: String, 8 + pub created_at: i64, 9 + pub updated_at: i64, 10 + } 11 + 12 + #[Object] 13 + impl SavedPlaylistFolder { 14 + async fn id(&self) -> &str { 15 + &self.id 16 + } 17 + async fn name(&self) -> &str { 18 + &self.name 19 + } 20 + async fn created_at(&self) -> i64 { 21 + self.created_at 22 + } 23 + async fn updated_at(&self) -> i64 { 24 + self.updated_at 25 + } 26 + } 27 + 28 + #[derive(Default, Clone, Serialize)] 29 + pub struct SavedPlaylist { 30 + pub id: String, 31 + pub name: String, 32 + pub description: Option<String>, 33 + pub image: Option<String>, 34 + pub folder_id: Option<String>, 35 + pub track_count: i64, 36 + pub created_at: i64, 37 + pub updated_at: i64, 38 + } 39 + 40 + #[Object] 41 + impl SavedPlaylist { 42 + async fn id(&self) -> &str { 43 + &self.id 44 + } 45 + async fn name(&self) -> &str { 46 + &self.name 47 + } 48 + async fn description(&self) -> Option<&str> { 49 + self.description.as_deref() 50 + } 51 + async fn image(&self) -> Option<&str> { 52 + self.image.as_deref() 53 + } 54 + async fn folder_id(&self) -> Option<&str> { 55 + self.folder_id.as_deref() 56 + } 57 + async fn track_count(&self) -> i64 { 58 + self.track_count 59 + } 60 + async fn created_at(&self) -> i64 { 61 + self.created_at 62 + } 63 + async fn updated_at(&self) -> i64 { 64 + self.updated_at 65 + } 66 + } 67 + 68 + impl From<rockbox_playlists::Playlist> for SavedPlaylist { 69 + fn from(p: rockbox_playlists::Playlist) -> Self { 70 + Self { 71 + id: p.id, 72 + name: p.name, 73 + description: p.description, 74 + image: p.image, 75 + folder_id: p.folder_id, 76 + track_count: p.track_count, 77 + created_at: p.created_at, 78 + updated_at: p.updated_at, 79 + } 80 + } 81 + } 82 + 83 + impl From<rockbox_playlists::PlaylistFolder> for SavedPlaylistFolder { 84 + fn from(f: rockbox_playlists::PlaylistFolder) -> Self { 85 + Self { 86 + id: f.id, 87 + name: f.name, 88 + created_at: f.created_at, 89 + updated_at: f.updated_at, 90 + } 91 + } 92 + }
+55
crates/graphql/src/schema/objects/smart_playlist.rs
··· 1 + use async_graphql::*; 2 + use rockbox_playlists::{SmartPlaylist as RsSmartPlaylist, TrackStats as RsTrackStats}; 3 + use serde::Serialize; 4 + 5 + #[derive(Default, Clone, Serialize, SimpleObject)] 6 + pub struct SmartPlaylist { 7 + pub id: String, 8 + pub name: String, 9 + pub description: Option<String>, 10 + pub image: Option<String>, 11 + pub folder_id: Option<String>, 12 + pub is_system: bool, 13 + pub rules: String, 14 + pub created_at: i64, 15 + pub updated_at: i64, 16 + } 17 + 18 + #[derive(Default, Clone, Serialize, SimpleObject)] 19 + pub struct TrackStats { 20 + pub track_id: String, 21 + pub play_count: i64, 22 + pub skip_count: i64, 23 + pub last_played: Option<i64>, 24 + pub last_skipped: Option<i64>, 25 + pub updated_at: i64, 26 + } 27 + 28 + impl From<RsSmartPlaylist> for SmartPlaylist { 29 + fn from(p: RsSmartPlaylist) -> Self { 30 + Self { 31 + id: p.id, 32 + name: p.name, 33 + description: p.description, 34 + image: p.image, 35 + folder_id: p.folder_id, 36 + is_system: p.is_system, 37 + rules: serde_json::to_string(&p.rules).unwrap_or_default(), 38 + created_at: p.created_at, 39 + updated_at: p.updated_at, 40 + } 41 + } 42 + } 43 + 44 + impl From<RsTrackStats> for TrackStats { 45 + fn from(s: RsTrackStats) -> Self { 46 + Self { 47 + track_id: s.track_id, 48 + play_count: s.play_count, 49 + skip_count: s.skip_count, 50 + last_played: s.last_played, 51 + last_skipped: s.last_skipped, 52 + updated_at: s.updated_at, 53 + } 54 + } 55 + }
+206
crates/graphql/src/schema/saved_playlist.rs
··· 1 + use async_graphql::*; 2 + use rockbox_playlists::{Playlist, PlaylistFolder}; 3 + 4 + use crate::{ 5 + rockbox_url, 6 + schema::objects::saved_playlist::{SavedPlaylist, SavedPlaylistFolder}, 7 + }; 8 + 9 + #[derive(Default)] 10 + pub struct SavedPlaylistQuery; 11 + 12 + #[Object] 13 + impl SavedPlaylistQuery { 14 + async fn saved_playlists( 15 + &self, 16 + ctx: &Context<'_>, 17 + folder_id: Option<String>, 18 + ) -> Result<Vec<SavedPlaylist>, Error> { 19 + let client = ctx.data::<reqwest::Client>()?; 20 + let mut url = format!("{}/saved-playlists", rockbox_url()); 21 + if let Some(fid) = folder_id.as_deref() { 22 + if !fid.is_empty() { 23 + url = format!("{}?folder_id={}", url, fid); 24 + } 25 + } 26 + let playlists = client 27 + .get(&url) 28 + .send() 29 + .await? 30 + .json::<Vec<Playlist>>() 31 + .await?; 32 + Ok(playlists.into_iter().map(SavedPlaylist::from).collect()) 33 + } 34 + 35 + async fn saved_playlist( 36 + &self, 37 + ctx: &Context<'_>, 38 + id: String, 39 + ) -> Result<Option<SavedPlaylist>, Error> { 40 + let client = ctx.data::<reqwest::Client>()?; 41 + let url = format!("{}/saved-playlists/{}", rockbox_url(), id); 42 + let resp = client.get(&url).send().await?; 43 + if resp.status().as_u16() == 404 { 44 + return Ok(None); 45 + } 46 + Ok(Some(SavedPlaylist::from(resp.json::<Playlist>().await?))) 47 + } 48 + 49 + async fn saved_playlist_track_ids( 50 + &self, 51 + ctx: &Context<'_>, 52 + playlist_id: String, 53 + ) -> Result<Vec<String>, Error> { 54 + let client = ctx.data::<reqwest::Client>()?; 55 + let url = format!( 56 + "{}/saved-playlists/{}/track-ids", 57 + rockbox_url(), 58 + playlist_id 59 + ); 60 + Ok(client.get(&url).send().await?.json::<Vec<String>>().await?) 61 + } 62 + 63 + async fn playlist_folders(&self, ctx: &Context<'_>) -> Result<Vec<SavedPlaylistFolder>, Error> { 64 + let client = ctx.data::<reqwest::Client>()?; 65 + let url = format!("{}/saved-playlists/folders", rockbox_url()); 66 + let folders = client 67 + .get(&url) 68 + .send() 69 + .await? 70 + .json::<Vec<PlaylistFolder>>() 71 + .await?; 72 + Ok(folders.into_iter().map(SavedPlaylistFolder::from).collect()) 73 + } 74 + } 75 + 76 + #[derive(Default)] 77 + pub struct SavedPlaylistMutation; 78 + 79 + #[Object] 80 + impl SavedPlaylistMutation { 81 + async fn create_playlist_folder( 82 + &self, 83 + ctx: &Context<'_>, 84 + name: String, 85 + ) -> Result<SavedPlaylistFolder, Error> { 86 + let client = ctx.data::<reqwest::Client>()?; 87 + let url = format!("{}/saved-playlists/folders", rockbox_url()); 88 + let folder = client 89 + .post(&url) 90 + .json(&serde_json::json!({ "name": name })) 91 + .send() 92 + .await? 93 + .json::<PlaylistFolder>() 94 + .await?; 95 + Ok(SavedPlaylistFolder::from(folder)) 96 + } 97 + 98 + async fn delete_playlist_folder(&self, ctx: &Context<'_>, id: String) -> Result<bool, Error> { 99 + let client = ctx.data::<reqwest::Client>()?; 100 + let url = format!("{}/saved-playlists/folders/{}", rockbox_url(), id); 101 + client.delete(&url).send().await?; 102 + Ok(true) 103 + } 104 + 105 + async fn create_saved_playlist( 106 + &self, 107 + ctx: &Context<'_>, 108 + name: String, 109 + description: Option<String>, 110 + image: Option<String>, 111 + folder_id: Option<String>, 112 + track_ids: Option<Vec<String>>, 113 + ) -> Result<SavedPlaylist, Error> { 114 + let client = ctx.data::<reqwest::Client>()?; 115 + let url = format!("{}/saved-playlists", rockbox_url()); 116 + let playlist = client 117 + .post(&url) 118 + .json(&serde_json::json!({ 119 + "name": name, 120 + "description": description, 121 + "image": image, 122 + "folder_id": folder_id, 123 + "track_ids": track_ids, 124 + })) 125 + .send() 126 + .await? 127 + .json::<Playlist>() 128 + .await?; 129 + Ok(SavedPlaylist::from(playlist)) 130 + } 131 + 132 + async fn update_saved_playlist( 133 + &self, 134 + ctx: &Context<'_>, 135 + id: String, 136 + name: String, 137 + description: Option<String>, 138 + image: Option<String>, 139 + folder_id: Option<String>, 140 + ) -> Result<bool, Error> { 141 + let client = ctx.data::<reqwest::Client>()?; 142 + let url = format!("{}/saved-playlists/{}", rockbox_url(), id); 143 + client 144 + .put(&url) 145 + .json(&serde_json::json!({ 146 + "name": name, 147 + "description": description, 148 + "image": image, 149 + "folder_id": folder_id, 150 + })) 151 + .send() 152 + .await?; 153 + Ok(true) 154 + } 155 + 156 + async fn delete_saved_playlist(&self, ctx: &Context<'_>, id: String) -> Result<bool, Error> { 157 + let client = ctx.data::<reqwest::Client>()?; 158 + let url = format!("{}/saved-playlists/{}", rockbox_url(), id); 159 + client.delete(&url).send().await?; 160 + Ok(true) 161 + } 162 + 163 + async fn add_tracks_to_saved_playlist( 164 + &self, 165 + ctx: &Context<'_>, 166 + playlist_id: String, 167 + track_ids: Vec<String>, 168 + ) -> Result<bool, Error> { 169 + let client = ctx.data::<reqwest::Client>()?; 170 + let url = format!("{}/saved-playlists/{}/tracks", rockbox_url(), playlist_id); 171 + client 172 + .post(&url) 173 + .json(&serde_json::json!({ "track_ids": track_ids })) 174 + .send() 175 + .await?; 176 + Ok(true) 177 + } 178 + 179 + async fn remove_track_from_saved_playlist( 180 + &self, 181 + ctx: &Context<'_>, 182 + playlist_id: String, 183 + track_id: String, 184 + ) -> Result<bool, Error> { 185 + let client = ctx.data::<reqwest::Client>()?; 186 + let url = format!( 187 + "{}/saved-playlists/{}/tracks/{}", 188 + rockbox_url(), 189 + playlist_id, 190 + track_id 191 + ); 192 + client.delete(&url).send().await?; 193 + Ok(true) 194 + } 195 + 196 + async fn play_saved_playlist( 197 + &self, 198 + ctx: &Context<'_>, 199 + playlist_id: String, 200 + ) -> Result<bool, Error> { 201 + let client = ctx.data::<reqwest::Client>()?; 202 + let url = format!("{}/saved-playlists/{}/play", rockbox_url(), playlist_id); 203 + client.post(&url).send().await?; 204 + Ok(true) 205 + } 206 + }
+173
crates/graphql/src/schema/smart_playlist.rs
··· 1 + use async_graphql::*; 2 + use rockbox_playlists::SmartPlaylist as RsSmartPlaylist; 3 + 4 + use crate::{ 5 + rockbox_url, 6 + schema::objects::smart_playlist::{SmartPlaylist, TrackStats}, 7 + }; 8 + 9 + #[derive(Default)] 10 + pub struct SmartPlaylistQuery; 11 + 12 + #[Object] 13 + impl SmartPlaylistQuery { 14 + async fn smart_playlists(&self, ctx: &Context<'_>) -> Result<Vec<SmartPlaylist>, Error> { 15 + let client = ctx.data::<reqwest::Client>()?; 16 + let url = format!("{}/smart-playlists", rockbox_url()); 17 + let playlists = client 18 + .get(&url) 19 + .send() 20 + .await? 21 + .json::<Vec<RsSmartPlaylist>>() 22 + .await?; 23 + Ok(playlists.into_iter().map(SmartPlaylist::from).collect()) 24 + } 25 + 26 + async fn smart_playlist( 27 + &self, 28 + ctx: &Context<'_>, 29 + id: String, 30 + ) -> Result<Option<SmartPlaylist>, Error> { 31 + let client = ctx.data::<reqwest::Client>()?; 32 + let url = format!("{}/smart-playlists/{}", rockbox_url(), id); 33 + let resp = client.get(&url).send().await?; 34 + if resp.status().as_u16() == 404 { 35 + return Ok(None); 36 + } 37 + Ok(Some(SmartPlaylist::from( 38 + resp.json::<RsSmartPlaylist>().await?, 39 + ))) 40 + } 41 + 42 + async fn smart_playlist_track_ids( 43 + &self, 44 + ctx: &Context<'_>, 45 + id: String, 46 + ) -> Result<Vec<String>, Error> { 47 + let client = ctx.data::<reqwest::Client>()?; 48 + let url = format!("{}/smart-playlists/{}/tracks", rockbox_url(), id); 49 + let tracks = client 50 + .get(&url) 51 + .send() 52 + .await? 53 + .json::<Vec<serde_json::Value>>() 54 + .await?; 55 + Ok(tracks 56 + .into_iter() 57 + .filter_map(|t| t.get("id").and_then(|v| v.as_str()).map(|s| s.to_string())) 58 + .collect()) 59 + } 60 + 61 + async fn track_stats( 62 + &self, 63 + ctx: &Context<'_>, 64 + track_id: String, 65 + ) -> Result<Option<TrackStats>, Error> { 66 + let client = ctx.data::<reqwest::Client>()?; 67 + let url = format!("{}/track-stats/{}", rockbox_url(), track_id); 68 + let resp = client.get(&url).send().await?; 69 + if resp.status().as_u16() == 404 { 70 + return Ok(None); 71 + } 72 + Ok(Some(TrackStats::from( 73 + resp.json::<rockbox_playlists::TrackStats>().await?, 74 + ))) 75 + } 76 + } 77 + 78 + #[derive(Default)] 79 + pub struct SmartPlaylistMutation; 80 + 81 + #[Object] 82 + impl SmartPlaylistMutation { 83 + async fn create_smart_playlist( 84 + &self, 85 + ctx: &Context<'_>, 86 + name: String, 87 + description: Option<String>, 88 + image: Option<String>, 89 + folder_id: Option<String>, 90 + rules: String, 91 + ) -> Result<SmartPlaylist, Error> { 92 + let client = ctx.data::<reqwest::Client>()?; 93 + let rules_val: serde_json::Value = serde_json::from_str(&rules)?; 94 + let url = format!("{}/smart-playlists", rockbox_url()); 95 + let playlist = client 96 + .post(&url) 97 + .json(&serde_json::json!({ 98 + "name": name, 99 + "description": description, 100 + "image": image, 101 + "folder_id": folder_id, 102 + "rules": rules_val, 103 + })) 104 + .send() 105 + .await? 106 + .json::<RsSmartPlaylist>() 107 + .await?; 108 + Ok(SmartPlaylist::from(playlist)) 109 + } 110 + 111 + async fn update_smart_playlist( 112 + &self, 113 + ctx: &Context<'_>, 114 + id: String, 115 + name: String, 116 + description: Option<String>, 117 + image: Option<String>, 118 + folder_id: Option<String>, 119 + rules: String, 120 + ) -> Result<bool, Error> { 121 + let client = ctx.data::<reqwest::Client>()?; 122 + let rules_val: serde_json::Value = serde_json::from_str(&rules)?; 123 + let url = format!("{}/smart-playlists/{}", rockbox_url(), id); 124 + client 125 + .put(&url) 126 + .json(&serde_json::json!({ 127 + "name": name, 128 + "description": description, 129 + "image": image, 130 + "folder_id": folder_id, 131 + "rules": rules_val, 132 + })) 133 + .send() 134 + .await?; 135 + Ok(true) 136 + } 137 + 138 + async fn delete_smart_playlist(&self, ctx: &Context<'_>, id: String) -> Result<bool, Error> { 139 + let client = ctx.data::<reqwest::Client>()?; 140 + let url = format!("{}/smart-playlists/{}", rockbox_url(), id); 141 + client.delete(&url).send().await?; 142 + Ok(true) 143 + } 144 + 145 + async fn play_smart_playlist(&self, ctx: &Context<'_>, id: String) -> Result<bool, Error> { 146 + let client = ctx.data::<reqwest::Client>()?; 147 + let url = format!("{}/smart-playlists/{}/play", rockbox_url(), id); 148 + client.post(&url).send().await?; 149 + Ok(true) 150 + } 151 + 152 + async fn record_track_played( 153 + &self, 154 + ctx: &Context<'_>, 155 + track_id: String, 156 + ) -> Result<bool, Error> { 157 + let client = ctx.data::<reqwest::Client>()?; 158 + let url = format!("{}/track-stats/{}/played", rockbox_url(), track_id); 159 + client.post(&url).send().await?; 160 + Ok(true) 161 + } 162 + 163 + async fn record_track_skipped( 164 + &self, 165 + ctx: &Context<'_>, 166 + track_id: String, 167 + ) -> Result<bool, Error> { 168 + let client = ctx.data::<reqwest::Client>()?; 169 + let url = format!("{}/track-stats/{}/skipped", rockbox_url(), track_id); 170 + client.post(&url).send().await?; 171 + Ok(true) 172 + } 173 + }
+1
crates/graphql/src/server.rs
··· 90 90 pub async fn start(cmd_tx: Arc<Mutex<Sender<RockboxCommand>>>) -> Result<(), Error> { 91 91 let client = reqwest::Client::new(); 92 92 let pool = create_connection_pool().await?; 93 + 93 94 let schema = Schema::build( 94 95 Query::default(), 95 96 Mutation::default(),
+45
crates/library/migrations/20260425000000_add_playlist_tables.sql
··· 1 + CREATE TABLE IF NOT EXISTS playlist_folders ( 2 + id TEXT PRIMARY KEY, 3 + name TEXT NOT NULL, 4 + created_at INTEGER NOT NULL, 5 + updated_at INTEGER NOT NULL 6 + ); 7 + 8 + CREATE TABLE IF NOT EXISTS saved_playlists ( 9 + id TEXT PRIMARY KEY, 10 + name TEXT NOT NULL, 11 + description TEXT, 12 + image TEXT, 13 + folder_id TEXT, 14 + created_at INTEGER NOT NULL, 15 + updated_at INTEGER NOT NULL 16 + ); 17 + 18 + CREATE TABLE IF NOT EXISTS saved_playlist_tracks ( 19 + id TEXT PRIMARY KEY, 20 + playlist_id TEXT NOT NULL, 21 + track_id TEXT NOT NULL, 22 + position INTEGER NOT NULL, 23 + created_at INTEGER NOT NULL 24 + ); 25 + 26 + CREATE TABLE IF NOT EXISTS smart_playlists ( 27 + id TEXT PRIMARY KEY, 28 + name TEXT NOT NULL, 29 + description TEXT, 30 + image TEXT, 31 + folder_id TEXT, 32 + is_system INTEGER NOT NULL DEFAULT 0, 33 + rules TEXT NOT NULL DEFAULT '{}', 34 + created_at INTEGER NOT NULL, 35 + updated_at INTEGER NOT NULL 36 + ); 37 + 38 + CREATE TABLE IF NOT EXISTS track_stats ( 39 + track_id TEXT PRIMARY KEY, 40 + play_count INTEGER NOT NULL DEFAULT 0, 41 + skip_count INTEGER NOT NULL DEFAULT 0, 42 + last_played INTEGER, 43 + last_skipped INTEGER, 44 + updated_at INTEGER NOT NULL 45 + );
+5
crates/library/migrations/20260425000001_add_playlist_indexes.sql
··· 1 + CREATE INDEX IF NOT EXISTS idx_saved_playlist_tracks_playlist_pos 2 + ON saved_playlist_tracks(playlist_id, position); 3 + 4 + CREATE INDEX IF NOT EXISTS idx_saved_playlist_tracks_track 5 + ON saved_playlist_tracks(track_id);
+10
crates/library/src/lib.rs
··· 75 75 Err(_) => println!("genres column already exists"), 76 76 } 77 77 78 + match pool 79 + .execute(include_str!( 80 + "../migrations/20260425000000_add_playlist_tables.sql" 81 + )) 82 + .await 83 + { 84 + Ok(_) => {} 85 + Err(_) => println!("playlist tables already exist"), 86 + } 87 + 78 88 sqlx::query("PRAGMA journal_mode=WAL") 79 89 .execute(&pool) 80 90 .await?;
+18
crates/playlists/Cargo.toml
··· 1 + [package] 2 + name = "rockbox-playlists" 3 + version = "0.1.0" 4 + authors.workspace = true 5 + edition.workspace = true 6 + license.workspace = true 7 + repository.workspace = true 8 + 9 + [dependencies] 10 + anyhow = "1.0" 11 + chrono = { version = "0.4", features = ["serde"] } 12 + rand = "0.8" 13 + serde = { version = "1.0", features = ["derive"] } 14 + serde_json = "1.0" 15 + sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } 16 + tokio = { version = "1", features = ["full"] } 17 + tracing = { workspace = true } 18 + uuid = { version = "1.3", features = ["v4"] }
+667
crates/playlists/src/lib.rs
··· 1 + pub mod rules; 2 + 3 + use anyhow::{anyhow, Result}; 4 + use chrono::Utc; 5 + use rules::RuleCriteria; 6 + use serde::{Deserialize, Serialize}; 7 + use sqlx::{Pool, Row, Sqlite}; 8 + use uuid::Uuid; 9 + 10 + #[derive(Debug, Clone, Serialize, Deserialize)] 11 + pub struct PlaylistFolder { 12 + pub id: String, 13 + pub name: String, 14 + pub created_at: i64, 15 + pub updated_at: i64, 16 + } 17 + 18 + #[derive(Debug, Clone, Serialize, Deserialize)] 19 + pub struct Playlist { 20 + pub id: String, 21 + pub name: String, 22 + pub description: Option<String>, 23 + pub image: Option<String>, 24 + pub folder_id: Option<String>, 25 + pub track_count: i64, 26 + pub created_at: i64, 27 + pub updated_at: i64, 28 + } 29 + 30 + #[derive(Debug, Clone, Serialize, Deserialize)] 31 + pub struct PlaylistTrack { 32 + pub id: String, 33 + pub playlist_id: String, 34 + pub track_id: String, 35 + pub position: i32, 36 + pub created_at: i64, 37 + } 38 + 39 + #[derive(Debug, Clone, Serialize, Deserialize)] 40 + pub struct SmartPlaylist { 41 + pub id: String, 42 + pub name: String, 43 + pub description: Option<String>, 44 + pub image: Option<String>, 45 + pub folder_id: Option<String>, 46 + pub is_system: bool, 47 + pub rules: RuleCriteria, 48 + pub created_at: i64, 49 + pub updated_at: i64, 50 + } 51 + 52 + #[derive(Debug, Clone, Serialize, Deserialize)] 53 + pub struct TrackStats { 54 + pub track_id: String, 55 + pub play_count: i64, 56 + pub skip_count: i64, 57 + pub last_played: Option<i64>, 58 + pub last_skipped: Option<i64>, 59 + pub updated_at: i64, 60 + } 61 + 62 + #[derive(Clone)] 63 + pub struct PlaylistStore { 64 + pool: Pool<Sqlite>, 65 + } 66 + 67 + impl PlaylistStore { 68 + pub fn new(pool: Pool<Sqlite>) -> Self { 69 + Self { pool } 70 + } 71 + 72 + pub async fn seed(&self) -> Result<()> { 73 + self.seed_system_smart_playlists().await 74 + } 75 + 76 + // ── Folders ──────────────────────────────────────────────────────────── 77 + 78 + pub async fn create_folder(&self, name: &str) -> Result<PlaylistFolder> { 79 + let id = Uuid::new_v4().to_string(); 80 + let now = Utc::now().timestamp(); 81 + sqlx::query( 82 + "INSERT INTO playlist_folders (id, name, created_at, updated_at) VALUES (?, ?, ?, ?)", 83 + ) 84 + .bind(&id) 85 + .bind(name) 86 + .bind(now) 87 + .bind(now) 88 + .execute(&self.pool) 89 + .await?; 90 + Ok(PlaylistFolder { 91 + id, 92 + name: name.to_string(), 93 + created_at: now, 94 + updated_at: now, 95 + }) 96 + } 97 + 98 + pub async fn list_folders(&self) -> Result<Vec<PlaylistFolder>> { 99 + let rows = sqlx::query( 100 + "SELECT id, name, created_at, updated_at FROM playlist_folders ORDER BY name ASC", 101 + ) 102 + .fetch_all(&self.pool) 103 + .await?; 104 + Ok(rows 105 + .into_iter() 106 + .map(|r| PlaylistFolder { 107 + id: r.get(0), 108 + name: r.get(1), 109 + created_at: r.get(2), 110 + updated_at: r.get(3), 111 + }) 112 + .collect()) 113 + } 114 + 115 + pub async fn get_folder(&self, id: &str) -> Result<Option<PlaylistFolder>> { 116 + let row = sqlx::query( 117 + "SELECT id, name, created_at, updated_at FROM playlist_folders WHERE id = ?", 118 + ) 119 + .bind(id) 120 + .fetch_optional(&self.pool) 121 + .await?; 122 + Ok(row.map(|r| PlaylistFolder { 123 + id: r.get(0), 124 + name: r.get(1), 125 + created_at: r.get(2), 126 + updated_at: r.get(3), 127 + })) 128 + } 129 + 130 + pub async fn delete_folder(&self, id: &str) -> Result<bool> { 131 + sqlx::query("UPDATE saved_playlists SET folder_id = NULL WHERE folder_id = ?") 132 + .bind(id) 133 + .execute(&self.pool) 134 + .await?; 135 + let result = sqlx::query("DELETE FROM playlist_folders WHERE id = ?") 136 + .bind(id) 137 + .execute(&self.pool) 138 + .await?; 139 + Ok(result.rows_affected() > 0) 140 + } 141 + 142 + // ── Playlists ────────────────────────────────────────────────────────── 143 + 144 + pub async fn create( 145 + &self, 146 + name: &str, 147 + description: Option<&str>, 148 + image: Option<&str>, 149 + folder_id: Option<&str>, 150 + ) -> Result<Playlist> { 151 + let id = Uuid::new_v4().to_string(); 152 + let now = Utc::now().timestamp(); 153 + sqlx::query( 154 + "INSERT INTO saved_playlists (id, name, description, image, folder_id, created_at, updated_at) 155 + VALUES (?, ?, ?, ?, ?, ?, ?)", 156 + ) 157 + .bind(&id) 158 + .bind(name) 159 + .bind(description) 160 + .bind(image) 161 + .bind(folder_id) 162 + .bind(now) 163 + .bind(now) 164 + .execute(&self.pool) 165 + .await?; 166 + Ok(Playlist { 167 + id, 168 + name: name.to_string(), 169 + description: description.map(|s| s.to_string()), 170 + image: image.map(|s| s.to_string()), 171 + folder_id: folder_id.map(|s| s.to_string()), 172 + track_count: 0, 173 + created_at: now, 174 + updated_at: now, 175 + }) 176 + } 177 + 178 + pub async fn list(&self) -> Result<Vec<Playlist>> { 179 + let rows = sqlx::query( 180 + "SELECT p.id, p.name, p.description, p.image, p.folder_id, 181 + (SELECT COUNT(*) FROM saved_playlist_tracks pt WHERE pt.playlist_id = p.id) AS track_count, 182 + p.created_at, p.updated_at 183 + FROM saved_playlists p 184 + ORDER BY p.created_at DESC", 185 + ) 186 + .fetch_all(&self.pool) 187 + .await?; 188 + Ok(rows.into_iter().map(row_to_playlist).collect()) 189 + } 190 + 191 + pub async fn list_by_folder(&self, folder_id: &str) -> Result<Vec<Playlist>> { 192 + let rows = sqlx::query( 193 + "SELECT p.id, p.name, p.description, p.image, p.folder_id, 194 + (SELECT COUNT(*) FROM saved_playlist_tracks pt WHERE pt.playlist_id = p.id) AS track_count, 195 + p.created_at, p.updated_at 196 + FROM saved_playlists p 197 + WHERE p.folder_id = ? 198 + ORDER BY p.created_at DESC", 199 + ) 200 + .bind(folder_id) 201 + .fetch_all(&self.pool) 202 + .await?; 203 + Ok(rows.into_iter().map(row_to_playlist).collect()) 204 + } 205 + 206 + pub async fn get(&self, id: &str) -> Result<Option<Playlist>> { 207 + let row = sqlx::query( 208 + "SELECT p.id, p.name, p.description, p.image, p.folder_id, 209 + (SELECT COUNT(*) FROM saved_playlist_tracks pt WHERE pt.playlist_id = p.id) AS track_count, 210 + p.created_at, p.updated_at 211 + FROM saved_playlists p WHERE p.id = ?", 212 + ) 213 + .bind(id) 214 + .fetch_optional(&self.pool) 215 + .await?; 216 + Ok(row.map(row_to_playlist)) 217 + } 218 + 219 + pub async fn update( 220 + &self, 221 + id: &str, 222 + name: &str, 223 + description: Option<&str>, 224 + image: Option<&str>, 225 + folder_id: Option<&str>, 226 + ) -> Result<()> { 227 + let now = Utc::now().timestamp(); 228 + sqlx::query( 229 + "UPDATE saved_playlists SET name = ?, description = ?, image = ?, folder_id = ?, updated_at = ? 230 + WHERE id = ?", 231 + ) 232 + .bind(name) 233 + .bind(description) 234 + .bind(image) 235 + .bind(folder_id) 236 + .bind(now) 237 + .bind(id) 238 + .execute(&self.pool) 239 + .await?; 240 + Ok(()) 241 + } 242 + 243 + pub async fn delete(&self, id: &str) -> Result<bool> { 244 + sqlx::query("DELETE FROM saved_playlist_tracks WHERE playlist_id = ?") 245 + .bind(id) 246 + .execute(&self.pool) 247 + .await?; 248 + let result = sqlx::query("DELETE FROM saved_playlists WHERE id = ?") 249 + .bind(id) 250 + .execute(&self.pool) 251 + .await?; 252 + Ok(result.rows_affected() > 0) 253 + } 254 + 255 + // ── Tracks ───────────────────────────────────────────────────────────── 256 + 257 + pub async fn add_tracks(&self, playlist_id: &str, track_ids: &[String]) -> Result<()> { 258 + let max_pos: i32 = sqlx::query( 259 + "SELECT COALESCE(MAX(position), -1) FROM saved_playlist_tracks WHERE playlist_id = ?", 260 + ) 261 + .bind(playlist_id) 262 + .fetch_one(&self.pool) 263 + .await 264 + .map(|r| r.get::<i32, _>(0)) 265 + .unwrap_or(-1); 266 + 267 + for (i, track_id) in track_ids.iter().enumerate() { 268 + let id = Uuid::new_v4().to_string(); 269 + let now = Utc::now().timestamp(); 270 + let position = max_pos + 1 + i as i32; 271 + sqlx::query( 272 + "INSERT INTO saved_playlist_tracks (id, playlist_id, track_id, position, created_at) 273 + VALUES (?, ?, ?, ?, ?)", 274 + ) 275 + .bind(&id) 276 + .bind(playlist_id) 277 + .bind(track_id) 278 + .bind(position) 279 + .bind(now) 280 + .execute(&self.pool) 281 + .await?; 282 + } 283 + Ok(()) 284 + } 285 + 286 + pub async fn remove_track(&self, playlist_id: &str, track_id: &str) -> Result<bool> { 287 + let result = 288 + sqlx::query("DELETE FROM saved_playlist_tracks WHERE playlist_id = ? AND track_id = ?") 289 + .bind(playlist_id) 290 + .bind(track_id) 291 + .execute(&self.pool) 292 + .await?; 293 + Ok(result.rows_affected() > 0) 294 + } 295 + 296 + pub async fn get_track_ids(&self, playlist_id: &str) -> Result<Vec<String>> { 297 + let rows = sqlx::query( 298 + "SELECT track_id FROM saved_playlist_tracks WHERE playlist_id = ? ORDER BY position ASC", 299 + ) 300 + .bind(playlist_id) 301 + .fetch_all(&self.pool) 302 + .await?; 303 + Ok(rows.into_iter().map(|r| r.get(0)).collect()) 304 + } 305 + 306 + // ── Smart playlists ──────────────────────────────────────────────────── 307 + 308 + async fn seed_system_smart_playlists(&self) -> Result<()> { 309 + use serde_json::json; 310 + 311 + let defaults: &[(&str, &str, &str, serde_json::Value)] = &[ 312 + ( 313 + "sys_recently_added", 314 + "Recently Added", 315 + "Tracks added to your library in the last 30 days.", 316 + json!({ "match_type": "all", "conditions": [], "limit": 50, 317 + "sort_by": "date_added", "sort_order": "DESC" }), 318 + ), 319 + ( 320 + "sys_recently_played", 321 + "Recently Played", 322 + "Tracks you've listened to in the last 14 days.", 323 + json!({ "match_type": "all", 324 + "conditions": [{"field":"last_played","operator":"in_last","value":14,"unit":"days"}], 325 + "limit": 50, "sort_by": "last_played", "sort_order": "DESC" }), 326 + ), 327 + ( 328 + "sys_rarely_played", 329 + "Rarely Played", 330 + "Tracks you've played fewer than 3 times.", 331 + json!({ "match_type": "all", 332 + "conditions": [ 333 + {"field":"play_count","operator":"greater_than","value":0}, 334 + {"field":"play_count","operator":"less_than","value":3} 335 + ], 336 + "limit": 50, "sort_by": "play_count", "sort_order": "ASC" }), 337 + ), 338 + ( 339 + "sys_most_played", 340 + "Most Played", 341 + "Your most listened-to tracks of all time.", 342 + json!({ "match_type": "all", 343 + "conditions": [{"field":"play_count","operator":"greater_than","value":0}], 344 + "limit": 50, "sort_by": "play_count", "sort_order": "DESC" }), 345 + ), 346 + ( 347 + "sys_forgotten_favorites", 348 + "Forgotten Favorites", 349 + "Tracks you used to love but haven't played in over 6 months.", 350 + json!({ "match_type": "all", 351 + "conditions": [ 352 + {"field":"play_count","operator":"greater_than","value":10}, 353 + {"field":"last_played","operator":"not_in_last","value":180,"unit":"days"} 354 + ], 355 + "limit": 50, "sort_by": "last_played", "sort_order": "ASC" }), 356 + ), 357 + ( 358 + "sys_new_favorites", 359 + "New Favorites", 360 + "Recently added tracks you're already playing a lot.", 361 + json!({ "match_type": "all", 362 + "conditions": [ 363 + {"field":"play_count","operator":"greater_than","value":5}, 364 + {"field":"date_added","operator":"in_last","value":90,"unit":"days"} 365 + ], 366 + "limit": 30, "sort_by": "play_count", "sort_order": "DESC" }), 367 + ), 368 + ( 369 + "sys_old_but_gold", 370 + "Old But Gold", 371 + "Classic tracks from before 2000 that you keep coming back to.", 372 + json!({ "match_type": "all", 373 + "conditions": [ 374 + {"field":"year","operator":"less_than","value":2000}, 375 + {"field":"play_count","operator":"greater_than","value":5} 376 + ], 377 + "limit": 50, "sort_by": "play_count", "sort_order": "DESC" }), 378 + ), 379 + ( 380 + "sys_90s_mix", 381 + "90s Mix", 382 + "A shuffle of your 90s music.", 383 + json!({ "match_type": "all", 384 + "conditions": [ 385 + {"field":"year","operator":"greater_than_or_equal","value":1990}, 386 + {"field":"year","operator":"less_than_or_equal","value":1999} 387 + ], 388 + "limit": 50, "sort_by": "random", "sort_order": "DESC" }), 389 + ), 390 + ( 391 + "sys_discovery_queue", 392 + "Discovery Queue", 393 + "Tracks in your library you've never played.", 394 + json!({ "match_type": "all", 395 + "conditions": [{"field":"play_count","operator":"equals","value":0}], 396 + "limit": 50, "sort_by": "date_added", "sort_order": "DESC" }), 397 + ), 398 + ( 399 + "sys_rediscover", 400 + "Rediscover", 401 + "Tracks you've played before but not for over 3 months.", 402 + json!({ "match_type": "all", 403 + "conditions": [ 404 + {"field":"play_count","operator":"greater_than","value":0}, 405 + {"field":"last_played","operator":"not_in_last","value":90,"unit":"days"} 406 + ], 407 + "limit": 50, "sort_by": "random", "sort_order": "DESC" }), 408 + ), 409 + ( 410 + "sys_skipped_too_often", 411 + "Skipped Too Often", 412 + "Tracks you keep skipping — maybe time to remove them?", 413 + json!({ "match_type": "all", 414 + "conditions": [{"field":"skip_count","operator":"greater_than","value":3}], 415 + "limit": 50, "sort_by": "skip_count", "sort_order": "DESC" }), 416 + ), 417 + ( 418 + "sys_loved_rarely_played", 419 + "Loved But Rarely Played", 420 + "Tracks you've liked but haven't played much.", 421 + json!({ "match_type": "all", 422 + "conditions": [ 423 + {"field":"is_liked","operator":"is","value":true}, 424 + {"field":"play_count","operator":"less_than","value":5} 425 + ], 426 + "limit": 50, "sort_by": "date_added", "sort_order": "DESC" }), 427 + ), 428 + ( 429 + "sys_smart_daily_mix", 430 + "Smart Daily Mix", 431 + "A curated daily mix based on your listening habits.", 432 + json!({ "match_type": "any", 433 + "conditions": [ 434 + {"field":"last_played","operator":"in_last","value":30,"unit":"days"}, 435 + {"field":"play_count","operator":"equals","value":0}, 436 + {"field":"play_count","operator":"greater_than","value":10} 437 + ], 438 + "limit": 25, "sort_by": "random", "sort_order": "DESC" }), 439 + ), 440 + ]; 441 + 442 + let now = Utc::now().timestamp(); 443 + for (id, name, description, rules) in defaults { 444 + let count: i64 = sqlx::query("SELECT COUNT(*) FROM smart_playlists WHERE id = ?") 445 + .bind(*id) 446 + .fetch_one(&self.pool) 447 + .await 448 + .map(|r| r.get(0)) 449 + .unwrap_or(0); 450 + if count > 0 { 451 + continue; 452 + } 453 + sqlx::query( 454 + "INSERT INTO smart_playlists 455 + (id, name, description, image, folder_id, is_system, rules, created_at, updated_at) 456 + VALUES (?, ?, ?, NULL, NULL, 1, ?, ?, ?)", 457 + ) 458 + .bind(*id) 459 + .bind(*name) 460 + .bind(*description) 461 + .bind(rules.to_string()) 462 + .bind(now) 463 + .bind(now) 464 + .execute(&self.pool) 465 + .await?; 466 + } 467 + Ok(()) 468 + } 469 + 470 + pub async fn list_smart_playlists(&self) -> Result<Vec<SmartPlaylist>> { 471 + let rows = sqlx::query( 472 + "SELECT id, name, description, image, folder_id, is_system, rules, created_at, updated_at 473 + FROM smart_playlists ORDER BY is_system DESC, name ASC", 474 + ) 475 + .fetch_all(&self.pool) 476 + .await?; 477 + Ok(rows.into_iter().map(row_to_smart_playlist).collect()) 478 + } 479 + 480 + pub async fn get_smart_playlist(&self, id: &str) -> Result<Option<SmartPlaylist>> { 481 + let row = sqlx::query( 482 + "SELECT id, name, description, image, folder_id, is_system, rules, created_at, updated_at 483 + FROM smart_playlists WHERE id = ?", 484 + ) 485 + .bind(id) 486 + .fetch_optional(&self.pool) 487 + .await?; 488 + Ok(row.map(row_to_smart_playlist)) 489 + } 490 + 491 + pub async fn create_smart_playlist( 492 + &self, 493 + name: &str, 494 + description: Option<&str>, 495 + image: Option<&str>, 496 + folder_id: Option<&str>, 497 + rules: &RuleCriteria, 498 + ) -> Result<SmartPlaylist> { 499 + let id = Uuid::new_v4().to_string(); 500 + let now = Utc::now().timestamp(); 501 + let rules_json = serde_json::to_string(rules)?; 502 + sqlx::query( 503 + "INSERT INTO smart_playlists 504 + (id, name, description, image, folder_id, is_system, rules, created_at, updated_at) 505 + VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?)", 506 + ) 507 + .bind(&id) 508 + .bind(name) 509 + .bind(description) 510 + .bind(image) 511 + .bind(folder_id) 512 + .bind(&rules_json) 513 + .bind(now) 514 + .bind(now) 515 + .execute(&self.pool) 516 + .await?; 517 + Ok(SmartPlaylist { 518 + id, 519 + name: name.to_string(), 520 + description: description.map(|s| s.to_string()), 521 + image: image.map(|s| s.to_string()), 522 + folder_id: folder_id.map(|s| s.to_string()), 523 + is_system: false, 524 + rules: rules.clone(), 525 + created_at: now, 526 + updated_at: now, 527 + }) 528 + } 529 + 530 + pub async fn update_smart_playlist( 531 + &self, 532 + id: &str, 533 + name: &str, 534 + description: Option<&str>, 535 + image: Option<&str>, 536 + folder_id: Option<&str>, 537 + rules: &RuleCriteria, 538 + ) -> Result<()> { 539 + let now = Utc::now().timestamp(); 540 + let rules_json = serde_json::to_string(rules)?; 541 + let result = sqlx::query( 542 + "UPDATE smart_playlists 543 + SET name = ?, description = ?, image = ?, folder_id = ?, rules = ?, updated_at = ? 544 + WHERE id = ? AND is_system = 0", 545 + ) 546 + .bind(name) 547 + .bind(description) 548 + .bind(image) 549 + .bind(folder_id) 550 + .bind(&rules_json) 551 + .bind(now) 552 + .bind(id) 553 + .execute(&self.pool) 554 + .await?; 555 + if result.rows_affected() == 0 { 556 + return Err(anyhow!("Smart playlist not found or is a system playlist")); 557 + } 558 + Ok(()) 559 + } 560 + 561 + pub async fn delete_smart_playlist(&self, id: &str) -> Result<bool> { 562 + let result = sqlx::query("DELETE FROM smart_playlists WHERE id = ? AND is_system = 0") 563 + .bind(id) 564 + .execute(&self.pool) 565 + .await?; 566 + Ok(result.rows_affected() > 0) 567 + } 568 + 569 + // ── Track stats ──────────────────────────────────────────────────────── 570 + 571 + pub async fn record_play(&self, track_id: &str) -> Result<()> { 572 + let now = Utc::now().timestamp(); 573 + sqlx::query( 574 + "INSERT INTO track_stats (track_id, play_count, skip_count, last_played, last_skipped, updated_at) 575 + VALUES (?, 1, 0, ?, NULL, ?) 576 + ON CONFLICT(track_id) DO UPDATE SET 577 + play_count = play_count + 1, 578 + last_played = excluded.last_played, 579 + updated_at = excluded.updated_at", 580 + ) 581 + .bind(track_id) 582 + .bind(now) 583 + .bind(now) 584 + .execute(&self.pool) 585 + .await?; 586 + Ok(()) 587 + } 588 + 589 + pub async fn record_skip(&self, track_id: &str) -> Result<()> { 590 + let now = Utc::now().timestamp(); 591 + sqlx::query( 592 + "INSERT INTO track_stats (track_id, play_count, skip_count, last_played, last_skipped, updated_at) 593 + VALUES (?, 0, 1, NULL, ?, ?) 594 + ON CONFLICT(track_id) DO UPDATE SET 595 + skip_count = skip_count + 1, 596 + last_skipped = excluded.last_skipped, 597 + updated_at = excluded.updated_at", 598 + ) 599 + .bind(track_id) 600 + .bind(now) 601 + .bind(now) 602 + .execute(&self.pool) 603 + .await?; 604 + Ok(()) 605 + } 606 + 607 + pub async fn get_track_stats(&self, track_id: &str) -> Result<Option<TrackStats>> { 608 + let row = sqlx::query( 609 + "SELECT track_id, play_count, skip_count, last_played, last_skipped, updated_at 610 + FROM track_stats WHERE track_id = ?", 611 + ) 612 + .bind(track_id) 613 + .fetch_optional(&self.pool) 614 + .await?; 615 + Ok(row.map(row_to_track_stats)) 616 + } 617 + 618 + pub async fn get_all_track_stats(&self) -> Result<Vec<TrackStats>> { 619 + let rows = sqlx::query( 620 + "SELECT track_id, play_count, skip_count, last_played, last_skipped, updated_at 621 + FROM track_stats", 622 + ) 623 + .fetch_all(&self.pool) 624 + .await?; 625 + Ok(rows.into_iter().map(row_to_track_stats).collect()) 626 + } 627 + } 628 + 629 + fn row_to_playlist(r: sqlx::sqlite::SqliteRow) -> Playlist { 630 + Playlist { 631 + id: r.get(0), 632 + name: r.get(1), 633 + description: r.get(2), 634 + image: r.get(3), 635 + folder_id: r.get(4), 636 + track_count: r.get(5), 637 + created_at: r.get(6), 638 + updated_at: r.get(7), 639 + } 640 + } 641 + 642 + fn row_to_smart_playlist(r: sqlx::sqlite::SqliteRow) -> SmartPlaylist { 643 + let is_system: i64 = r.get(5); 644 + let rules_str: String = r.get(6); 645 + SmartPlaylist { 646 + id: r.get(0), 647 + name: r.get(1), 648 + description: r.get(2), 649 + image: r.get(3), 650 + folder_id: r.get(4), 651 + is_system: is_system != 0, 652 + rules: serde_json::from_str(&rules_str).unwrap_or_default(), 653 + created_at: r.get(7), 654 + updated_at: r.get(8), 655 + } 656 + } 657 + 658 + fn row_to_track_stats(r: sqlx::sqlite::SqliteRow) -> TrackStats { 659 + TrackStats { 660 + track_id: r.get(0), 661 + play_count: r.get(1), 662 + skip_count: r.get(2), 663 + last_played: r.get(3), 664 + last_skipped: r.get(4), 665 + updated_at: r.get(5), 666 + } 667 + }
+306
crates/playlists/src/rules.rs
··· 1 + use chrono::Utc; 2 + use serde::{Deserialize, Serialize}; 3 + 4 + // ── Field / Operator / Unit enums ────────────────────────────────────────── 5 + 6 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 7 + #[serde(rename_all = "snake_case")] 8 + pub enum MatchType { 9 + All, 10 + Any, 11 + } 12 + 13 + impl Default for MatchType { 14 + fn default() -> Self { 15 + MatchType::All 16 + } 17 + } 18 + 19 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 20 + #[serde(rename_all = "snake_case")] 21 + pub enum RuleField { 22 + PlayCount, 23 + SkipCount, 24 + LastPlayed, 25 + LastSkipped, 26 + DateAdded, 27 + Year, 28 + Genre, 29 + Artist, 30 + Album, 31 + DurationMs, 32 + Bitrate, 33 + IsLiked, 34 + } 35 + 36 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 37 + #[serde(rename_all = "snake_case")] 38 + pub enum RuleOperator { 39 + Is, 40 + IsNot, 41 + Contains, 42 + NotContains, 43 + GreaterThan, 44 + LessThan, 45 + GreaterThanOrEqual, 46 + LessThanOrEqual, 47 + Equals, 48 + Between, 49 + InLast, 50 + NotInLast, 51 + IsEmpty, 52 + IsNotEmpty, 53 + } 54 + 55 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 56 + #[serde(rename_all = "snake_case")] 57 + pub enum TimeUnit { 58 + Days, 59 + Weeks, 60 + Months, 61 + Years, 62 + } 63 + 64 + impl TimeUnit { 65 + pub fn to_seconds(&self, n: i64) -> i64 { 66 + match self { 67 + TimeUnit::Days => n * 86_400, 68 + TimeUnit::Weeks => n * 604_800, 69 + TimeUnit::Months => n * 2_592_000, 70 + TimeUnit::Years => n * 31_536_000, 71 + } 72 + } 73 + } 74 + 75 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 76 + #[serde(rename_all = "snake_case")] 77 + pub enum SortField { 78 + Random, 79 + PlayCount, 80 + SkipCount, 81 + LastPlayed, 82 + DateAdded, 83 + Year, 84 + Title, 85 + Artist, 86 + Album, 87 + DurationMs, 88 + } 89 + 90 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 91 + #[serde(rename_all = "UPPERCASE")] 92 + pub enum SortOrder { 93 + Asc, 94 + Desc, 95 + } 96 + 97 + impl Default for SortOrder { 98 + fn default() -> Self { 99 + SortOrder::Desc 100 + } 101 + } 102 + 103 + // ── Condition / RuleCriteria ──────────────────────────────────────────────── 104 + 105 + #[derive(Debug, Clone, Serialize, Deserialize)] 106 + pub struct Condition { 107 + pub field: RuleField, 108 + pub operator: RuleOperator, 109 + /// Primary value — numeric or string depending on field. 110 + #[serde(default)] 111 + pub value: Option<serde_json::Value>, 112 + /// Second value for `between`. 113 + #[serde(default)] 114 + pub value2: Option<serde_json::Value>, 115 + /// Time unit for `in_last` / `not_in_last`. 116 + #[serde(default)] 117 + pub unit: Option<TimeUnit>, 118 + } 119 + 120 + #[derive(Debug, Clone, Serialize, Deserialize, Default)] 121 + pub struct RuleCriteria { 122 + #[serde(default)] 123 + pub match_type: MatchType, 124 + #[serde(default)] 125 + pub conditions: Vec<Condition>, 126 + pub limit: Option<usize>, 127 + pub sort_by: Option<SortField>, 128 + pub sort_order: Option<SortOrder>, 129 + } 130 + 131 + // ── Candidate track fed into the resolver ────────────────────────────────── 132 + 133 + #[derive(Debug, Clone)] 134 + pub struct Candidate { 135 + pub id: String, 136 + pub title: String, 137 + pub artist: String, 138 + pub album: String, 139 + pub year: Option<i64>, 140 + pub genre: Option<String>, 141 + pub duration_ms: i64, 142 + pub bitrate: i64, 143 + pub date_added_ts: i64, 144 + // stats (default 0 / None when never played) 145 + pub play_count: i64, 146 + pub skip_count: i64, 147 + pub last_played: Option<i64>, 148 + pub last_skipped: Option<i64>, 149 + pub is_liked: bool, 150 + } 151 + 152 + // ── Resolver ─────────────────────────────────────────────────────────────── 153 + 154 + pub fn resolve(criteria: &RuleCriteria, mut candidates: Vec<Candidate>) -> Vec<Candidate> { 155 + let now = Utc::now().timestamp(); 156 + 157 + // Filter 158 + candidates.retain(|c| { 159 + if criteria.conditions.is_empty() { 160 + return true; 161 + } 162 + let results: Vec<bool> = criteria 163 + .conditions 164 + .iter() 165 + .map(|cond| eval_condition(cond, c, now)) 166 + .collect(); 167 + match criteria.match_type { 168 + MatchType::All => results.iter().all(|&r| r), 169 + MatchType::Any => results.iter().any(|&r| r), 170 + } 171 + }); 172 + 173 + // Sort 174 + sort_candidates(&mut candidates, criteria, now); 175 + 176 + // Limit 177 + if let Some(limit) = criteria.limit { 178 + candidates.truncate(limit); 179 + } 180 + 181 + candidates 182 + } 183 + 184 + fn eval_condition(cond: &Condition, c: &Candidate, now: i64) -> bool { 185 + match &cond.field { 186 + RuleField::PlayCount => eval_numeric(cond, c.play_count), 187 + RuleField::SkipCount => eval_numeric(cond, c.skip_count), 188 + RuleField::DurationMs => eval_numeric(cond, c.duration_ms), 189 + RuleField::Bitrate => eval_numeric(cond, c.bitrate), 190 + RuleField::Year => { 191 + if let Some(y) = c.year { 192 + eval_numeric(cond, y) 193 + } else { 194 + matches!(cond.operator, RuleOperator::IsEmpty) 195 + } 196 + } 197 + RuleField::LastPlayed => eval_timestamp(cond, c.last_played, now), 198 + RuleField::LastSkipped => eval_timestamp(cond, c.last_skipped, now), 199 + RuleField::DateAdded => eval_timestamp(cond, Some(c.date_added_ts), now), 200 + RuleField::Genre => eval_string(cond, c.genre.as_deref()), 201 + RuleField::Artist => eval_string(cond, Some(&c.artist)), 202 + RuleField::Album => eval_string(cond, Some(&c.album)), 203 + RuleField::IsLiked => { 204 + let want = cond 205 + .value 206 + .as_ref() 207 + .and_then(|v| v.as_bool()) 208 + .unwrap_or(true); 209 + c.is_liked == want 210 + } 211 + } 212 + } 213 + 214 + fn eval_numeric(cond: &Condition, val: i64) -> bool { 215 + let n = cond.value.as_ref().and_then(|v| v.as_i64()).unwrap_or(0); 216 + let n2 = cond.value2.as_ref().and_then(|v| v.as_i64()).unwrap_or(0); 217 + match cond.operator { 218 + RuleOperator::Equals | RuleOperator::Is => val == n, 219 + RuleOperator::IsNot => val != n, 220 + RuleOperator::GreaterThan => val > n, 221 + RuleOperator::LessThan => val < n, 222 + RuleOperator::GreaterThanOrEqual => val >= n, 223 + RuleOperator::LessThanOrEqual => val <= n, 224 + RuleOperator::Between => val >= n && val <= n2, 225 + _ => false, 226 + } 227 + } 228 + 229 + fn eval_timestamp(cond: &Condition, ts: Option<i64>, now: i64) -> bool { 230 + match cond.operator { 231 + RuleOperator::IsEmpty => ts.is_none(), 232 + RuleOperator::IsNotEmpty => ts.is_some(), 233 + RuleOperator::InLast => { 234 + if let (Some(ts), Some(n)) = (ts, cond.value.as_ref().and_then(|v| v.as_i64())) { 235 + let secs = cond.unit.as_ref().unwrap_or(&TimeUnit::Days).to_seconds(n); 236 + now - ts <= secs 237 + } else { 238 + false 239 + } 240 + } 241 + RuleOperator::NotInLast => { 242 + if let Some(n) = cond.value.as_ref().and_then(|v| v.as_i64()) { 243 + let secs = cond.unit.as_ref().unwrap_or(&TimeUnit::Days).to_seconds(n); 244 + match ts { 245 + Some(ts) => now - ts > secs, 246 + None => true, // never played → qualifies as "not in last N" 247 + } 248 + } else { 249 + false 250 + } 251 + } 252 + _ => { 253 + // Treat as numeric comparison on the unix timestamp 254 + eval_numeric(cond, ts.unwrap_or(0)) 255 + } 256 + } 257 + } 258 + 259 + fn eval_string(cond: &Condition, val: Option<&str>) -> bool { 260 + let s = val.unwrap_or(""); 261 + let target = cond.value.as_ref().and_then(|v| v.as_str()).unwrap_or(""); 262 + match cond.operator { 263 + RuleOperator::Is | RuleOperator::Equals => s.eq_ignore_ascii_case(target), 264 + RuleOperator::IsNot => !s.eq_ignore_ascii_case(target), 265 + RuleOperator::Contains => s.to_lowercase().contains(&target.to_lowercase()), 266 + RuleOperator::NotContains => !s.to_lowercase().contains(&target.to_lowercase()), 267 + RuleOperator::IsEmpty => s.is_empty(), 268 + RuleOperator::IsNotEmpty => !s.is_empty(), 269 + _ => false, 270 + } 271 + } 272 + 273 + fn sort_candidates(candidates: &mut Vec<Candidate>, criteria: &RuleCriteria, _now: i64) { 274 + use rand::seq::SliceRandom; 275 + 276 + let sort_by = criteria.sort_by.as_ref().unwrap_or(&SortField::DateAdded); 277 + let asc = matches!( 278 + criteria.sort_order.as_ref().unwrap_or(&SortOrder::Desc), 279 + SortOrder::Asc 280 + ); 281 + 282 + if matches!(sort_by, SortField::Random) { 283 + candidates.shuffle(&mut rand::thread_rng()); 284 + return; 285 + } 286 + 287 + candidates.sort_by(|a, b| { 288 + let cmp = match sort_by { 289 + SortField::PlayCount => a.play_count.cmp(&b.play_count), 290 + SortField::SkipCount => a.skip_count.cmp(&b.skip_count), 291 + SortField::LastPlayed => a.last_played.unwrap_or(0).cmp(&b.last_played.unwrap_or(0)), 292 + SortField::DateAdded => a.date_added_ts.cmp(&b.date_added_ts), 293 + SortField::Year => a.year.unwrap_or(0).cmp(&b.year.unwrap_or(0)), 294 + SortField::Title => a.title.cmp(&b.title), 295 + SortField::Artist => a.artist.cmp(&b.artist), 296 + SortField::Album => a.album.cmp(&b.album), 297 + SortField::DurationMs => a.duration_ms.cmp(&b.duration_ms), 298 + SortField::Random => std::cmp::Ordering::Equal, 299 + }; 300 + if asc { 301 + cmp 302 + } else { 303 + cmp.reverse() 304 + } 305 + }); 306 + }
+17
crates/rocksky/src/api/rockbox.v1alpha1.rs
··· 516 516 pub term: ::prost::alloc::string::String, 517 517 } 518 518 #[derive(Clone, PartialEq, ::prost::Message)] 519 + pub struct SearchPlaylist { 520 + #[prost(string, tag = "1")] 521 + pub id: ::prost::alloc::string::String, 522 + #[prost(string, tag = "2")] 523 + pub name: ::prost::alloc::string::String, 524 + #[prost(string, optional, tag = "3")] 525 + pub description: ::core::option::Option<::prost::alloc::string::String>, 526 + #[prost(string, optional, tag = "4")] 527 + pub image: ::core::option::Option<::prost::alloc::string::String>, 528 + #[prost(bool, tag = "5")] 529 + pub is_smart: bool, 530 + #[prost(int64, tag = "6")] 531 + pub track_count: i64, 532 + } 533 + #[derive(Clone, PartialEq, ::prost::Message)] 519 534 pub struct SearchResponse { 520 535 #[prost(message, repeated, tag = "1")] 521 536 pub tracks: ::prost::alloc::vec::Vec<Track>, ··· 523 538 pub albums: ::prost::alloc::vec::Vec<Album>, 524 539 #[prost(message, repeated, tag = "3")] 525 540 pub artists: ::prost::alloc::vec::Vec<Artist>, 541 + #[prost(message, repeated, tag = "4")] 542 + pub playlists: ::prost::alloc::vec::Vec<SearchPlaylist>, 526 543 } 527 544 /// Generated client implementations. 528 545 pub mod library_service_client {
crates/rocksky/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+1
crates/rpc/Cargo.toml
··· 18 18 ], default-features = false } 19 19 rockbox-graphql = { path = "../graphql" } 20 20 rockbox-library = { path = "../library" } 21 + rockbox-playlists = { path = "../playlists" } # needed for Playlist/PlaylistFolder type deserialization 21 22 rockbox-rocksky = {path = "../rocksky"} 22 23 rockbox-typesense = { path = "../typesense" } 23 24 rockbox-sys = { path = "../sys" }
+2
crates/rpc/build.rs
··· 10 10 "proto/rockbox/v1alpha1/metadata.proto", 11 11 "proto/rockbox/v1alpha1/playback.proto", 12 12 "proto/rockbox/v1alpha1/playlist.proto", 13 + "proto/rockbox/v1alpha1/saved_playlist.proto", 14 + "proto/rockbox/v1alpha1/smart_playlist.proto", 13 15 "proto/rockbox/v1alpha1/settings.proto", 14 16 "proto/rockbox/v1alpha1/sound.proto", 15 17 "proto/rockbox/v1alpha1/system.proto",
+10
crates/rpc/proto/rockbox/v1alpha1/library.proto
··· 145 145 string term = 1; 146 146 } 147 147 148 + message SearchPlaylist { 149 + string id = 1; 150 + string name = 2; 151 + optional string description = 3; 152 + optional string image = 4; 153 + bool is_smart = 5; 154 + int64 track_count = 6; 155 + } 156 + 148 157 message SearchResponse { 149 158 repeated Track tracks = 1; 150 159 repeated Album albums = 2; 151 160 repeated Artist artists = 3; 161 + repeated SearchPlaylist playlists = 4; 152 162 } 153 163 154 164 service LibraryService {
+114
crates/rpc/proto/rockbox/v1alpha1/saved_playlist.proto
··· 1 + syntax = "proto3"; 2 + 3 + package rockbox.v1alpha1; 4 + 5 + // ── Folders ──────────────────────────────────────────────────────────────── 6 + 7 + message PlaylistFolder { 8 + string id = 1; 9 + string name = 2; 10 + int64 created_at = 3; 11 + int64 updated_at = 4; 12 + } 13 + 14 + message CreatePlaylistFolderRequest { string name = 1; } 15 + message CreatePlaylistFolderResponse { PlaylistFolder folder = 1; } 16 + 17 + message GetPlaylistFoldersRequest {} 18 + message GetPlaylistFoldersResponse { repeated PlaylistFolder folders = 1; } 19 + 20 + message DeletePlaylistFolderRequest { string id = 1; } 21 + message DeletePlaylistFolderResponse {} 22 + 23 + // ── Playlists ────────────────────────────────────────────────────────────── 24 + 25 + message SavedPlaylist { 26 + string id = 1; 27 + string name = 2; 28 + optional string description = 3; 29 + optional string image = 4; 30 + optional string folder_id = 5; 31 + int64 track_count = 6; 32 + int64 created_at = 7; 33 + int64 updated_at = 8; 34 + } 35 + 36 + message GetSavedPlaylistsRequest { 37 + optional string folder_id = 1; 38 + } 39 + message GetSavedPlaylistsResponse { repeated SavedPlaylist playlists = 1; } 40 + 41 + message GetSavedPlaylistRequest { string id = 1; } 42 + message GetSavedPlaylistResponse { optional SavedPlaylist playlist = 1; } 43 + 44 + message CreateSavedPlaylistRequest { 45 + string name = 1; 46 + optional string description = 2; 47 + optional string image = 3; 48 + optional string folder_id = 4; 49 + repeated string track_ids = 5; 50 + } 51 + message CreateSavedPlaylistResponse { SavedPlaylist playlist = 1; } 52 + 53 + message UpdateSavedPlaylistRequest { 54 + string id = 1; 55 + string name = 2; 56 + optional string description = 3; 57 + optional string image = 4; 58 + optional string folder_id = 5; 59 + } 60 + message UpdateSavedPlaylistResponse {} 61 + 62 + message DeleteSavedPlaylistRequest { string id = 1; } 63 + message DeleteSavedPlaylistResponse {} 64 + 65 + // ── Tracks ───────────────────────────────────────────────────────────────── 66 + 67 + message GetSavedPlaylistTracksRequest { string playlist_id = 1; } 68 + message GetSavedPlaylistTracksResponse { repeated string track_ids = 1; } 69 + 70 + message AddTracksToSavedPlaylistRequest { 71 + string playlist_id = 1; 72 + repeated string track_ids = 2; 73 + } 74 + message AddTracksToSavedPlaylistResponse {} 75 + 76 + message RemoveTrackFromSavedPlaylistRequest { 77 + string playlist_id = 1; 78 + string track_id = 2; 79 + } 80 + message RemoveTrackFromSavedPlaylistResponse {} 81 + 82 + message PlaySavedPlaylistRequest { string playlist_id = 1; } 83 + message PlaySavedPlaylistResponse {} 84 + 85 + // ── Service ──────────────────────────────────────────────────────────────── 86 + 87 + service SavedPlaylistService { 88 + rpc CreatePlaylistFolder(CreatePlaylistFolderRequest) 89 + returns (CreatePlaylistFolderResponse) {} 90 + rpc GetPlaylistFolders(GetPlaylistFoldersRequest) 91 + returns (GetPlaylistFoldersResponse) {} 92 + rpc DeletePlaylistFolder(DeletePlaylistFolderRequest) 93 + returns (DeletePlaylistFolderResponse) {} 94 + 95 + rpc GetSavedPlaylists(GetSavedPlaylistsRequest) 96 + returns (GetSavedPlaylistsResponse) {} 97 + rpc GetSavedPlaylist(GetSavedPlaylistRequest) 98 + returns (GetSavedPlaylistResponse) {} 99 + rpc CreateSavedPlaylist(CreateSavedPlaylistRequest) 100 + returns (CreateSavedPlaylistResponse) {} 101 + rpc UpdateSavedPlaylist(UpdateSavedPlaylistRequest) 102 + returns (UpdateSavedPlaylistResponse) {} 103 + rpc DeleteSavedPlaylist(DeleteSavedPlaylistRequest) 104 + returns (DeleteSavedPlaylistResponse) {} 105 + 106 + rpc GetSavedPlaylistTracks(GetSavedPlaylistTracksRequest) 107 + returns (GetSavedPlaylistTracksResponse) {} 108 + rpc AddTracksToSavedPlaylist(AddTracksToSavedPlaylistRequest) 109 + returns (AddTracksToSavedPlaylistResponse) {} 110 + rpc RemoveTrackFromSavedPlaylist(RemoveTrackFromSavedPlaylistRequest) 111 + returns (RemoveTrackFromSavedPlaylistResponse) {} 112 + rpc PlaySavedPlaylist(PlaySavedPlaylistRequest) 113 + returns (PlaySavedPlaylistResponse) {} 114 + }
+104
crates/rpc/proto/rockbox/v1alpha1/smart_playlist.proto
··· 1 + syntax = "proto3"; 2 + 3 + package rockbox.v1alpha1; 4 + 5 + // ── Rule types ───────────────────────────────────────────────────────────── 6 + 7 + message RuleCondition { 8 + string field = 1; 9 + string operator = 2; 10 + optional string value = 3; 11 + optional string value2 = 4; 12 + optional string unit = 5; 13 + } 14 + 15 + message RuleCriteria { 16 + string match_type = 1; 17 + repeated RuleCondition conditions = 2; 18 + optional int32 limit = 3; 19 + optional string sort_by = 4; 20 + optional string sort_order = 5; 21 + } 22 + 23 + // ── Smart playlists ──────────────────────────────────────────────────────── 24 + 25 + message SmartPlaylist { 26 + string id = 1; 27 + string name = 2; 28 + optional string description = 3; 29 + optional string image = 4; 30 + optional string folder_id = 5; 31 + bool is_system = 6; 32 + RuleCriteria rules = 7; 33 + int64 created_at = 8; 34 + int64 updated_at = 9; 35 + } 36 + 37 + message GetSmartPlaylistsRequest {} 38 + message GetSmartPlaylistsResponse { repeated SmartPlaylist playlists = 1; } 39 + 40 + message GetSmartPlaylistRequest { string id = 1; } 41 + message GetSmartPlaylistResponse { optional SmartPlaylist playlist = 1; } 42 + 43 + message CreateSmartPlaylistRequest { 44 + string name = 1; 45 + optional string description = 2; 46 + optional string image = 3; 47 + optional string folder_id = 4; 48 + RuleCriteria rules = 5; 49 + } 50 + message CreateSmartPlaylistResponse { SmartPlaylist playlist = 1; } 51 + 52 + message UpdateSmartPlaylistRequest { 53 + string id = 1; 54 + string name = 2; 55 + optional string description = 3; 56 + optional string image = 4; 57 + optional string folder_id = 5; 58 + RuleCriteria rules = 6; 59 + } 60 + message UpdateSmartPlaylistResponse {} 61 + 62 + message DeleteSmartPlaylistRequest { string id = 1; } 63 + message DeleteSmartPlaylistResponse {} 64 + 65 + message GetSmartPlaylistTracksRequest { string id = 1; } 66 + message GetSmartPlaylistTracksResponse { repeated string track_ids = 1; } 67 + 68 + message PlaySmartPlaylistRequest { string id = 1; } 69 + message PlaySmartPlaylistResponse {} 70 + 71 + // ── Track stats ──────────────────────────────────────────────────────────── 72 + 73 + message TrackStats { 74 + string track_id = 1; 75 + int64 play_count = 2; 76 + int64 skip_count = 3; 77 + optional int64 last_played = 4; 78 + optional int64 last_skipped = 5; 79 + int64 updated_at = 6; 80 + } 81 + 82 + message RecordTrackPlayedRequest { string track_id = 1; } 83 + message RecordTrackPlayedResponse {} 84 + 85 + message RecordTrackSkippedRequest { string track_id = 1; } 86 + message RecordTrackSkippedResponse {} 87 + 88 + message GetTrackStatsRequest { string track_id = 1; } 89 + message GetTrackStatsResponse { optional TrackStats stats = 1; } 90 + 91 + // ── Service ──────────────────────────────────────────────────────────────── 92 + 93 + service SmartPlaylistService { 94 + rpc GetSmartPlaylists(GetSmartPlaylistsRequest) returns (GetSmartPlaylistsResponse); 95 + rpc GetSmartPlaylist(GetSmartPlaylistRequest) returns (GetSmartPlaylistResponse); 96 + rpc CreateSmartPlaylist(CreateSmartPlaylistRequest) returns (CreateSmartPlaylistResponse); 97 + rpc UpdateSmartPlaylist(UpdateSmartPlaylistRequest) returns (UpdateSmartPlaylistResponse); 98 + rpc DeleteSmartPlaylist(DeleteSmartPlaylistRequest) returns (DeleteSmartPlaylistResponse); 99 + rpc GetSmartPlaylistTracks(GetSmartPlaylistTracksRequest) returns (GetSmartPlaylistTracksResponse); 100 + rpc PlaySmartPlaylist(PlaySmartPlaylistRequest) returns (PlaySmartPlaylistResponse); 101 + rpc RecordTrackPlayed(RecordTrackPlayedRequest) returns (RecordTrackPlayedResponse); 102 + rpc RecordTrackSkipped(RecordTrackSkippedRequest) returns (RecordTrackSkippedResponse); 103 + rpc GetTrackStats(GetTrackStatsRequest) returns (GetTrackStatsResponse); 104 + }
+2233
crates/rpc/src/api/rockbox.v1alpha1.rs
··· 1043 1043 pub term: ::prost::alloc::string::String, 1044 1044 } 1045 1045 #[derive(Clone, PartialEq, ::prost::Message)] 1046 + pub struct SearchPlaylist { 1047 + #[prost(string, tag = "1")] 1048 + pub id: ::prost::alloc::string::String, 1049 + #[prost(string, tag = "2")] 1050 + pub name: ::prost::alloc::string::String, 1051 + #[prost(string, optional, tag = "3")] 1052 + pub description: ::core::option::Option<::prost::alloc::string::String>, 1053 + #[prost(string, optional, tag = "4")] 1054 + pub image: ::core::option::Option<::prost::alloc::string::String>, 1055 + #[prost(bool, tag = "5")] 1056 + pub is_smart: bool, 1057 + #[prost(int64, tag = "6")] 1058 + pub track_count: i64, 1059 + } 1060 + #[derive(Clone, PartialEq, ::prost::Message)] 1046 1061 pub struct SearchResponse { 1047 1062 #[prost(message, repeated, tag = "1")] 1048 1063 pub tracks: ::prost::alloc::vec::Vec<Track>, ··· 1050 1065 pub albums: ::prost::alloc::vec::Vec<Album>, 1051 1066 #[prost(message, repeated, tag = "3")] 1052 1067 pub artists: ::prost::alloc::vec::Vec<Artist>, 1068 + #[prost(message, repeated, tag = "4")] 1069 + pub playlists: ::prost::alloc::vec::Vec<SearchPlaylist>, 1053 1070 } 1054 1071 /// Generated client implementations. 1055 1072 pub mod library_service_client { ··· 6064 6081 /// Generated gRPC service name 6065 6082 pub const SERVICE_NAME: &str = "rockbox.v1alpha1.PlaylistService"; 6066 6083 impl<T> tonic::server::NamedService for PlaylistServiceServer<T> { 6084 + const NAME: &'static str = SERVICE_NAME; 6085 + } 6086 + } 6087 + #[derive(Clone, PartialEq, ::prost::Message)] 6088 + pub struct PlaylistFolder { 6089 + #[prost(string, tag = "1")] 6090 + pub id: ::prost::alloc::string::String, 6091 + #[prost(string, tag = "2")] 6092 + pub name: ::prost::alloc::string::String, 6093 + #[prost(int64, tag = "3")] 6094 + pub created_at: i64, 6095 + #[prost(int64, tag = "4")] 6096 + pub updated_at: i64, 6097 + } 6098 + #[derive(Clone, PartialEq, ::prost::Message)] 6099 + pub struct CreatePlaylistFolderRequest { 6100 + #[prost(string, tag = "1")] 6101 + pub name: ::prost::alloc::string::String, 6102 + } 6103 + #[derive(Clone, PartialEq, ::prost::Message)] 6104 + pub struct CreatePlaylistFolderResponse { 6105 + #[prost(message, optional, tag = "1")] 6106 + pub folder: ::core::option::Option<PlaylistFolder>, 6107 + } 6108 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 6109 + pub struct GetPlaylistFoldersRequest {} 6110 + #[derive(Clone, PartialEq, ::prost::Message)] 6111 + pub struct GetPlaylistFoldersResponse { 6112 + #[prost(message, repeated, tag = "1")] 6113 + pub folders: ::prost::alloc::vec::Vec<PlaylistFolder>, 6114 + } 6115 + #[derive(Clone, PartialEq, ::prost::Message)] 6116 + pub struct DeletePlaylistFolderRequest { 6117 + #[prost(string, tag = "1")] 6118 + pub id: ::prost::alloc::string::String, 6119 + } 6120 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 6121 + pub struct DeletePlaylistFolderResponse {} 6122 + #[derive(Clone, PartialEq, ::prost::Message)] 6123 + pub struct SavedPlaylist { 6124 + #[prost(string, tag = "1")] 6125 + pub id: ::prost::alloc::string::String, 6126 + #[prost(string, tag = "2")] 6127 + pub name: ::prost::alloc::string::String, 6128 + #[prost(string, optional, tag = "3")] 6129 + pub description: ::core::option::Option<::prost::alloc::string::String>, 6130 + #[prost(string, optional, tag = "4")] 6131 + pub image: ::core::option::Option<::prost::alloc::string::String>, 6132 + #[prost(string, optional, tag = "5")] 6133 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 6134 + #[prost(int64, tag = "6")] 6135 + pub track_count: i64, 6136 + #[prost(int64, tag = "7")] 6137 + pub created_at: i64, 6138 + #[prost(int64, tag = "8")] 6139 + pub updated_at: i64, 6140 + } 6141 + #[derive(Clone, PartialEq, ::prost::Message)] 6142 + pub struct GetSavedPlaylistsRequest { 6143 + #[prost(string, optional, tag = "1")] 6144 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 6145 + } 6146 + #[derive(Clone, PartialEq, ::prost::Message)] 6147 + pub struct GetSavedPlaylistsResponse { 6148 + #[prost(message, repeated, tag = "1")] 6149 + pub playlists: ::prost::alloc::vec::Vec<SavedPlaylist>, 6150 + } 6151 + #[derive(Clone, PartialEq, ::prost::Message)] 6152 + pub struct GetSavedPlaylistRequest { 6153 + #[prost(string, tag = "1")] 6154 + pub id: ::prost::alloc::string::String, 6155 + } 6156 + #[derive(Clone, PartialEq, ::prost::Message)] 6157 + pub struct GetSavedPlaylistResponse { 6158 + #[prost(message, optional, tag = "1")] 6159 + pub playlist: ::core::option::Option<SavedPlaylist>, 6160 + } 6161 + #[derive(Clone, PartialEq, ::prost::Message)] 6162 + pub struct CreateSavedPlaylistRequest { 6163 + #[prost(string, tag = "1")] 6164 + pub name: ::prost::alloc::string::String, 6165 + #[prost(string, optional, tag = "2")] 6166 + pub description: ::core::option::Option<::prost::alloc::string::String>, 6167 + #[prost(string, optional, tag = "3")] 6168 + pub image: ::core::option::Option<::prost::alloc::string::String>, 6169 + #[prost(string, optional, tag = "4")] 6170 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 6171 + #[prost(string, repeated, tag = "5")] 6172 + pub track_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 6173 + } 6174 + #[derive(Clone, PartialEq, ::prost::Message)] 6175 + pub struct CreateSavedPlaylistResponse { 6176 + #[prost(message, optional, tag = "1")] 6177 + pub playlist: ::core::option::Option<SavedPlaylist>, 6178 + } 6179 + #[derive(Clone, PartialEq, ::prost::Message)] 6180 + pub struct UpdateSavedPlaylistRequest { 6181 + #[prost(string, tag = "1")] 6182 + pub id: ::prost::alloc::string::String, 6183 + #[prost(string, tag = "2")] 6184 + pub name: ::prost::alloc::string::String, 6185 + #[prost(string, optional, tag = "3")] 6186 + pub description: ::core::option::Option<::prost::alloc::string::String>, 6187 + #[prost(string, optional, tag = "4")] 6188 + pub image: ::core::option::Option<::prost::alloc::string::String>, 6189 + #[prost(string, optional, tag = "5")] 6190 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 6191 + } 6192 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 6193 + pub struct UpdateSavedPlaylistResponse {} 6194 + #[derive(Clone, PartialEq, ::prost::Message)] 6195 + pub struct DeleteSavedPlaylistRequest { 6196 + #[prost(string, tag = "1")] 6197 + pub id: ::prost::alloc::string::String, 6198 + } 6199 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 6200 + pub struct DeleteSavedPlaylistResponse {} 6201 + #[derive(Clone, PartialEq, ::prost::Message)] 6202 + pub struct GetSavedPlaylistTracksRequest { 6203 + #[prost(string, tag = "1")] 6204 + pub playlist_id: ::prost::alloc::string::String, 6205 + } 6206 + #[derive(Clone, PartialEq, ::prost::Message)] 6207 + pub struct GetSavedPlaylistTracksResponse { 6208 + #[prost(string, repeated, tag = "1")] 6209 + pub track_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 6210 + } 6211 + #[derive(Clone, PartialEq, ::prost::Message)] 6212 + pub struct AddTracksToSavedPlaylistRequest { 6213 + #[prost(string, tag = "1")] 6214 + pub playlist_id: ::prost::alloc::string::String, 6215 + #[prost(string, repeated, tag = "2")] 6216 + pub track_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 6217 + } 6218 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 6219 + pub struct AddTracksToSavedPlaylistResponse {} 6220 + #[derive(Clone, PartialEq, ::prost::Message)] 6221 + pub struct RemoveTrackFromSavedPlaylistRequest { 6222 + #[prost(string, tag = "1")] 6223 + pub playlist_id: ::prost::alloc::string::String, 6224 + #[prost(string, tag = "2")] 6225 + pub track_id: ::prost::alloc::string::String, 6226 + } 6227 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 6228 + pub struct RemoveTrackFromSavedPlaylistResponse {} 6229 + #[derive(Clone, PartialEq, ::prost::Message)] 6230 + pub struct PlaySavedPlaylistRequest { 6231 + #[prost(string, tag = "1")] 6232 + pub playlist_id: ::prost::alloc::string::String, 6233 + } 6234 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 6235 + pub struct PlaySavedPlaylistResponse {} 6236 + /// Generated client implementations. 6237 + pub mod saved_playlist_service_client { 6238 + #![allow( 6239 + unused_variables, 6240 + dead_code, 6241 + missing_docs, 6242 + clippy::wildcard_imports, 6243 + clippy::let_unit_value 6244 + )] 6245 + use tonic::codegen::http::Uri; 6246 + use tonic::codegen::*; 6247 + #[derive(Debug, Clone)] 6248 + pub struct SavedPlaylistServiceClient<T> { 6249 + inner: tonic::client::Grpc<T>, 6250 + } 6251 + impl SavedPlaylistServiceClient<tonic::transport::Channel> { 6252 + /// Attempt to create a new client by connecting to a given endpoint. 6253 + pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> 6254 + where 6255 + D: TryInto<tonic::transport::Endpoint>, 6256 + D::Error: Into<StdError>, 6257 + { 6258 + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; 6259 + Ok(Self::new(conn)) 6260 + } 6261 + } 6262 + impl<T> SavedPlaylistServiceClient<T> 6263 + where 6264 + T: tonic::client::GrpcService<tonic::body::BoxBody>, 6265 + T::Error: Into<StdError>, 6266 + T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static, 6267 + <T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send, 6268 + { 6269 + pub fn new(inner: T) -> Self { 6270 + let inner = tonic::client::Grpc::new(inner); 6271 + Self { inner } 6272 + } 6273 + pub fn with_origin(inner: T, origin: Uri) -> Self { 6274 + let inner = tonic::client::Grpc::with_origin(inner, origin); 6275 + Self { inner } 6276 + } 6277 + pub fn with_interceptor<F>( 6278 + inner: T, 6279 + interceptor: F, 6280 + ) -> SavedPlaylistServiceClient<InterceptedService<T, F>> 6281 + where 6282 + F: tonic::service::Interceptor, 6283 + T::ResponseBody: Default, 6284 + T: tonic::codegen::Service< 6285 + http::Request<tonic::body::BoxBody>, 6286 + Response = http::Response< 6287 + <T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody, 6288 + >, 6289 + >, 6290 + <T as tonic::codegen::Service<http::Request<tonic::body::BoxBody>>>::Error: 6291 + Into<StdError> + std::marker::Send + std::marker::Sync, 6292 + { 6293 + SavedPlaylistServiceClient::new(InterceptedService::new(inner, interceptor)) 6294 + } 6295 + /// Compress requests with the given encoding. 6296 + /// 6297 + /// This requires the server to support it otherwise it might respond with an 6298 + /// error. 6299 + #[must_use] 6300 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 6301 + self.inner = self.inner.send_compressed(encoding); 6302 + self 6303 + } 6304 + /// Enable decompressing responses. 6305 + #[must_use] 6306 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 6307 + self.inner = self.inner.accept_compressed(encoding); 6308 + self 6309 + } 6310 + /// Limits the maximum size of a decoded message. 6311 + /// 6312 + /// Default: `4MB` 6313 + #[must_use] 6314 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 6315 + self.inner = self.inner.max_decoding_message_size(limit); 6316 + self 6317 + } 6318 + /// Limits the maximum size of an encoded message. 6319 + /// 6320 + /// Default: `usize::MAX` 6321 + #[must_use] 6322 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 6323 + self.inner = self.inner.max_encoding_message_size(limit); 6324 + self 6325 + } 6326 + pub async fn create_playlist_folder( 6327 + &mut self, 6328 + request: impl tonic::IntoRequest<super::CreatePlaylistFolderRequest>, 6329 + ) -> std::result::Result<tonic::Response<super::CreatePlaylistFolderResponse>, tonic::Status> 6330 + { 6331 + self.inner.ready().await.map_err(|e| { 6332 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6333 + })?; 6334 + let codec = tonic::codec::ProstCodec::default(); 6335 + let path = http::uri::PathAndQuery::from_static( 6336 + "/rockbox.v1alpha1.SavedPlaylistService/CreatePlaylistFolder", 6337 + ); 6338 + let mut req = request.into_request(); 6339 + req.extensions_mut().insert(GrpcMethod::new( 6340 + "rockbox.v1alpha1.SavedPlaylistService", 6341 + "CreatePlaylistFolder", 6342 + )); 6343 + self.inner.unary(req, path, codec).await 6344 + } 6345 + pub async fn get_playlist_folders( 6346 + &mut self, 6347 + request: impl tonic::IntoRequest<super::GetPlaylistFoldersRequest>, 6348 + ) -> std::result::Result<tonic::Response<super::GetPlaylistFoldersResponse>, tonic::Status> 6349 + { 6350 + self.inner.ready().await.map_err(|e| { 6351 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6352 + })?; 6353 + let codec = tonic::codec::ProstCodec::default(); 6354 + let path = http::uri::PathAndQuery::from_static( 6355 + "/rockbox.v1alpha1.SavedPlaylistService/GetPlaylistFolders", 6356 + ); 6357 + let mut req = request.into_request(); 6358 + req.extensions_mut().insert(GrpcMethod::new( 6359 + "rockbox.v1alpha1.SavedPlaylistService", 6360 + "GetPlaylistFolders", 6361 + )); 6362 + self.inner.unary(req, path, codec).await 6363 + } 6364 + pub async fn delete_playlist_folder( 6365 + &mut self, 6366 + request: impl tonic::IntoRequest<super::DeletePlaylistFolderRequest>, 6367 + ) -> std::result::Result<tonic::Response<super::DeletePlaylistFolderResponse>, tonic::Status> 6368 + { 6369 + self.inner.ready().await.map_err(|e| { 6370 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6371 + })?; 6372 + let codec = tonic::codec::ProstCodec::default(); 6373 + let path = http::uri::PathAndQuery::from_static( 6374 + "/rockbox.v1alpha1.SavedPlaylistService/DeletePlaylistFolder", 6375 + ); 6376 + let mut req = request.into_request(); 6377 + req.extensions_mut().insert(GrpcMethod::new( 6378 + "rockbox.v1alpha1.SavedPlaylistService", 6379 + "DeletePlaylistFolder", 6380 + )); 6381 + self.inner.unary(req, path, codec).await 6382 + } 6383 + pub async fn get_saved_playlists( 6384 + &mut self, 6385 + request: impl tonic::IntoRequest<super::GetSavedPlaylistsRequest>, 6386 + ) -> std::result::Result<tonic::Response<super::GetSavedPlaylistsResponse>, tonic::Status> 6387 + { 6388 + self.inner.ready().await.map_err(|e| { 6389 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6390 + })?; 6391 + let codec = tonic::codec::ProstCodec::default(); 6392 + let path = http::uri::PathAndQuery::from_static( 6393 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylists", 6394 + ); 6395 + let mut req = request.into_request(); 6396 + req.extensions_mut().insert(GrpcMethod::new( 6397 + "rockbox.v1alpha1.SavedPlaylistService", 6398 + "GetSavedPlaylists", 6399 + )); 6400 + self.inner.unary(req, path, codec).await 6401 + } 6402 + pub async fn get_saved_playlist( 6403 + &mut self, 6404 + request: impl tonic::IntoRequest<super::GetSavedPlaylistRequest>, 6405 + ) -> std::result::Result<tonic::Response<super::GetSavedPlaylistResponse>, tonic::Status> 6406 + { 6407 + self.inner.ready().await.map_err(|e| { 6408 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6409 + })?; 6410 + let codec = tonic::codec::ProstCodec::default(); 6411 + let path = http::uri::PathAndQuery::from_static( 6412 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylist", 6413 + ); 6414 + let mut req = request.into_request(); 6415 + req.extensions_mut().insert(GrpcMethod::new( 6416 + "rockbox.v1alpha1.SavedPlaylistService", 6417 + "GetSavedPlaylist", 6418 + )); 6419 + self.inner.unary(req, path, codec).await 6420 + } 6421 + pub async fn create_saved_playlist( 6422 + &mut self, 6423 + request: impl tonic::IntoRequest<super::CreateSavedPlaylistRequest>, 6424 + ) -> std::result::Result<tonic::Response<super::CreateSavedPlaylistResponse>, tonic::Status> 6425 + { 6426 + self.inner.ready().await.map_err(|e| { 6427 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6428 + })?; 6429 + let codec = tonic::codec::ProstCodec::default(); 6430 + let path = http::uri::PathAndQuery::from_static( 6431 + "/rockbox.v1alpha1.SavedPlaylistService/CreateSavedPlaylist", 6432 + ); 6433 + let mut req = request.into_request(); 6434 + req.extensions_mut().insert(GrpcMethod::new( 6435 + "rockbox.v1alpha1.SavedPlaylistService", 6436 + "CreateSavedPlaylist", 6437 + )); 6438 + self.inner.unary(req, path, codec).await 6439 + } 6440 + pub async fn update_saved_playlist( 6441 + &mut self, 6442 + request: impl tonic::IntoRequest<super::UpdateSavedPlaylistRequest>, 6443 + ) -> std::result::Result<tonic::Response<super::UpdateSavedPlaylistResponse>, tonic::Status> 6444 + { 6445 + self.inner.ready().await.map_err(|e| { 6446 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6447 + })?; 6448 + let codec = tonic::codec::ProstCodec::default(); 6449 + let path = http::uri::PathAndQuery::from_static( 6450 + "/rockbox.v1alpha1.SavedPlaylistService/UpdateSavedPlaylist", 6451 + ); 6452 + let mut req = request.into_request(); 6453 + req.extensions_mut().insert(GrpcMethod::new( 6454 + "rockbox.v1alpha1.SavedPlaylistService", 6455 + "UpdateSavedPlaylist", 6456 + )); 6457 + self.inner.unary(req, path, codec).await 6458 + } 6459 + pub async fn delete_saved_playlist( 6460 + &mut self, 6461 + request: impl tonic::IntoRequest<super::DeleteSavedPlaylistRequest>, 6462 + ) -> std::result::Result<tonic::Response<super::DeleteSavedPlaylistResponse>, tonic::Status> 6463 + { 6464 + self.inner.ready().await.map_err(|e| { 6465 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6466 + })?; 6467 + let codec = tonic::codec::ProstCodec::default(); 6468 + let path = http::uri::PathAndQuery::from_static( 6469 + "/rockbox.v1alpha1.SavedPlaylistService/DeleteSavedPlaylist", 6470 + ); 6471 + let mut req = request.into_request(); 6472 + req.extensions_mut().insert(GrpcMethod::new( 6473 + "rockbox.v1alpha1.SavedPlaylistService", 6474 + "DeleteSavedPlaylist", 6475 + )); 6476 + self.inner.unary(req, path, codec).await 6477 + } 6478 + pub async fn get_saved_playlist_tracks( 6479 + &mut self, 6480 + request: impl tonic::IntoRequest<super::GetSavedPlaylistTracksRequest>, 6481 + ) -> std::result::Result< 6482 + tonic::Response<super::GetSavedPlaylistTracksResponse>, 6483 + tonic::Status, 6484 + > { 6485 + self.inner.ready().await.map_err(|e| { 6486 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6487 + })?; 6488 + let codec = tonic::codec::ProstCodec::default(); 6489 + let path = http::uri::PathAndQuery::from_static( 6490 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylistTracks", 6491 + ); 6492 + let mut req = request.into_request(); 6493 + req.extensions_mut().insert(GrpcMethod::new( 6494 + "rockbox.v1alpha1.SavedPlaylistService", 6495 + "GetSavedPlaylistTracks", 6496 + )); 6497 + self.inner.unary(req, path, codec).await 6498 + } 6499 + pub async fn add_tracks_to_saved_playlist( 6500 + &mut self, 6501 + request: impl tonic::IntoRequest<super::AddTracksToSavedPlaylistRequest>, 6502 + ) -> std::result::Result< 6503 + tonic::Response<super::AddTracksToSavedPlaylistResponse>, 6504 + tonic::Status, 6505 + > { 6506 + self.inner.ready().await.map_err(|e| { 6507 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6508 + })?; 6509 + let codec = tonic::codec::ProstCodec::default(); 6510 + let path = http::uri::PathAndQuery::from_static( 6511 + "/rockbox.v1alpha1.SavedPlaylistService/AddTracksToSavedPlaylist", 6512 + ); 6513 + let mut req = request.into_request(); 6514 + req.extensions_mut().insert(GrpcMethod::new( 6515 + "rockbox.v1alpha1.SavedPlaylistService", 6516 + "AddTracksToSavedPlaylist", 6517 + )); 6518 + self.inner.unary(req, path, codec).await 6519 + } 6520 + pub async fn remove_track_from_saved_playlist( 6521 + &mut self, 6522 + request: impl tonic::IntoRequest<super::RemoveTrackFromSavedPlaylistRequest>, 6523 + ) -> std::result::Result< 6524 + tonic::Response<super::RemoveTrackFromSavedPlaylistResponse>, 6525 + tonic::Status, 6526 + > { 6527 + self.inner.ready().await.map_err(|e| { 6528 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6529 + })?; 6530 + let codec = tonic::codec::ProstCodec::default(); 6531 + let path = http::uri::PathAndQuery::from_static( 6532 + "/rockbox.v1alpha1.SavedPlaylistService/RemoveTrackFromSavedPlaylist", 6533 + ); 6534 + let mut req = request.into_request(); 6535 + req.extensions_mut().insert(GrpcMethod::new( 6536 + "rockbox.v1alpha1.SavedPlaylistService", 6537 + "RemoveTrackFromSavedPlaylist", 6538 + )); 6539 + self.inner.unary(req, path, codec).await 6540 + } 6541 + pub async fn play_saved_playlist( 6542 + &mut self, 6543 + request: impl tonic::IntoRequest<super::PlaySavedPlaylistRequest>, 6544 + ) -> std::result::Result<tonic::Response<super::PlaySavedPlaylistResponse>, tonic::Status> 6545 + { 6546 + self.inner.ready().await.map_err(|e| { 6547 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 6548 + })?; 6549 + let codec = tonic::codec::ProstCodec::default(); 6550 + let path = http::uri::PathAndQuery::from_static( 6551 + "/rockbox.v1alpha1.SavedPlaylistService/PlaySavedPlaylist", 6552 + ); 6553 + let mut req = request.into_request(); 6554 + req.extensions_mut().insert(GrpcMethod::new( 6555 + "rockbox.v1alpha1.SavedPlaylistService", 6556 + "PlaySavedPlaylist", 6557 + )); 6558 + self.inner.unary(req, path, codec).await 6559 + } 6560 + } 6561 + } 6562 + /// Generated server implementations. 6563 + pub mod saved_playlist_service_server { 6564 + #![allow( 6565 + unused_variables, 6566 + dead_code, 6567 + missing_docs, 6568 + clippy::wildcard_imports, 6569 + clippy::let_unit_value 6570 + )] 6571 + use tonic::codegen::*; 6572 + /// Generated trait containing gRPC methods that should be implemented for use with SavedPlaylistServiceServer. 6573 + #[async_trait] 6574 + pub trait SavedPlaylistService: std::marker::Send + std::marker::Sync + 'static { 6575 + async fn create_playlist_folder( 6576 + &self, 6577 + request: tonic::Request<super::CreatePlaylistFolderRequest>, 6578 + ) -> std::result::Result<tonic::Response<super::CreatePlaylistFolderResponse>, tonic::Status>; 6579 + async fn get_playlist_folders( 6580 + &self, 6581 + request: tonic::Request<super::GetPlaylistFoldersRequest>, 6582 + ) -> std::result::Result<tonic::Response<super::GetPlaylistFoldersResponse>, tonic::Status>; 6583 + async fn delete_playlist_folder( 6584 + &self, 6585 + request: tonic::Request<super::DeletePlaylistFolderRequest>, 6586 + ) -> std::result::Result<tonic::Response<super::DeletePlaylistFolderResponse>, tonic::Status>; 6587 + async fn get_saved_playlists( 6588 + &self, 6589 + request: tonic::Request<super::GetSavedPlaylistsRequest>, 6590 + ) -> std::result::Result<tonic::Response<super::GetSavedPlaylistsResponse>, tonic::Status>; 6591 + async fn get_saved_playlist( 6592 + &self, 6593 + request: tonic::Request<super::GetSavedPlaylistRequest>, 6594 + ) -> std::result::Result<tonic::Response<super::GetSavedPlaylistResponse>, tonic::Status>; 6595 + async fn create_saved_playlist( 6596 + &self, 6597 + request: tonic::Request<super::CreateSavedPlaylistRequest>, 6598 + ) -> std::result::Result<tonic::Response<super::CreateSavedPlaylistResponse>, tonic::Status>; 6599 + async fn update_saved_playlist( 6600 + &self, 6601 + request: tonic::Request<super::UpdateSavedPlaylistRequest>, 6602 + ) -> std::result::Result<tonic::Response<super::UpdateSavedPlaylistResponse>, tonic::Status>; 6603 + async fn delete_saved_playlist( 6604 + &self, 6605 + request: tonic::Request<super::DeleteSavedPlaylistRequest>, 6606 + ) -> std::result::Result<tonic::Response<super::DeleteSavedPlaylistResponse>, tonic::Status>; 6607 + async fn get_saved_playlist_tracks( 6608 + &self, 6609 + request: tonic::Request<super::GetSavedPlaylistTracksRequest>, 6610 + ) -> std::result::Result< 6611 + tonic::Response<super::GetSavedPlaylistTracksResponse>, 6612 + tonic::Status, 6613 + >; 6614 + async fn add_tracks_to_saved_playlist( 6615 + &self, 6616 + request: tonic::Request<super::AddTracksToSavedPlaylistRequest>, 6617 + ) -> std::result::Result< 6618 + tonic::Response<super::AddTracksToSavedPlaylistResponse>, 6619 + tonic::Status, 6620 + >; 6621 + async fn remove_track_from_saved_playlist( 6622 + &self, 6623 + request: tonic::Request<super::RemoveTrackFromSavedPlaylistRequest>, 6624 + ) -> std::result::Result< 6625 + tonic::Response<super::RemoveTrackFromSavedPlaylistResponse>, 6626 + tonic::Status, 6627 + >; 6628 + async fn play_saved_playlist( 6629 + &self, 6630 + request: tonic::Request<super::PlaySavedPlaylistRequest>, 6631 + ) -> std::result::Result<tonic::Response<super::PlaySavedPlaylistResponse>, tonic::Status>; 6632 + } 6633 + #[derive(Debug)] 6634 + pub struct SavedPlaylistServiceServer<T> { 6635 + inner: Arc<T>, 6636 + accept_compression_encodings: EnabledCompressionEncodings, 6637 + send_compression_encodings: EnabledCompressionEncodings, 6638 + max_decoding_message_size: Option<usize>, 6639 + max_encoding_message_size: Option<usize>, 6640 + } 6641 + impl<T> SavedPlaylistServiceServer<T> { 6642 + pub fn new(inner: T) -> Self { 6643 + Self::from_arc(Arc::new(inner)) 6644 + } 6645 + pub fn from_arc(inner: Arc<T>) -> Self { 6646 + Self { 6647 + inner, 6648 + accept_compression_encodings: Default::default(), 6649 + send_compression_encodings: Default::default(), 6650 + max_decoding_message_size: None, 6651 + max_encoding_message_size: None, 6652 + } 6653 + } 6654 + pub fn with_interceptor<F>(inner: T, interceptor: F) -> InterceptedService<Self, F> 6655 + where 6656 + F: tonic::service::Interceptor, 6657 + { 6658 + InterceptedService::new(Self::new(inner), interceptor) 6659 + } 6660 + /// Enable decompressing requests with the given encoding. 6661 + #[must_use] 6662 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 6663 + self.accept_compression_encodings.enable(encoding); 6664 + self 6665 + } 6666 + /// Compress responses with the given encoding, if the client supports it. 6667 + #[must_use] 6668 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 6669 + self.send_compression_encodings.enable(encoding); 6670 + self 6671 + } 6672 + /// Limits the maximum size of a decoded message. 6673 + /// 6674 + /// Default: `4MB` 6675 + #[must_use] 6676 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 6677 + self.max_decoding_message_size = Some(limit); 6678 + self 6679 + } 6680 + /// Limits the maximum size of an encoded message. 6681 + /// 6682 + /// Default: `usize::MAX` 6683 + #[must_use] 6684 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 6685 + self.max_encoding_message_size = Some(limit); 6686 + self 6687 + } 6688 + } 6689 + impl<T, B> tonic::codegen::Service<http::Request<B>> for SavedPlaylistServiceServer<T> 6690 + where 6691 + T: SavedPlaylistService, 6692 + B: Body + std::marker::Send + 'static, 6693 + B::Error: Into<StdError> + std::marker::Send + 'static, 6694 + { 6695 + type Response = http::Response<tonic::body::BoxBody>; 6696 + type Error = std::convert::Infallible; 6697 + type Future = BoxFuture<Self::Response, Self::Error>; 6698 + fn poll_ready( 6699 + &mut self, 6700 + _cx: &mut Context<'_>, 6701 + ) -> Poll<std::result::Result<(), Self::Error>> { 6702 + Poll::Ready(Ok(())) 6703 + } 6704 + fn call(&mut self, req: http::Request<B>) -> Self::Future { 6705 + match req.uri().path() { 6706 + "/rockbox.v1alpha1.SavedPlaylistService/CreatePlaylistFolder" => { 6707 + #[allow(non_camel_case_types)] 6708 + struct CreatePlaylistFolderSvc<T: SavedPlaylistService>(pub Arc<T>); 6709 + impl<T: SavedPlaylistService> 6710 + tonic::server::UnaryService<super::CreatePlaylistFolderRequest> 6711 + for CreatePlaylistFolderSvc<T> 6712 + { 6713 + type Response = super::CreatePlaylistFolderResponse; 6714 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 6715 + fn call( 6716 + &mut self, 6717 + request: tonic::Request<super::CreatePlaylistFolderRequest>, 6718 + ) -> Self::Future { 6719 + let inner = Arc::clone(&self.0); 6720 + let fut = async move { 6721 + <T as SavedPlaylistService>::create_playlist_folder(&inner, request) 6722 + .await 6723 + }; 6724 + Box::pin(fut) 6725 + } 6726 + } 6727 + let accept_compression_encodings = self.accept_compression_encodings; 6728 + let send_compression_encodings = self.send_compression_encodings; 6729 + let max_decoding_message_size = self.max_decoding_message_size; 6730 + let max_encoding_message_size = self.max_encoding_message_size; 6731 + let inner = self.inner.clone(); 6732 + let fut = async move { 6733 + let method = CreatePlaylistFolderSvc(inner); 6734 + let codec = tonic::codec::ProstCodec::default(); 6735 + let mut grpc = tonic::server::Grpc::new(codec) 6736 + .apply_compression_config( 6737 + accept_compression_encodings, 6738 + send_compression_encodings, 6739 + ) 6740 + .apply_max_message_size_config( 6741 + max_decoding_message_size, 6742 + max_encoding_message_size, 6743 + ); 6744 + let res = grpc.unary(method, req).await; 6745 + Ok(res) 6746 + }; 6747 + Box::pin(fut) 6748 + } 6749 + "/rockbox.v1alpha1.SavedPlaylistService/GetPlaylistFolders" => { 6750 + #[allow(non_camel_case_types)] 6751 + struct GetPlaylistFoldersSvc<T: SavedPlaylistService>(pub Arc<T>); 6752 + impl<T: SavedPlaylistService> 6753 + tonic::server::UnaryService<super::GetPlaylistFoldersRequest> 6754 + for GetPlaylistFoldersSvc<T> 6755 + { 6756 + type Response = super::GetPlaylistFoldersResponse; 6757 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 6758 + fn call( 6759 + &mut self, 6760 + request: tonic::Request<super::GetPlaylistFoldersRequest>, 6761 + ) -> Self::Future { 6762 + let inner = Arc::clone(&self.0); 6763 + let fut = async move { 6764 + <T as SavedPlaylistService>::get_playlist_folders(&inner, request) 6765 + .await 6766 + }; 6767 + Box::pin(fut) 6768 + } 6769 + } 6770 + let accept_compression_encodings = self.accept_compression_encodings; 6771 + let send_compression_encodings = self.send_compression_encodings; 6772 + let max_decoding_message_size = self.max_decoding_message_size; 6773 + let max_encoding_message_size = self.max_encoding_message_size; 6774 + let inner = self.inner.clone(); 6775 + let fut = async move { 6776 + let method = GetPlaylistFoldersSvc(inner); 6777 + let codec = tonic::codec::ProstCodec::default(); 6778 + let mut grpc = tonic::server::Grpc::new(codec) 6779 + .apply_compression_config( 6780 + accept_compression_encodings, 6781 + send_compression_encodings, 6782 + ) 6783 + .apply_max_message_size_config( 6784 + max_decoding_message_size, 6785 + max_encoding_message_size, 6786 + ); 6787 + let res = grpc.unary(method, req).await; 6788 + Ok(res) 6789 + }; 6790 + Box::pin(fut) 6791 + } 6792 + "/rockbox.v1alpha1.SavedPlaylistService/DeletePlaylistFolder" => { 6793 + #[allow(non_camel_case_types)] 6794 + struct DeletePlaylistFolderSvc<T: SavedPlaylistService>(pub Arc<T>); 6795 + impl<T: SavedPlaylistService> 6796 + tonic::server::UnaryService<super::DeletePlaylistFolderRequest> 6797 + for DeletePlaylistFolderSvc<T> 6798 + { 6799 + type Response = super::DeletePlaylistFolderResponse; 6800 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 6801 + fn call( 6802 + &mut self, 6803 + request: tonic::Request<super::DeletePlaylistFolderRequest>, 6804 + ) -> Self::Future { 6805 + let inner = Arc::clone(&self.0); 6806 + let fut = async move { 6807 + <T as SavedPlaylistService>::delete_playlist_folder(&inner, request) 6808 + .await 6809 + }; 6810 + Box::pin(fut) 6811 + } 6812 + } 6813 + let accept_compression_encodings = self.accept_compression_encodings; 6814 + let send_compression_encodings = self.send_compression_encodings; 6815 + let max_decoding_message_size = self.max_decoding_message_size; 6816 + let max_encoding_message_size = self.max_encoding_message_size; 6817 + let inner = self.inner.clone(); 6818 + let fut = async move { 6819 + let method = DeletePlaylistFolderSvc(inner); 6820 + let codec = tonic::codec::ProstCodec::default(); 6821 + let mut grpc = tonic::server::Grpc::new(codec) 6822 + .apply_compression_config( 6823 + accept_compression_encodings, 6824 + send_compression_encodings, 6825 + ) 6826 + .apply_max_message_size_config( 6827 + max_decoding_message_size, 6828 + max_encoding_message_size, 6829 + ); 6830 + let res = grpc.unary(method, req).await; 6831 + Ok(res) 6832 + }; 6833 + Box::pin(fut) 6834 + } 6835 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylists" => { 6836 + #[allow(non_camel_case_types)] 6837 + struct GetSavedPlaylistsSvc<T: SavedPlaylistService>(pub Arc<T>); 6838 + impl<T: SavedPlaylistService> 6839 + tonic::server::UnaryService<super::GetSavedPlaylistsRequest> 6840 + for GetSavedPlaylistsSvc<T> 6841 + { 6842 + type Response = super::GetSavedPlaylistsResponse; 6843 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 6844 + fn call( 6845 + &mut self, 6846 + request: tonic::Request<super::GetSavedPlaylistsRequest>, 6847 + ) -> Self::Future { 6848 + let inner = Arc::clone(&self.0); 6849 + let fut = async move { 6850 + <T as SavedPlaylistService>::get_saved_playlists(&inner, request) 6851 + .await 6852 + }; 6853 + Box::pin(fut) 6854 + } 6855 + } 6856 + let accept_compression_encodings = self.accept_compression_encodings; 6857 + let send_compression_encodings = self.send_compression_encodings; 6858 + let max_decoding_message_size = self.max_decoding_message_size; 6859 + let max_encoding_message_size = self.max_encoding_message_size; 6860 + let inner = self.inner.clone(); 6861 + let fut = async move { 6862 + let method = GetSavedPlaylistsSvc(inner); 6863 + let codec = tonic::codec::ProstCodec::default(); 6864 + let mut grpc = tonic::server::Grpc::new(codec) 6865 + .apply_compression_config( 6866 + accept_compression_encodings, 6867 + send_compression_encodings, 6868 + ) 6869 + .apply_max_message_size_config( 6870 + max_decoding_message_size, 6871 + max_encoding_message_size, 6872 + ); 6873 + let res = grpc.unary(method, req).await; 6874 + Ok(res) 6875 + }; 6876 + Box::pin(fut) 6877 + } 6878 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylist" => { 6879 + #[allow(non_camel_case_types)] 6880 + struct GetSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 6881 + impl<T: SavedPlaylistService> 6882 + tonic::server::UnaryService<super::GetSavedPlaylistRequest> 6883 + for GetSavedPlaylistSvc<T> 6884 + { 6885 + type Response = super::GetSavedPlaylistResponse; 6886 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 6887 + fn call( 6888 + &mut self, 6889 + request: tonic::Request<super::GetSavedPlaylistRequest>, 6890 + ) -> Self::Future { 6891 + let inner = Arc::clone(&self.0); 6892 + let fut = async move { 6893 + <T as SavedPlaylistService>::get_saved_playlist(&inner, request) 6894 + .await 6895 + }; 6896 + Box::pin(fut) 6897 + } 6898 + } 6899 + let accept_compression_encodings = self.accept_compression_encodings; 6900 + let send_compression_encodings = self.send_compression_encodings; 6901 + let max_decoding_message_size = self.max_decoding_message_size; 6902 + let max_encoding_message_size = self.max_encoding_message_size; 6903 + let inner = self.inner.clone(); 6904 + let fut = async move { 6905 + let method = GetSavedPlaylistSvc(inner); 6906 + let codec = tonic::codec::ProstCodec::default(); 6907 + let mut grpc = tonic::server::Grpc::new(codec) 6908 + .apply_compression_config( 6909 + accept_compression_encodings, 6910 + send_compression_encodings, 6911 + ) 6912 + .apply_max_message_size_config( 6913 + max_decoding_message_size, 6914 + max_encoding_message_size, 6915 + ); 6916 + let res = grpc.unary(method, req).await; 6917 + Ok(res) 6918 + }; 6919 + Box::pin(fut) 6920 + } 6921 + "/rockbox.v1alpha1.SavedPlaylistService/CreateSavedPlaylist" => { 6922 + #[allow(non_camel_case_types)] 6923 + struct CreateSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 6924 + impl<T: SavedPlaylistService> 6925 + tonic::server::UnaryService<super::CreateSavedPlaylistRequest> 6926 + for CreateSavedPlaylistSvc<T> 6927 + { 6928 + type Response = super::CreateSavedPlaylistResponse; 6929 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 6930 + fn call( 6931 + &mut self, 6932 + request: tonic::Request<super::CreateSavedPlaylistRequest>, 6933 + ) -> Self::Future { 6934 + let inner = Arc::clone(&self.0); 6935 + let fut = async move { 6936 + <T as SavedPlaylistService>::create_saved_playlist(&inner, request) 6937 + .await 6938 + }; 6939 + Box::pin(fut) 6940 + } 6941 + } 6942 + let accept_compression_encodings = self.accept_compression_encodings; 6943 + let send_compression_encodings = self.send_compression_encodings; 6944 + let max_decoding_message_size = self.max_decoding_message_size; 6945 + let max_encoding_message_size = self.max_encoding_message_size; 6946 + let inner = self.inner.clone(); 6947 + let fut = async move { 6948 + let method = CreateSavedPlaylistSvc(inner); 6949 + let codec = tonic::codec::ProstCodec::default(); 6950 + let mut grpc = tonic::server::Grpc::new(codec) 6951 + .apply_compression_config( 6952 + accept_compression_encodings, 6953 + send_compression_encodings, 6954 + ) 6955 + .apply_max_message_size_config( 6956 + max_decoding_message_size, 6957 + max_encoding_message_size, 6958 + ); 6959 + let res = grpc.unary(method, req).await; 6960 + Ok(res) 6961 + }; 6962 + Box::pin(fut) 6963 + } 6964 + "/rockbox.v1alpha1.SavedPlaylistService/UpdateSavedPlaylist" => { 6965 + #[allow(non_camel_case_types)] 6966 + struct UpdateSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 6967 + impl<T: SavedPlaylistService> 6968 + tonic::server::UnaryService<super::UpdateSavedPlaylistRequest> 6969 + for UpdateSavedPlaylistSvc<T> 6970 + { 6971 + type Response = super::UpdateSavedPlaylistResponse; 6972 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 6973 + fn call( 6974 + &mut self, 6975 + request: tonic::Request<super::UpdateSavedPlaylistRequest>, 6976 + ) -> Self::Future { 6977 + let inner = Arc::clone(&self.0); 6978 + let fut = async move { 6979 + <T as SavedPlaylistService>::update_saved_playlist(&inner, request) 6980 + .await 6981 + }; 6982 + Box::pin(fut) 6983 + } 6984 + } 6985 + let accept_compression_encodings = self.accept_compression_encodings; 6986 + let send_compression_encodings = self.send_compression_encodings; 6987 + let max_decoding_message_size = self.max_decoding_message_size; 6988 + let max_encoding_message_size = self.max_encoding_message_size; 6989 + let inner = self.inner.clone(); 6990 + let fut = async move { 6991 + let method = UpdateSavedPlaylistSvc(inner); 6992 + let codec = tonic::codec::ProstCodec::default(); 6993 + let mut grpc = tonic::server::Grpc::new(codec) 6994 + .apply_compression_config( 6995 + accept_compression_encodings, 6996 + send_compression_encodings, 6997 + ) 6998 + .apply_max_message_size_config( 6999 + max_decoding_message_size, 7000 + max_encoding_message_size, 7001 + ); 7002 + let res = grpc.unary(method, req).await; 7003 + Ok(res) 7004 + }; 7005 + Box::pin(fut) 7006 + } 7007 + "/rockbox.v1alpha1.SavedPlaylistService/DeleteSavedPlaylist" => { 7008 + #[allow(non_camel_case_types)] 7009 + struct DeleteSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 7010 + impl<T: SavedPlaylistService> 7011 + tonic::server::UnaryService<super::DeleteSavedPlaylistRequest> 7012 + for DeleteSavedPlaylistSvc<T> 7013 + { 7014 + type Response = super::DeleteSavedPlaylistResponse; 7015 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 7016 + fn call( 7017 + &mut self, 7018 + request: tonic::Request<super::DeleteSavedPlaylistRequest>, 7019 + ) -> Self::Future { 7020 + let inner = Arc::clone(&self.0); 7021 + let fut = async move { 7022 + <T as SavedPlaylistService>::delete_saved_playlist(&inner, request) 7023 + .await 7024 + }; 7025 + Box::pin(fut) 7026 + } 7027 + } 7028 + let accept_compression_encodings = self.accept_compression_encodings; 7029 + let send_compression_encodings = self.send_compression_encodings; 7030 + let max_decoding_message_size = self.max_decoding_message_size; 7031 + let max_encoding_message_size = self.max_encoding_message_size; 7032 + let inner = self.inner.clone(); 7033 + let fut = async move { 7034 + let method = DeleteSavedPlaylistSvc(inner); 7035 + let codec = tonic::codec::ProstCodec::default(); 7036 + let mut grpc = tonic::server::Grpc::new(codec) 7037 + .apply_compression_config( 7038 + accept_compression_encodings, 7039 + send_compression_encodings, 7040 + ) 7041 + .apply_max_message_size_config( 7042 + max_decoding_message_size, 7043 + max_encoding_message_size, 7044 + ); 7045 + let res = grpc.unary(method, req).await; 7046 + Ok(res) 7047 + }; 7048 + Box::pin(fut) 7049 + } 7050 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylistTracks" => { 7051 + #[allow(non_camel_case_types)] 7052 + struct GetSavedPlaylistTracksSvc<T: SavedPlaylistService>(pub Arc<T>); 7053 + impl<T: SavedPlaylistService> 7054 + tonic::server::UnaryService<super::GetSavedPlaylistTracksRequest> 7055 + for GetSavedPlaylistTracksSvc<T> 7056 + { 7057 + type Response = super::GetSavedPlaylistTracksResponse; 7058 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 7059 + fn call( 7060 + &mut self, 7061 + request: tonic::Request<super::GetSavedPlaylistTracksRequest>, 7062 + ) -> Self::Future { 7063 + let inner = Arc::clone(&self.0); 7064 + let fut = async move { 7065 + <T as SavedPlaylistService>::get_saved_playlist_tracks( 7066 + &inner, request, 7067 + ) 7068 + .await 7069 + }; 7070 + Box::pin(fut) 7071 + } 7072 + } 7073 + let accept_compression_encodings = self.accept_compression_encodings; 7074 + let send_compression_encodings = self.send_compression_encodings; 7075 + let max_decoding_message_size = self.max_decoding_message_size; 7076 + let max_encoding_message_size = self.max_encoding_message_size; 7077 + let inner = self.inner.clone(); 7078 + let fut = async move { 7079 + let method = GetSavedPlaylistTracksSvc(inner); 7080 + let codec = tonic::codec::ProstCodec::default(); 7081 + let mut grpc = tonic::server::Grpc::new(codec) 7082 + .apply_compression_config( 7083 + accept_compression_encodings, 7084 + send_compression_encodings, 7085 + ) 7086 + .apply_max_message_size_config( 7087 + max_decoding_message_size, 7088 + max_encoding_message_size, 7089 + ); 7090 + let res = grpc.unary(method, req).await; 7091 + Ok(res) 7092 + }; 7093 + Box::pin(fut) 7094 + } 7095 + "/rockbox.v1alpha1.SavedPlaylistService/AddTracksToSavedPlaylist" => { 7096 + #[allow(non_camel_case_types)] 7097 + struct AddTracksToSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 7098 + impl<T: SavedPlaylistService> 7099 + tonic::server::UnaryService<super::AddTracksToSavedPlaylistRequest> 7100 + for AddTracksToSavedPlaylistSvc<T> 7101 + { 7102 + type Response = super::AddTracksToSavedPlaylistResponse; 7103 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 7104 + fn call( 7105 + &mut self, 7106 + request: tonic::Request<super::AddTracksToSavedPlaylistRequest>, 7107 + ) -> Self::Future { 7108 + let inner = Arc::clone(&self.0); 7109 + let fut = async move { 7110 + <T as SavedPlaylistService>::add_tracks_to_saved_playlist( 7111 + &inner, request, 7112 + ) 7113 + .await 7114 + }; 7115 + Box::pin(fut) 7116 + } 7117 + } 7118 + let accept_compression_encodings = self.accept_compression_encodings; 7119 + let send_compression_encodings = self.send_compression_encodings; 7120 + let max_decoding_message_size = self.max_decoding_message_size; 7121 + let max_encoding_message_size = self.max_encoding_message_size; 7122 + let inner = self.inner.clone(); 7123 + let fut = async move { 7124 + let method = AddTracksToSavedPlaylistSvc(inner); 7125 + let codec = tonic::codec::ProstCodec::default(); 7126 + let mut grpc = tonic::server::Grpc::new(codec) 7127 + .apply_compression_config( 7128 + accept_compression_encodings, 7129 + send_compression_encodings, 7130 + ) 7131 + .apply_max_message_size_config( 7132 + max_decoding_message_size, 7133 + max_encoding_message_size, 7134 + ); 7135 + let res = grpc.unary(method, req).await; 7136 + Ok(res) 7137 + }; 7138 + Box::pin(fut) 7139 + } 7140 + "/rockbox.v1alpha1.SavedPlaylistService/RemoveTrackFromSavedPlaylist" => { 7141 + #[allow(non_camel_case_types)] 7142 + struct RemoveTrackFromSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 7143 + impl<T: SavedPlaylistService> 7144 + tonic::server::UnaryService<super::RemoveTrackFromSavedPlaylistRequest> 7145 + for RemoveTrackFromSavedPlaylistSvc<T> 7146 + { 7147 + type Response = super::RemoveTrackFromSavedPlaylistResponse; 7148 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 7149 + fn call( 7150 + &mut self, 7151 + request: tonic::Request<super::RemoveTrackFromSavedPlaylistRequest>, 7152 + ) -> Self::Future { 7153 + let inner = Arc::clone(&self.0); 7154 + let fut = async move { 7155 + <T as SavedPlaylistService>::remove_track_from_saved_playlist( 7156 + &inner, request, 7157 + ) 7158 + .await 7159 + }; 7160 + Box::pin(fut) 7161 + } 7162 + } 7163 + let accept_compression_encodings = self.accept_compression_encodings; 7164 + let send_compression_encodings = self.send_compression_encodings; 7165 + let max_decoding_message_size = self.max_decoding_message_size; 7166 + let max_encoding_message_size = self.max_encoding_message_size; 7167 + let inner = self.inner.clone(); 7168 + let fut = async move { 7169 + let method = RemoveTrackFromSavedPlaylistSvc(inner); 7170 + let codec = tonic::codec::ProstCodec::default(); 7171 + let mut grpc = tonic::server::Grpc::new(codec) 7172 + .apply_compression_config( 7173 + accept_compression_encodings, 7174 + send_compression_encodings, 7175 + ) 7176 + .apply_max_message_size_config( 7177 + max_decoding_message_size, 7178 + max_encoding_message_size, 7179 + ); 7180 + let res = grpc.unary(method, req).await; 7181 + Ok(res) 7182 + }; 7183 + Box::pin(fut) 7184 + } 7185 + "/rockbox.v1alpha1.SavedPlaylistService/PlaySavedPlaylist" => { 7186 + #[allow(non_camel_case_types)] 7187 + struct PlaySavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 7188 + impl<T: SavedPlaylistService> 7189 + tonic::server::UnaryService<super::PlaySavedPlaylistRequest> 7190 + for PlaySavedPlaylistSvc<T> 7191 + { 7192 + type Response = super::PlaySavedPlaylistResponse; 7193 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 7194 + fn call( 7195 + &mut self, 7196 + request: tonic::Request<super::PlaySavedPlaylistRequest>, 7197 + ) -> Self::Future { 7198 + let inner = Arc::clone(&self.0); 7199 + let fut = async move { 7200 + <T as SavedPlaylistService>::play_saved_playlist(&inner, request) 7201 + .await 7202 + }; 7203 + Box::pin(fut) 7204 + } 7205 + } 7206 + let accept_compression_encodings = self.accept_compression_encodings; 7207 + let send_compression_encodings = self.send_compression_encodings; 7208 + let max_decoding_message_size = self.max_decoding_message_size; 7209 + let max_encoding_message_size = self.max_encoding_message_size; 7210 + let inner = self.inner.clone(); 7211 + let fut = async move { 7212 + let method = PlaySavedPlaylistSvc(inner); 7213 + let codec = tonic::codec::ProstCodec::default(); 7214 + let mut grpc = tonic::server::Grpc::new(codec) 7215 + .apply_compression_config( 7216 + accept_compression_encodings, 7217 + send_compression_encodings, 7218 + ) 7219 + .apply_max_message_size_config( 7220 + max_decoding_message_size, 7221 + max_encoding_message_size, 7222 + ); 7223 + let res = grpc.unary(method, req).await; 7224 + Ok(res) 7225 + }; 7226 + Box::pin(fut) 7227 + } 7228 + _ => Box::pin(async move { 7229 + let mut response = http::Response::new(empty_body()); 7230 + let headers = response.headers_mut(); 7231 + headers.insert( 7232 + tonic::Status::GRPC_STATUS, 7233 + (tonic::Code::Unimplemented as i32).into(), 7234 + ); 7235 + headers.insert( 7236 + http::header::CONTENT_TYPE, 7237 + tonic::metadata::GRPC_CONTENT_TYPE, 7238 + ); 7239 + Ok(response) 7240 + }), 7241 + } 7242 + } 7243 + } 7244 + impl<T> Clone for SavedPlaylistServiceServer<T> { 7245 + fn clone(&self) -> Self { 7246 + let inner = self.inner.clone(); 7247 + Self { 7248 + inner, 7249 + accept_compression_encodings: self.accept_compression_encodings, 7250 + send_compression_encodings: self.send_compression_encodings, 7251 + max_decoding_message_size: self.max_decoding_message_size, 7252 + max_encoding_message_size: self.max_encoding_message_size, 7253 + } 7254 + } 7255 + } 7256 + /// Generated gRPC service name 7257 + pub const SERVICE_NAME: &str = "rockbox.v1alpha1.SavedPlaylistService"; 7258 + impl<T> tonic::server::NamedService for SavedPlaylistServiceServer<T> { 7259 + const NAME: &'static str = SERVICE_NAME; 7260 + } 7261 + } 7262 + #[derive(Clone, PartialEq, ::prost::Message)] 7263 + pub struct RuleCondition { 7264 + #[prost(string, tag = "1")] 7265 + pub field: ::prost::alloc::string::String, 7266 + #[prost(string, tag = "2")] 7267 + pub operator: ::prost::alloc::string::String, 7268 + #[prost(string, optional, tag = "3")] 7269 + pub value: ::core::option::Option<::prost::alloc::string::String>, 7270 + #[prost(string, optional, tag = "4")] 7271 + pub value2: ::core::option::Option<::prost::alloc::string::String>, 7272 + #[prost(string, optional, tag = "5")] 7273 + pub unit: ::core::option::Option<::prost::alloc::string::String>, 7274 + } 7275 + #[derive(Clone, PartialEq, ::prost::Message)] 7276 + pub struct RuleCriteria { 7277 + #[prost(string, tag = "1")] 7278 + pub match_type: ::prost::alloc::string::String, 7279 + #[prost(message, repeated, tag = "2")] 7280 + pub conditions: ::prost::alloc::vec::Vec<RuleCondition>, 7281 + #[prost(int32, optional, tag = "3")] 7282 + pub limit: ::core::option::Option<i32>, 7283 + #[prost(string, optional, tag = "4")] 7284 + pub sort_by: ::core::option::Option<::prost::alloc::string::String>, 7285 + #[prost(string, optional, tag = "5")] 7286 + pub sort_order: ::core::option::Option<::prost::alloc::string::String>, 7287 + } 7288 + #[derive(Clone, PartialEq, ::prost::Message)] 7289 + pub struct SmartPlaylist { 7290 + #[prost(string, tag = "1")] 7291 + pub id: ::prost::alloc::string::String, 7292 + #[prost(string, tag = "2")] 7293 + pub name: ::prost::alloc::string::String, 7294 + #[prost(string, optional, tag = "3")] 7295 + pub description: ::core::option::Option<::prost::alloc::string::String>, 7296 + #[prost(string, optional, tag = "4")] 7297 + pub image: ::core::option::Option<::prost::alloc::string::String>, 7298 + #[prost(string, optional, tag = "5")] 7299 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 7300 + #[prost(bool, tag = "6")] 7301 + pub is_system: bool, 7302 + #[prost(message, optional, tag = "7")] 7303 + pub rules: ::core::option::Option<RuleCriteria>, 7304 + #[prost(int64, tag = "8")] 7305 + pub created_at: i64, 7306 + #[prost(int64, tag = "9")] 7307 + pub updated_at: i64, 7308 + } 7309 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 7310 + pub struct GetSmartPlaylistsRequest {} 7311 + #[derive(Clone, PartialEq, ::prost::Message)] 7312 + pub struct GetSmartPlaylistsResponse { 7313 + #[prost(message, repeated, tag = "1")] 7314 + pub playlists: ::prost::alloc::vec::Vec<SmartPlaylist>, 7315 + } 7316 + #[derive(Clone, PartialEq, ::prost::Message)] 7317 + pub struct GetSmartPlaylistRequest { 7318 + #[prost(string, tag = "1")] 7319 + pub id: ::prost::alloc::string::String, 7320 + } 7321 + #[derive(Clone, PartialEq, ::prost::Message)] 7322 + pub struct GetSmartPlaylistResponse { 7323 + #[prost(message, optional, tag = "1")] 7324 + pub playlist: ::core::option::Option<SmartPlaylist>, 7325 + } 7326 + #[derive(Clone, PartialEq, ::prost::Message)] 7327 + pub struct CreateSmartPlaylistRequest { 7328 + #[prost(string, tag = "1")] 7329 + pub name: ::prost::alloc::string::String, 7330 + #[prost(string, optional, tag = "2")] 7331 + pub description: ::core::option::Option<::prost::alloc::string::String>, 7332 + #[prost(string, optional, tag = "3")] 7333 + pub image: ::core::option::Option<::prost::alloc::string::String>, 7334 + #[prost(string, optional, tag = "4")] 7335 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 7336 + #[prost(message, optional, tag = "5")] 7337 + pub rules: ::core::option::Option<RuleCriteria>, 7338 + } 7339 + #[derive(Clone, PartialEq, ::prost::Message)] 7340 + pub struct CreateSmartPlaylistResponse { 7341 + #[prost(message, optional, tag = "1")] 7342 + pub playlist: ::core::option::Option<SmartPlaylist>, 7343 + } 7344 + #[derive(Clone, PartialEq, ::prost::Message)] 7345 + pub struct UpdateSmartPlaylistRequest { 7346 + #[prost(string, tag = "1")] 7347 + pub id: ::prost::alloc::string::String, 7348 + #[prost(string, tag = "2")] 7349 + pub name: ::prost::alloc::string::String, 7350 + #[prost(string, optional, tag = "3")] 7351 + pub description: ::core::option::Option<::prost::alloc::string::String>, 7352 + #[prost(string, optional, tag = "4")] 7353 + pub image: ::core::option::Option<::prost::alloc::string::String>, 7354 + #[prost(string, optional, tag = "5")] 7355 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 7356 + #[prost(message, optional, tag = "6")] 7357 + pub rules: ::core::option::Option<RuleCriteria>, 7358 + } 7359 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 7360 + pub struct UpdateSmartPlaylistResponse {} 7361 + #[derive(Clone, PartialEq, ::prost::Message)] 7362 + pub struct DeleteSmartPlaylistRequest { 7363 + #[prost(string, tag = "1")] 7364 + pub id: ::prost::alloc::string::String, 7365 + } 7366 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 7367 + pub struct DeleteSmartPlaylistResponse {} 7368 + #[derive(Clone, PartialEq, ::prost::Message)] 7369 + pub struct GetSmartPlaylistTracksRequest { 7370 + #[prost(string, tag = "1")] 7371 + pub id: ::prost::alloc::string::String, 7372 + } 7373 + #[derive(Clone, PartialEq, ::prost::Message)] 7374 + pub struct GetSmartPlaylistTracksResponse { 7375 + #[prost(string, repeated, tag = "1")] 7376 + pub track_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 7377 + } 7378 + #[derive(Clone, PartialEq, ::prost::Message)] 7379 + pub struct PlaySmartPlaylistRequest { 7380 + #[prost(string, tag = "1")] 7381 + pub id: ::prost::alloc::string::String, 7382 + } 7383 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 7384 + pub struct PlaySmartPlaylistResponse {} 7385 + #[derive(Clone, PartialEq, ::prost::Message)] 7386 + pub struct TrackStats { 7387 + #[prost(string, tag = "1")] 7388 + pub track_id: ::prost::alloc::string::String, 7389 + #[prost(int64, tag = "2")] 7390 + pub play_count: i64, 7391 + #[prost(int64, tag = "3")] 7392 + pub skip_count: i64, 7393 + #[prost(int64, optional, tag = "4")] 7394 + pub last_played: ::core::option::Option<i64>, 7395 + #[prost(int64, optional, tag = "5")] 7396 + pub last_skipped: ::core::option::Option<i64>, 7397 + #[prost(int64, tag = "6")] 7398 + pub updated_at: i64, 7399 + } 7400 + #[derive(Clone, PartialEq, ::prost::Message)] 7401 + pub struct RecordTrackPlayedRequest { 7402 + #[prost(string, tag = "1")] 7403 + pub track_id: ::prost::alloc::string::String, 7404 + } 7405 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 7406 + pub struct RecordTrackPlayedResponse {} 7407 + #[derive(Clone, PartialEq, ::prost::Message)] 7408 + pub struct RecordTrackSkippedRequest { 7409 + #[prost(string, tag = "1")] 7410 + pub track_id: ::prost::alloc::string::String, 7411 + } 7412 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 7413 + pub struct RecordTrackSkippedResponse {} 7414 + #[derive(Clone, PartialEq, ::prost::Message)] 7415 + pub struct GetTrackStatsRequest { 7416 + #[prost(string, tag = "1")] 7417 + pub track_id: ::prost::alloc::string::String, 7418 + } 7419 + #[derive(Clone, PartialEq, ::prost::Message)] 7420 + pub struct GetTrackStatsResponse { 7421 + #[prost(message, optional, tag = "1")] 7422 + pub stats: ::core::option::Option<TrackStats>, 7423 + } 7424 + /// Generated client implementations. 7425 + pub mod smart_playlist_service_client { 7426 + #![allow( 7427 + unused_variables, 7428 + dead_code, 7429 + missing_docs, 7430 + clippy::wildcard_imports, 7431 + clippy::let_unit_value 7432 + )] 7433 + use tonic::codegen::http::Uri; 7434 + use tonic::codegen::*; 7435 + #[derive(Debug, Clone)] 7436 + pub struct SmartPlaylistServiceClient<T> { 7437 + inner: tonic::client::Grpc<T>, 7438 + } 7439 + impl SmartPlaylistServiceClient<tonic::transport::Channel> { 7440 + /// Attempt to create a new client by connecting to a given endpoint. 7441 + pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> 7442 + where 7443 + D: TryInto<tonic::transport::Endpoint>, 7444 + D::Error: Into<StdError>, 7445 + { 7446 + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; 7447 + Ok(Self::new(conn)) 7448 + } 7449 + } 7450 + impl<T> SmartPlaylistServiceClient<T> 7451 + where 7452 + T: tonic::client::GrpcService<tonic::body::BoxBody>, 7453 + T::Error: Into<StdError>, 7454 + T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static, 7455 + <T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send, 7456 + { 7457 + pub fn new(inner: T) -> Self { 7458 + let inner = tonic::client::Grpc::new(inner); 7459 + Self { inner } 7460 + } 7461 + pub fn with_origin(inner: T, origin: Uri) -> Self { 7462 + let inner = tonic::client::Grpc::with_origin(inner, origin); 7463 + Self { inner } 7464 + } 7465 + pub fn with_interceptor<F>( 7466 + inner: T, 7467 + interceptor: F, 7468 + ) -> SmartPlaylistServiceClient<InterceptedService<T, F>> 7469 + where 7470 + F: tonic::service::Interceptor, 7471 + T::ResponseBody: Default, 7472 + T: tonic::codegen::Service< 7473 + http::Request<tonic::body::BoxBody>, 7474 + Response = http::Response< 7475 + <T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody, 7476 + >, 7477 + >, 7478 + <T as tonic::codegen::Service<http::Request<tonic::body::BoxBody>>>::Error: 7479 + Into<StdError> + std::marker::Send + std::marker::Sync, 7480 + { 7481 + SmartPlaylistServiceClient::new(InterceptedService::new(inner, interceptor)) 7482 + } 7483 + /// Compress requests with the given encoding. 7484 + /// 7485 + /// This requires the server to support it otherwise it might respond with an 7486 + /// error. 7487 + #[must_use] 7488 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 7489 + self.inner = self.inner.send_compressed(encoding); 7490 + self 7491 + } 7492 + /// Enable decompressing responses. 7493 + #[must_use] 7494 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 7495 + self.inner = self.inner.accept_compressed(encoding); 7496 + self 7497 + } 7498 + /// Limits the maximum size of a decoded message. 7499 + /// 7500 + /// Default: `4MB` 7501 + #[must_use] 7502 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 7503 + self.inner = self.inner.max_decoding_message_size(limit); 7504 + self 7505 + } 7506 + /// Limits the maximum size of an encoded message. 7507 + /// 7508 + /// Default: `usize::MAX` 7509 + #[must_use] 7510 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 7511 + self.inner = self.inner.max_encoding_message_size(limit); 7512 + self 7513 + } 7514 + pub async fn get_smart_playlists( 7515 + &mut self, 7516 + request: impl tonic::IntoRequest<super::GetSmartPlaylistsRequest>, 7517 + ) -> std::result::Result<tonic::Response<super::GetSmartPlaylistsResponse>, tonic::Status> 7518 + { 7519 + self.inner.ready().await.map_err(|e| { 7520 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7521 + })?; 7522 + let codec = tonic::codec::ProstCodec::default(); 7523 + let path = http::uri::PathAndQuery::from_static( 7524 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylists", 7525 + ); 7526 + let mut req = request.into_request(); 7527 + req.extensions_mut().insert(GrpcMethod::new( 7528 + "rockbox.v1alpha1.SmartPlaylistService", 7529 + "GetSmartPlaylists", 7530 + )); 7531 + self.inner.unary(req, path, codec).await 7532 + } 7533 + pub async fn get_smart_playlist( 7534 + &mut self, 7535 + request: impl tonic::IntoRequest<super::GetSmartPlaylistRequest>, 7536 + ) -> std::result::Result<tonic::Response<super::GetSmartPlaylistResponse>, tonic::Status> 7537 + { 7538 + self.inner.ready().await.map_err(|e| { 7539 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7540 + })?; 7541 + let codec = tonic::codec::ProstCodec::default(); 7542 + let path = http::uri::PathAndQuery::from_static( 7543 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylist", 7544 + ); 7545 + let mut req = request.into_request(); 7546 + req.extensions_mut().insert(GrpcMethod::new( 7547 + "rockbox.v1alpha1.SmartPlaylistService", 7548 + "GetSmartPlaylist", 7549 + )); 7550 + self.inner.unary(req, path, codec).await 7551 + } 7552 + pub async fn create_smart_playlist( 7553 + &mut self, 7554 + request: impl tonic::IntoRequest<super::CreateSmartPlaylistRequest>, 7555 + ) -> std::result::Result<tonic::Response<super::CreateSmartPlaylistResponse>, tonic::Status> 7556 + { 7557 + self.inner.ready().await.map_err(|e| { 7558 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7559 + })?; 7560 + let codec = tonic::codec::ProstCodec::default(); 7561 + let path = http::uri::PathAndQuery::from_static( 7562 + "/rockbox.v1alpha1.SmartPlaylistService/CreateSmartPlaylist", 7563 + ); 7564 + let mut req = request.into_request(); 7565 + req.extensions_mut().insert(GrpcMethod::new( 7566 + "rockbox.v1alpha1.SmartPlaylistService", 7567 + "CreateSmartPlaylist", 7568 + )); 7569 + self.inner.unary(req, path, codec).await 7570 + } 7571 + pub async fn update_smart_playlist( 7572 + &mut self, 7573 + request: impl tonic::IntoRequest<super::UpdateSmartPlaylistRequest>, 7574 + ) -> std::result::Result<tonic::Response<super::UpdateSmartPlaylistResponse>, tonic::Status> 7575 + { 7576 + self.inner.ready().await.map_err(|e| { 7577 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7578 + })?; 7579 + let codec = tonic::codec::ProstCodec::default(); 7580 + let path = http::uri::PathAndQuery::from_static( 7581 + "/rockbox.v1alpha1.SmartPlaylistService/UpdateSmartPlaylist", 7582 + ); 7583 + let mut req = request.into_request(); 7584 + req.extensions_mut().insert(GrpcMethod::new( 7585 + "rockbox.v1alpha1.SmartPlaylistService", 7586 + "UpdateSmartPlaylist", 7587 + )); 7588 + self.inner.unary(req, path, codec).await 7589 + } 7590 + pub async fn delete_smart_playlist( 7591 + &mut self, 7592 + request: impl tonic::IntoRequest<super::DeleteSmartPlaylistRequest>, 7593 + ) -> std::result::Result<tonic::Response<super::DeleteSmartPlaylistResponse>, tonic::Status> 7594 + { 7595 + self.inner.ready().await.map_err(|e| { 7596 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7597 + })?; 7598 + let codec = tonic::codec::ProstCodec::default(); 7599 + let path = http::uri::PathAndQuery::from_static( 7600 + "/rockbox.v1alpha1.SmartPlaylistService/DeleteSmartPlaylist", 7601 + ); 7602 + let mut req = request.into_request(); 7603 + req.extensions_mut().insert(GrpcMethod::new( 7604 + "rockbox.v1alpha1.SmartPlaylistService", 7605 + "DeleteSmartPlaylist", 7606 + )); 7607 + self.inner.unary(req, path, codec).await 7608 + } 7609 + pub async fn get_smart_playlist_tracks( 7610 + &mut self, 7611 + request: impl tonic::IntoRequest<super::GetSmartPlaylistTracksRequest>, 7612 + ) -> std::result::Result< 7613 + tonic::Response<super::GetSmartPlaylistTracksResponse>, 7614 + tonic::Status, 7615 + > { 7616 + self.inner.ready().await.map_err(|e| { 7617 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7618 + })?; 7619 + let codec = tonic::codec::ProstCodec::default(); 7620 + let path = http::uri::PathAndQuery::from_static( 7621 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylistTracks", 7622 + ); 7623 + let mut req = request.into_request(); 7624 + req.extensions_mut().insert(GrpcMethod::new( 7625 + "rockbox.v1alpha1.SmartPlaylistService", 7626 + "GetSmartPlaylistTracks", 7627 + )); 7628 + self.inner.unary(req, path, codec).await 7629 + } 7630 + pub async fn play_smart_playlist( 7631 + &mut self, 7632 + request: impl tonic::IntoRequest<super::PlaySmartPlaylistRequest>, 7633 + ) -> std::result::Result<tonic::Response<super::PlaySmartPlaylistResponse>, tonic::Status> 7634 + { 7635 + self.inner.ready().await.map_err(|e| { 7636 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7637 + })?; 7638 + let codec = tonic::codec::ProstCodec::default(); 7639 + let path = http::uri::PathAndQuery::from_static( 7640 + "/rockbox.v1alpha1.SmartPlaylistService/PlaySmartPlaylist", 7641 + ); 7642 + let mut req = request.into_request(); 7643 + req.extensions_mut().insert(GrpcMethod::new( 7644 + "rockbox.v1alpha1.SmartPlaylistService", 7645 + "PlaySmartPlaylist", 7646 + )); 7647 + self.inner.unary(req, path, codec).await 7648 + } 7649 + pub async fn record_track_played( 7650 + &mut self, 7651 + request: impl tonic::IntoRequest<super::RecordTrackPlayedRequest>, 7652 + ) -> std::result::Result<tonic::Response<super::RecordTrackPlayedResponse>, tonic::Status> 7653 + { 7654 + self.inner.ready().await.map_err(|e| { 7655 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7656 + })?; 7657 + let codec = tonic::codec::ProstCodec::default(); 7658 + let path = http::uri::PathAndQuery::from_static( 7659 + "/rockbox.v1alpha1.SmartPlaylistService/RecordTrackPlayed", 7660 + ); 7661 + let mut req = request.into_request(); 7662 + req.extensions_mut().insert(GrpcMethod::new( 7663 + "rockbox.v1alpha1.SmartPlaylistService", 7664 + "RecordTrackPlayed", 7665 + )); 7666 + self.inner.unary(req, path, codec).await 7667 + } 7668 + pub async fn record_track_skipped( 7669 + &mut self, 7670 + request: impl tonic::IntoRequest<super::RecordTrackSkippedRequest>, 7671 + ) -> std::result::Result<tonic::Response<super::RecordTrackSkippedResponse>, tonic::Status> 7672 + { 7673 + self.inner.ready().await.map_err(|e| { 7674 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7675 + })?; 7676 + let codec = tonic::codec::ProstCodec::default(); 7677 + let path = http::uri::PathAndQuery::from_static( 7678 + "/rockbox.v1alpha1.SmartPlaylistService/RecordTrackSkipped", 7679 + ); 7680 + let mut req = request.into_request(); 7681 + req.extensions_mut().insert(GrpcMethod::new( 7682 + "rockbox.v1alpha1.SmartPlaylistService", 7683 + "RecordTrackSkipped", 7684 + )); 7685 + self.inner.unary(req, path, codec).await 7686 + } 7687 + pub async fn get_track_stats( 7688 + &mut self, 7689 + request: impl tonic::IntoRequest<super::GetTrackStatsRequest>, 7690 + ) -> std::result::Result<tonic::Response<super::GetTrackStatsResponse>, tonic::Status> 7691 + { 7692 + self.inner.ready().await.map_err(|e| { 7693 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 7694 + })?; 7695 + let codec = tonic::codec::ProstCodec::default(); 7696 + let path = http::uri::PathAndQuery::from_static( 7697 + "/rockbox.v1alpha1.SmartPlaylistService/GetTrackStats", 7698 + ); 7699 + let mut req = request.into_request(); 7700 + req.extensions_mut().insert(GrpcMethod::new( 7701 + "rockbox.v1alpha1.SmartPlaylistService", 7702 + "GetTrackStats", 7703 + )); 7704 + self.inner.unary(req, path, codec).await 7705 + } 7706 + } 7707 + } 7708 + /// Generated server implementations. 7709 + pub mod smart_playlist_service_server { 7710 + #![allow( 7711 + unused_variables, 7712 + dead_code, 7713 + missing_docs, 7714 + clippy::wildcard_imports, 7715 + clippy::let_unit_value 7716 + )] 7717 + use tonic::codegen::*; 7718 + /// Generated trait containing gRPC methods that should be implemented for use with SmartPlaylistServiceServer. 7719 + #[async_trait] 7720 + pub trait SmartPlaylistService: std::marker::Send + std::marker::Sync + 'static { 7721 + async fn get_smart_playlists( 7722 + &self, 7723 + request: tonic::Request<super::GetSmartPlaylistsRequest>, 7724 + ) -> std::result::Result<tonic::Response<super::GetSmartPlaylistsResponse>, tonic::Status>; 7725 + async fn get_smart_playlist( 7726 + &self, 7727 + request: tonic::Request<super::GetSmartPlaylistRequest>, 7728 + ) -> std::result::Result<tonic::Response<super::GetSmartPlaylistResponse>, tonic::Status>; 7729 + async fn create_smart_playlist( 7730 + &self, 7731 + request: tonic::Request<super::CreateSmartPlaylistRequest>, 7732 + ) -> std::result::Result<tonic::Response<super::CreateSmartPlaylistResponse>, tonic::Status>; 7733 + async fn update_smart_playlist( 7734 + &self, 7735 + request: tonic::Request<super::UpdateSmartPlaylistRequest>, 7736 + ) -> std::result::Result<tonic::Response<super::UpdateSmartPlaylistResponse>, tonic::Status>; 7737 + async fn delete_smart_playlist( 7738 + &self, 7739 + request: tonic::Request<super::DeleteSmartPlaylistRequest>, 7740 + ) -> std::result::Result<tonic::Response<super::DeleteSmartPlaylistResponse>, tonic::Status>; 7741 + async fn get_smart_playlist_tracks( 7742 + &self, 7743 + request: tonic::Request<super::GetSmartPlaylistTracksRequest>, 7744 + ) -> std::result::Result< 7745 + tonic::Response<super::GetSmartPlaylistTracksResponse>, 7746 + tonic::Status, 7747 + >; 7748 + async fn play_smart_playlist( 7749 + &self, 7750 + request: tonic::Request<super::PlaySmartPlaylistRequest>, 7751 + ) -> std::result::Result<tonic::Response<super::PlaySmartPlaylistResponse>, tonic::Status>; 7752 + async fn record_track_played( 7753 + &self, 7754 + request: tonic::Request<super::RecordTrackPlayedRequest>, 7755 + ) -> std::result::Result<tonic::Response<super::RecordTrackPlayedResponse>, tonic::Status>; 7756 + async fn record_track_skipped( 7757 + &self, 7758 + request: tonic::Request<super::RecordTrackSkippedRequest>, 7759 + ) -> std::result::Result<tonic::Response<super::RecordTrackSkippedResponse>, tonic::Status>; 7760 + async fn get_track_stats( 7761 + &self, 7762 + request: tonic::Request<super::GetTrackStatsRequest>, 7763 + ) -> std::result::Result<tonic::Response<super::GetTrackStatsResponse>, tonic::Status>; 7764 + } 7765 + #[derive(Debug)] 7766 + pub struct SmartPlaylistServiceServer<T> { 7767 + inner: Arc<T>, 7768 + accept_compression_encodings: EnabledCompressionEncodings, 7769 + send_compression_encodings: EnabledCompressionEncodings, 7770 + max_decoding_message_size: Option<usize>, 7771 + max_encoding_message_size: Option<usize>, 7772 + } 7773 + impl<T> SmartPlaylistServiceServer<T> { 7774 + pub fn new(inner: T) -> Self { 7775 + Self::from_arc(Arc::new(inner)) 7776 + } 7777 + pub fn from_arc(inner: Arc<T>) -> Self { 7778 + Self { 7779 + inner, 7780 + accept_compression_encodings: Default::default(), 7781 + send_compression_encodings: Default::default(), 7782 + max_decoding_message_size: None, 7783 + max_encoding_message_size: None, 7784 + } 7785 + } 7786 + pub fn with_interceptor<F>(inner: T, interceptor: F) -> InterceptedService<Self, F> 7787 + where 7788 + F: tonic::service::Interceptor, 7789 + { 7790 + InterceptedService::new(Self::new(inner), interceptor) 7791 + } 7792 + /// Enable decompressing requests with the given encoding. 7793 + #[must_use] 7794 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 7795 + self.accept_compression_encodings.enable(encoding); 7796 + self 7797 + } 7798 + /// Compress responses with the given encoding, if the client supports it. 7799 + #[must_use] 7800 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 7801 + self.send_compression_encodings.enable(encoding); 7802 + self 7803 + } 7804 + /// Limits the maximum size of a decoded message. 7805 + /// 7806 + /// Default: `4MB` 7807 + #[must_use] 7808 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 7809 + self.max_decoding_message_size = Some(limit); 7810 + self 7811 + } 7812 + /// Limits the maximum size of an encoded message. 7813 + /// 7814 + /// Default: `usize::MAX` 7815 + #[must_use] 7816 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 7817 + self.max_encoding_message_size = Some(limit); 7818 + self 7819 + } 7820 + } 7821 + impl<T, B> tonic::codegen::Service<http::Request<B>> for SmartPlaylistServiceServer<T> 7822 + where 7823 + T: SmartPlaylistService, 7824 + B: Body + std::marker::Send + 'static, 7825 + B::Error: Into<StdError> + std::marker::Send + 'static, 7826 + { 7827 + type Response = http::Response<tonic::body::BoxBody>; 7828 + type Error = std::convert::Infallible; 7829 + type Future = BoxFuture<Self::Response, Self::Error>; 7830 + fn poll_ready( 7831 + &mut self, 7832 + _cx: &mut Context<'_>, 7833 + ) -> Poll<std::result::Result<(), Self::Error>> { 7834 + Poll::Ready(Ok(())) 7835 + } 7836 + fn call(&mut self, req: http::Request<B>) -> Self::Future { 7837 + match req.uri().path() { 7838 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylists" => { 7839 + #[allow(non_camel_case_types)] 7840 + struct GetSmartPlaylistsSvc<T: SmartPlaylistService>(pub Arc<T>); 7841 + impl<T: SmartPlaylistService> 7842 + tonic::server::UnaryService<super::GetSmartPlaylistsRequest> 7843 + for GetSmartPlaylistsSvc<T> 7844 + { 7845 + type Response = super::GetSmartPlaylistsResponse; 7846 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 7847 + fn call( 7848 + &mut self, 7849 + request: tonic::Request<super::GetSmartPlaylistsRequest>, 7850 + ) -> Self::Future { 7851 + let inner = Arc::clone(&self.0); 7852 + let fut = async move { 7853 + <T as SmartPlaylistService>::get_smart_playlists(&inner, request) 7854 + .await 7855 + }; 7856 + Box::pin(fut) 7857 + } 7858 + } 7859 + let accept_compression_encodings = self.accept_compression_encodings; 7860 + let send_compression_encodings = self.send_compression_encodings; 7861 + let max_decoding_message_size = self.max_decoding_message_size; 7862 + let max_encoding_message_size = self.max_encoding_message_size; 7863 + let inner = self.inner.clone(); 7864 + let fut = async move { 7865 + let method = GetSmartPlaylistsSvc(inner); 7866 + let codec = tonic::codec::ProstCodec::default(); 7867 + let mut grpc = tonic::server::Grpc::new(codec) 7868 + .apply_compression_config( 7869 + accept_compression_encodings, 7870 + send_compression_encodings, 7871 + ) 7872 + .apply_max_message_size_config( 7873 + max_decoding_message_size, 7874 + max_encoding_message_size, 7875 + ); 7876 + let res = grpc.unary(method, req).await; 7877 + Ok(res) 7878 + }; 7879 + Box::pin(fut) 7880 + } 7881 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylist" => { 7882 + #[allow(non_camel_case_types)] 7883 + struct GetSmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 7884 + impl<T: SmartPlaylistService> 7885 + tonic::server::UnaryService<super::GetSmartPlaylistRequest> 7886 + for GetSmartPlaylistSvc<T> 7887 + { 7888 + type Response = super::GetSmartPlaylistResponse; 7889 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 7890 + fn call( 7891 + &mut self, 7892 + request: tonic::Request<super::GetSmartPlaylistRequest>, 7893 + ) -> Self::Future { 7894 + let inner = Arc::clone(&self.0); 7895 + let fut = async move { 7896 + <T as SmartPlaylistService>::get_smart_playlist(&inner, request) 7897 + .await 7898 + }; 7899 + Box::pin(fut) 7900 + } 7901 + } 7902 + let accept_compression_encodings = self.accept_compression_encodings; 7903 + let send_compression_encodings = self.send_compression_encodings; 7904 + let max_decoding_message_size = self.max_decoding_message_size; 7905 + let max_encoding_message_size = self.max_encoding_message_size; 7906 + let inner = self.inner.clone(); 7907 + let fut = async move { 7908 + let method = GetSmartPlaylistSvc(inner); 7909 + let codec = tonic::codec::ProstCodec::default(); 7910 + let mut grpc = tonic::server::Grpc::new(codec) 7911 + .apply_compression_config( 7912 + accept_compression_encodings, 7913 + send_compression_encodings, 7914 + ) 7915 + .apply_max_message_size_config( 7916 + max_decoding_message_size, 7917 + max_encoding_message_size, 7918 + ); 7919 + let res = grpc.unary(method, req).await; 7920 + Ok(res) 7921 + }; 7922 + Box::pin(fut) 7923 + } 7924 + "/rockbox.v1alpha1.SmartPlaylistService/CreateSmartPlaylist" => { 7925 + #[allow(non_camel_case_types)] 7926 + struct CreateSmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 7927 + impl<T: SmartPlaylistService> 7928 + tonic::server::UnaryService<super::CreateSmartPlaylistRequest> 7929 + for CreateSmartPlaylistSvc<T> 7930 + { 7931 + type Response = super::CreateSmartPlaylistResponse; 7932 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 7933 + fn call( 7934 + &mut self, 7935 + request: tonic::Request<super::CreateSmartPlaylistRequest>, 7936 + ) -> Self::Future { 7937 + let inner = Arc::clone(&self.0); 7938 + let fut = async move { 7939 + <T as SmartPlaylistService>::create_smart_playlist(&inner, request) 7940 + .await 7941 + }; 7942 + Box::pin(fut) 7943 + } 7944 + } 7945 + let accept_compression_encodings = self.accept_compression_encodings; 7946 + let send_compression_encodings = self.send_compression_encodings; 7947 + let max_decoding_message_size = self.max_decoding_message_size; 7948 + let max_encoding_message_size = self.max_encoding_message_size; 7949 + let inner = self.inner.clone(); 7950 + let fut = async move { 7951 + let method = CreateSmartPlaylistSvc(inner); 7952 + let codec = tonic::codec::ProstCodec::default(); 7953 + let mut grpc = tonic::server::Grpc::new(codec) 7954 + .apply_compression_config( 7955 + accept_compression_encodings, 7956 + send_compression_encodings, 7957 + ) 7958 + .apply_max_message_size_config( 7959 + max_decoding_message_size, 7960 + max_encoding_message_size, 7961 + ); 7962 + let res = grpc.unary(method, req).await; 7963 + Ok(res) 7964 + }; 7965 + Box::pin(fut) 7966 + } 7967 + "/rockbox.v1alpha1.SmartPlaylistService/UpdateSmartPlaylist" => { 7968 + #[allow(non_camel_case_types)] 7969 + struct UpdateSmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 7970 + impl<T: SmartPlaylistService> 7971 + tonic::server::UnaryService<super::UpdateSmartPlaylistRequest> 7972 + for UpdateSmartPlaylistSvc<T> 7973 + { 7974 + type Response = super::UpdateSmartPlaylistResponse; 7975 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 7976 + fn call( 7977 + &mut self, 7978 + request: tonic::Request<super::UpdateSmartPlaylistRequest>, 7979 + ) -> Self::Future { 7980 + let inner = Arc::clone(&self.0); 7981 + let fut = async move { 7982 + <T as SmartPlaylistService>::update_smart_playlist(&inner, request) 7983 + .await 7984 + }; 7985 + Box::pin(fut) 7986 + } 7987 + } 7988 + let accept_compression_encodings = self.accept_compression_encodings; 7989 + let send_compression_encodings = self.send_compression_encodings; 7990 + let max_decoding_message_size = self.max_decoding_message_size; 7991 + let max_encoding_message_size = self.max_encoding_message_size; 7992 + let inner = self.inner.clone(); 7993 + let fut = async move { 7994 + let method = UpdateSmartPlaylistSvc(inner); 7995 + let codec = tonic::codec::ProstCodec::default(); 7996 + let mut grpc = tonic::server::Grpc::new(codec) 7997 + .apply_compression_config( 7998 + accept_compression_encodings, 7999 + send_compression_encodings, 8000 + ) 8001 + .apply_max_message_size_config( 8002 + max_decoding_message_size, 8003 + max_encoding_message_size, 8004 + ); 8005 + let res = grpc.unary(method, req).await; 8006 + Ok(res) 8007 + }; 8008 + Box::pin(fut) 8009 + } 8010 + "/rockbox.v1alpha1.SmartPlaylistService/DeleteSmartPlaylist" => { 8011 + #[allow(non_camel_case_types)] 8012 + struct DeleteSmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 8013 + impl<T: SmartPlaylistService> 8014 + tonic::server::UnaryService<super::DeleteSmartPlaylistRequest> 8015 + for DeleteSmartPlaylistSvc<T> 8016 + { 8017 + type Response = super::DeleteSmartPlaylistResponse; 8018 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 8019 + fn call( 8020 + &mut self, 8021 + request: tonic::Request<super::DeleteSmartPlaylistRequest>, 8022 + ) -> Self::Future { 8023 + let inner = Arc::clone(&self.0); 8024 + let fut = async move { 8025 + <T as SmartPlaylistService>::delete_smart_playlist(&inner, request) 8026 + .await 8027 + }; 8028 + Box::pin(fut) 8029 + } 8030 + } 8031 + let accept_compression_encodings = self.accept_compression_encodings; 8032 + let send_compression_encodings = self.send_compression_encodings; 8033 + let max_decoding_message_size = self.max_decoding_message_size; 8034 + let max_encoding_message_size = self.max_encoding_message_size; 8035 + let inner = self.inner.clone(); 8036 + let fut = async move { 8037 + let method = DeleteSmartPlaylistSvc(inner); 8038 + let codec = tonic::codec::ProstCodec::default(); 8039 + let mut grpc = tonic::server::Grpc::new(codec) 8040 + .apply_compression_config( 8041 + accept_compression_encodings, 8042 + send_compression_encodings, 8043 + ) 8044 + .apply_max_message_size_config( 8045 + max_decoding_message_size, 8046 + max_encoding_message_size, 8047 + ); 8048 + let res = grpc.unary(method, req).await; 8049 + Ok(res) 8050 + }; 8051 + Box::pin(fut) 8052 + } 8053 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylistTracks" => { 8054 + #[allow(non_camel_case_types)] 8055 + struct GetSmartPlaylistTracksSvc<T: SmartPlaylistService>(pub Arc<T>); 8056 + impl<T: SmartPlaylistService> 8057 + tonic::server::UnaryService<super::GetSmartPlaylistTracksRequest> 8058 + for GetSmartPlaylistTracksSvc<T> 8059 + { 8060 + type Response = super::GetSmartPlaylistTracksResponse; 8061 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 8062 + fn call( 8063 + &mut self, 8064 + request: tonic::Request<super::GetSmartPlaylistTracksRequest>, 8065 + ) -> Self::Future { 8066 + let inner = Arc::clone(&self.0); 8067 + let fut = async move { 8068 + <T as SmartPlaylistService>::get_smart_playlist_tracks( 8069 + &inner, request, 8070 + ) 8071 + .await 8072 + }; 8073 + Box::pin(fut) 8074 + } 8075 + } 8076 + let accept_compression_encodings = self.accept_compression_encodings; 8077 + let send_compression_encodings = self.send_compression_encodings; 8078 + let max_decoding_message_size = self.max_decoding_message_size; 8079 + let max_encoding_message_size = self.max_encoding_message_size; 8080 + let inner = self.inner.clone(); 8081 + let fut = async move { 8082 + let method = GetSmartPlaylistTracksSvc(inner); 8083 + let codec = tonic::codec::ProstCodec::default(); 8084 + let mut grpc = tonic::server::Grpc::new(codec) 8085 + .apply_compression_config( 8086 + accept_compression_encodings, 8087 + send_compression_encodings, 8088 + ) 8089 + .apply_max_message_size_config( 8090 + max_decoding_message_size, 8091 + max_encoding_message_size, 8092 + ); 8093 + let res = grpc.unary(method, req).await; 8094 + Ok(res) 8095 + }; 8096 + Box::pin(fut) 8097 + } 8098 + "/rockbox.v1alpha1.SmartPlaylistService/PlaySmartPlaylist" => { 8099 + #[allow(non_camel_case_types)] 8100 + struct PlaySmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 8101 + impl<T: SmartPlaylistService> 8102 + tonic::server::UnaryService<super::PlaySmartPlaylistRequest> 8103 + for PlaySmartPlaylistSvc<T> 8104 + { 8105 + type Response = super::PlaySmartPlaylistResponse; 8106 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 8107 + fn call( 8108 + &mut self, 8109 + request: tonic::Request<super::PlaySmartPlaylistRequest>, 8110 + ) -> Self::Future { 8111 + let inner = Arc::clone(&self.0); 8112 + let fut = async move { 8113 + <T as SmartPlaylistService>::play_smart_playlist(&inner, request) 8114 + .await 8115 + }; 8116 + Box::pin(fut) 8117 + } 8118 + } 8119 + let accept_compression_encodings = self.accept_compression_encodings; 8120 + let send_compression_encodings = self.send_compression_encodings; 8121 + let max_decoding_message_size = self.max_decoding_message_size; 8122 + let max_encoding_message_size = self.max_encoding_message_size; 8123 + let inner = self.inner.clone(); 8124 + let fut = async move { 8125 + let method = PlaySmartPlaylistSvc(inner); 8126 + let codec = tonic::codec::ProstCodec::default(); 8127 + let mut grpc = tonic::server::Grpc::new(codec) 8128 + .apply_compression_config( 8129 + accept_compression_encodings, 8130 + send_compression_encodings, 8131 + ) 8132 + .apply_max_message_size_config( 8133 + max_decoding_message_size, 8134 + max_encoding_message_size, 8135 + ); 8136 + let res = grpc.unary(method, req).await; 8137 + Ok(res) 8138 + }; 8139 + Box::pin(fut) 8140 + } 8141 + "/rockbox.v1alpha1.SmartPlaylistService/RecordTrackPlayed" => { 8142 + #[allow(non_camel_case_types)] 8143 + struct RecordTrackPlayedSvc<T: SmartPlaylistService>(pub Arc<T>); 8144 + impl<T: SmartPlaylistService> 8145 + tonic::server::UnaryService<super::RecordTrackPlayedRequest> 8146 + for RecordTrackPlayedSvc<T> 8147 + { 8148 + type Response = super::RecordTrackPlayedResponse; 8149 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 8150 + fn call( 8151 + &mut self, 8152 + request: tonic::Request<super::RecordTrackPlayedRequest>, 8153 + ) -> Self::Future { 8154 + let inner = Arc::clone(&self.0); 8155 + let fut = async move { 8156 + <T as SmartPlaylistService>::record_track_played(&inner, request) 8157 + .await 8158 + }; 8159 + Box::pin(fut) 8160 + } 8161 + } 8162 + let accept_compression_encodings = self.accept_compression_encodings; 8163 + let send_compression_encodings = self.send_compression_encodings; 8164 + let max_decoding_message_size = self.max_decoding_message_size; 8165 + let max_encoding_message_size = self.max_encoding_message_size; 8166 + let inner = self.inner.clone(); 8167 + let fut = async move { 8168 + let method = RecordTrackPlayedSvc(inner); 8169 + let codec = tonic::codec::ProstCodec::default(); 8170 + let mut grpc = tonic::server::Grpc::new(codec) 8171 + .apply_compression_config( 8172 + accept_compression_encodings, 8173 + send_compression_encodings, 8174 + ) 8175 + .apply_max_message_size_config( 8176 + max_decoding_message_size, 8177 + max_encoding_message_size, 8178 + ); 8179 + let res = grpc.unary(method, req).await; 8180 + Ok(res) 8181 + }; 8182 + Box::pin(fut) 8183 + } 8184 + "/rockbox.v1alpha1.SmartPlaylistService/RecordTrackSkipped" => { 8185 + #[allow(non_camel_case_types)] 8186 + struct RecordTrackSkippedSvc<T: SmartPlaylistService>(pub Arc<T>); 8187 + impl<T: SmartPlaylistService> 8188 + tonic::server::UnaryService<super::RecordTrackSkippedRequest> 8189 + for RecordTrackSkippedSvc<T> 8190 + { 8191 + type Response = super::RecordTrackSkippedResponse; 8192 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 8193 + fn call( 8194 + &mut self, 8195 + request: tonic::Request<super::RecordTrackSkippedRequest>, 8196 + ) -> Self::Future { 8197 + let inner = Arc::clone(&self.0); 8198 + let fut = async move { 8199 + <T as SmartPlaylistService>::record_track_skipped(&inner, request) 8200 + .await 8201 + }; 8202 + Box::pin(fut) 8203 + } 8204 + } 8205 + let accept_compression_encodings = self.accept_compression_encodings; 8206 + let send_compression_encodings = self.send_compression_encodings; 8207 + let max_decoding_message_size = self.max_decoding_message_size; 8208 + let max_encoding_message_size = self.max_encoding_message_size; 8209 + let inner = self.inner.clone(); 8210 + let fut = async move { 8211 + let method = RecordTrackSkippedSvc(inner); 8212 + let codec = tonic::codec::ProstCodec::default(); 8213 + let mut grpc = tonic::server::Grpc::new(codec) 8214 + .apply_compression_config( 8215 + accept_compression_encodings, 8216 + send_compression_encodings, 8217 + ) 8218 + .apply_max_message_size_config( 8219 + max_decoding_message_size, 8220 + max_encoding_message_size, 8221 + ); 8222 + let res = grpc.unary(method, req).await; 8223 + Ok(res) 8224 + }; 8225 + Box::pin(fut) 8226 + } 8227 + "/rockbox.v1alpha1.SmartPlaylistService/GetTrackStats" => { 8228 + #[allow(non_camel_case_types)] 8229 + struct GetTrackStatsSvc<T: SmartPlaylistService>(pub Arc<T>); 8230 + impl<T: SmartPlaylistService> 8231 + tonic::server::UnaryService<super::GetTrackStatsRequest> 8232 + for GetTrackStatsSvc<T> 8233 + { 8234 + type Response = super::GetTrackStatsResponse; 8235 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 8236 + fn call( 8237 + &mut self, 8238 + request: tonic::Request<super::GetTrackStatsRequest>, 8239 + ) -> Self::Future { 8240 + let inner = Arc::clone(&self.0); 8241 + let fut = async move { 8242 + <T as SmartPlaylistService>::get_track_stats(&inner, request).await 8243 + }; 8244 + Box::pin(fut) 8245 + } 8246 + } 8247 + let accept_compression_encodings = self.accept_compression_encodings; 8248 + let send_compression_encodings = self.send_compression_encodings; 8249 + let max_decoding_message_size = self.max_decoding_message_size; 8250 + let max_encoding_message_size = self.max_encoding_message_size; 8251 + let inner = self.inner.clone(); 8252 + let fut = async move { 8253 + let method = GetTrackStatsSvc(inner); 8254 + let codec = tonic::codec::ProstCodec::default(); 8255 + let mut grpc = tonic::server::Grpc::new(codec) 8256 + .apply_compression_config( 8257 + accept_compression_encodings, 8258 + send_compression_encodings, 8259 + ) 8260 + .apply_max_message_size_config( 8261 + max_decoding_message_size, 8262 + max_encoding_message_size, 8263 + ); 8264 + let res = grpc.unary(method, req).await; 8265 + Ok(res) 8266 + }; 8267 + Box::pin(fut) 8268 + } 8269 + _ => Box::pin(async move { 8270 + let mut response = http::Response::new(empty_body()); 8271 + let headers = response.headers_mut(); 8272 + headers.insert( 8273 + tonic::Status::GRPC_STATUS, 8274 + (tonic::Code::Unimplemented as i32).into(), 8275 + ); 8276 + headers.insert( 8277 + http::header::CONTENT_TYPE, 8278 + tonic::metadata::GRPC_CONTENT_TYPE, 8279 + ); 8280 + Ok(response) 8281 + }), 8282 + } 8283 + } 8284 + } 8285 + impl<T> Clone for SmartPlaylistServiceServer<T> { 8286 + fn clone(&self) -> Self { 8287 + let inner = self.inner.clone(); 8288 + Self { 8289 + inner, 8290 + accept_compression_encodings: self.accept_compression_encodings, 8291 + send_compression_encodings: self.send_compression_encodings, 8292 + max_decoding_message_size: self.max_decoding_message_size, 8293 + max_encoding_message_size: self.max_encoding_message_size, 8294 + } 8295 + } 8296 + } 8297 + /// Generated gRPC service name 8298 + pub const SERVICE_NAME: &str = "rockbox.v1alpha1.SmartPlaylistService"; 8299 + impl<T> tonic::server::NamedService for SmartPlaylistServiceServer<T> { 6067 8300 const NAME: &'static str = SERVICE_NAME; 6068 8301 } 6069 8302 }
crates/rpc/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+2
crates/rpc/src/lib.rs
··· 8 8 pub mod metadata; 9 9 pub mod playback; 10 10 pub mod playlist; 11 + pub mod saved_playlist; 11 12 pub mod server; 12 13 pub mod settings; 14 + pub mod smart_playlist; 13 15 pub mod sound; 14 16 pub mod system; 15 17 pub mod types;
+22 -4
crates/rpc/src/library.rs
··· 2 2 3 3 use rockbox_graphql::{simplebroker::SimpleBroker, types::ScanCompleted}; 4 4 use rockbox_library::{entity::favourites::Favourites, repo}; 5 - use rockbox_typesense::client::{search_albums, search_artists, search_tracks}; 5 + use rockbox_typesense::client::{search_albums, search_artists, search_playlists, search_tracks}; 6 6 use sqlx::Sqlite; 7 7 use tokio_stream::{Stream, StreamExt}; 8 8 ··· 13 13 GetArtistsRequest, GetArtistsResponse, GetLikedAlbumsRequest, GetLikedAlbumsResponse, 14 14 GetLikedTracksRequest, GetLikedTracksResponse, GetTrackRequest, GetTrackResponse, 15 15 GetTracksRequest, GetTracksResponse, LikeAlbumRequest, LikeAlbumResponse, LikeTrackRequest, 16 - LikeTrackResponse, ScanLibraryRequest, ScanLibraryResponse, SearchRequest, SearchResponse, 17 - StreamLibraryRequest, StreamLibraryResponse, UnlikeAlbumRequest, UnlikeAlbumResponse, 18 - UnlikeTrackRequest, UnlikeTrackResponse, 16 + LikeTrackResponse, ScanLibraryRequest, ScanLibraryResponse, SearchPlaylist, SearchRequest, 17 + SearchResponse, StreamLibraryRequest, StreamLibraryResponse, UnlikeAlbumRequest, 18 + UnlikeAlbumResponse, UnlikeTrackRequest, UnlikeTrackResponse, 19 19 }, 20 20 rockbox_url, 21 21 }; ··· 288 288 .map_err(|e| tonic::Status::internal(e.to_string()))? 289 289 .map(|r| r.hits.into_iter().map(|h| h.document.into()).collect()) 290 290 .unwrap_or_default(); 291 + let playlists = search_playlists(&term) 292 + .await 293 + .unwrap_or_default() 294 + .map(|r| { 295 + r.hits 296 + .into_iter() 297 + .map(|h| SearchPlaylist { 298 + id: h.document.id, 299 + name: h.document.name, 300 + description: h.document.description, 301 + image: h.document.image, 302 + is_smart: h.document.is_smart, 303 + track_count: h.document.track_count, 304 + }) 305 + .collect() 306 + }) 307 + .unwrap_or_default(); 291 308 292 309 Ok(tonic::Response::new(SearchResponse { 293 310 tracks, 294 311 albums, 295 312 artists, 313 + playlists, 296 314 })) 297 315 } 298 316
+259
crates/rpc/src/saved_playlist.rs
··· 1 + use rockbox_playlists::{Playlist, PlaylistFolder, PlaylistStore}; 2 + use rockbox_typesense::client::{delete_playlist as ts_delete_playlist, insert_playlists}; 3 + use rockbox_typesense::types::Playlist as TsPlaylist; 4 + 5 + use crate::api::rockbox::v1alpha1::{ 6 + saved_playlist_service_server::SavedPlaylistService, AddTracksToSavedPlaylistRequest, 7 + AddTracksToSavedPlaylistResponse, CreatePlaylistFolderRequest, CreatePlaylistFolderResponse, 8 + CreateSavedPlaylistRequest, CreateSavedPlaylistResponse, DeletePlaylistFolderRequest, 9 + DeletePlaylistFolderResponse, DeleteSavedPlaylistRequest, DeleteSavedPlaylistResponse, 10 + GetPlaylistFoldersRequest, GetPlaylistFoldersResponse, GetSavedPlaylistRequest, 11 + GetSavedPlaylistResponse, GetSavedPlaylistTracksRequest, GetSavedPlaylistTracksResponse, 12 + GetSavedPlaylistsRequest, GetSavedPlaylistsResponse, PlaySavedPlaylistRequest, 13 + PlaySavedPlaylistResponse, PlaylistFolder as ProtoFolder, RemoveTrackFromSavedPlaylistRequest, 14 + RemoveTrackFromSavedPlaylistResponse, SavedPlaylist as ProtoPlaylist, 15 + UpdateSavedPlaylistRequest, UpdateSavedPlaylistResponse, 16 + }; 17 + use crate::rockbox_url; 18 + 19 + pub struct SavedPlaylist { 20 + store: PlaylistStore, 21 + client: reqwest::Client, 22 + } 23 + 24 + impl SavedPlaylist { 25 + pub fn new(store: PlaylistStore, client: reqwest::Client) -> Self { 26 + Self { store, client } 27 + } 28 + } 29 + 30 + fn to_ts_playlist(p: &Playlist) -> TsPlaylist { 31 + TsPlaylist { 32 + id: p.id.clone(), 33 + name: p.name.clone(), 34 + description: p.description.clone(), 35 + image: p.image.clone(), 36 + is_smart: false, 37 + track_count: p.track_count, 38 + } 39 + } 40 + 41 + fn to_proto_folder(f: PlaylistFolder) -> ProtoFolder { 42 + ProtoFolder { 43 + id: f.id, 44 + name: f.name, 45 + created_at: f.created_at, 46 + updated_at: f.updated_at, 47 + } 48 + } 49 + 50 + fn to_proto_playlist(p: Playlist) -> ProtoPlaylist { 51 + ProtoPlaylist { 52 + id: p.id, 53 + name: p.name, 54 + description: p.description, 55 + image: p.image, 56 + folder_id: p.folder_id, 57 + track_count: p.track_count, 58 + created_at: p.created_at, 59 + updated_at: p.updated_at, 60 + } 61 + } 62 + 63 + #[tonic::async_trait] 64 + impl SavedPlaylistService for SavedPlaylist { 65 + async fn create_playlist_folder( 66 + &self, 67 + request: tonic::Request<CreatePlaylistFolderRequest>, 68 + ) -> Result<tonic::Response<CreatePlaylistFolderResponse>, tonic::Status> { 69 + let name = request.into_inner().name; 70 + let folder = self 71 + .store 72 + .create_folder(&name) 73 + .await 74 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 75 + Ok(tonic::Response::new(CreatePlaylistFolderResponse { 76 + folder: Some(to_proto_folder(folder)), 77 + })) 78 + } 79 + 80 + async fn get_playlist_folders( 81 + &self, 82 + _request: tonic::Request<GetPlaylistFoldersRequest>, 83 + ) -> Result<tonic::Response<GetPlaylistFoldersResponse>, tonic::Status> { 84 + let folders = self 85 + .store 86 + .list_folders() 87 + .await 88 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 89 + Ok(tonic::Response::new(GetPlaylistFoldersResponse { 90 + folders: folders.into_iter().map(to_proto_folder).collect(), 91 + })) 92 + } 93 + 94 + async fn delete_playlist_folder( 95 + &self, 96 + request: tonic::Request<DeletePlaylistFolderRequest>, 97 + ) -> Result<tonic::Response<DeletePlaylistFolderResponse>, tonic::Status> { 98 + let id = request.into_inner().id; 99 + self.store 100 + .delete_folder(&id) 101 + .await 102 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 103 + Ok(tonic::Response::new(DeletePlaylistFolderResponse {})) 104 + } 105 + 106 + async fn get_saved_playlists( 107 + &self, 108 + request: tonic::Request<GetSavedPlaylistsRequest>, 109 + ) -> Result<tonic::Response<GetSavedPlaylistsResponse>, tonic::Status> { 110 + let folder_id = request.into_inner().folder_id; 111 + let playlists = if let Some(fid) = folder_id.as_deref().filter(|s| !s.is_empty()) { 112 + self.store 113 + .list_by_folder(fid) 114 + .await 115 + .map_err(|e| tonic::Status::internal(e.to_string()))? 116 + } else { 117 + self.store 118 + .list() 119 + .await 120 + .map_err(|e| tonic::Status::internal(e.to_string()))? 121 + }; 122 + Ok(tonic::Response::new(GetSavedPlaylistsResponse { 123 + playlists: playlists.into_iter().map(to_proto_playlist).collect(), 124 + })) 125 + } 126 + 127 + async fn get_saved_playlist( 128 + &self, 129 + request: tonic::Request<GetSavedPlaylistRequest>, 130 + ) -> Result<tonic::Response<GetSavedPlaylistResponse>, tonic::Status> { 131 + let id = request.into_inner().id; 132 + let playlist = self 133 + .store 134 + .get(&id) 135 + .await 136 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 137 + Ok(tonic::Response::new(GetSavedPlaylistResponse { 138 + playlist: playlist.map(to_proto_playlist), 139 + })) 140 + } 141 + 142 + async fn create_saved_playlist( 143 + &self, 144 + request: tonic::Request<CreateSavedPlaylistRequest>, 145 + ) -> Result<tonic::Response<CreateSavedPlaylistResponse>, tonic::Status> { 146 + let req = request.into_inner(); 147 + let playlist = self 148 + .store 149 + .create( 150 + &req.name, 151 + req.description.as_deref(), 152 + req.image.as_deref(), 153 + req.folder_id.as_deref(), 154 + ) 155 + .await 156 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 157 + if !req.track_ids.is_empty() { 158 + self.store 159 + .add_tracks(&playlist.id, &req.track_ids) 160 + .await 161 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 162 + } 163 + let ts_p = to_ts_playlist(&playlist); 164 + let _ = insert_playlists(vec![ts_p]).await; 165 + Ok(tonic::Response::new(CreateSavedPlaylistResponse { 166 + playlist: Some(to_proto_playlist(playlist)), 167 + })) 168 + } 169 + 170 + async fn update_saved_playlist( 171 + &self, 172 + request: tonic::Request<UpdateSavedPlaylistRequest>, 173 + ) -> Result<tonic::Response<UpdateSavedPlaylistResponse>, tonic::Status> { 174 + let req = request.into_inner(); 175 + self.store 176 + .update( 177 + &req.id, 178 + &req.name, 179 + req.description.as_deref(), 180 + req.image.as_deref(), 181 + req.folder_id.as_deref(), 182 + ) 183 + .await 184 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 185 + if let Ok(Some(updated)) = self.store.get(&req.id).await { 186 + let ts_p = to_ts_playlist(&updated); 187 + let _ = insert_playlists(vec![ts_p]).await; 188 + } 189 + Ok(tonic::Response::new(UpdateSavedPlaylistResponse {})) 190 + } 191 + 192 + async fn delete_saved_playlist( 193 + &self, 194 + request: tonic::Request<DeleteSavedPlaylistRequest>, 195 + ) -> Result<tonic::Response<DeleteSavedPlaylistResponse>, tonic::Status> { 196 + let id = request.into_inner().id; 197 + self.store 198 + .delete(&id) 199 + .await 200 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 201 + let _ = ts_delete_playlist(&id).await; 202 + Ok(tonic::Response::new(DeleteSavedPlaylistResponse {})) 203 + } 204 + 205 + async fn get_saved_playlist_tracks( 206 + &self, 207 + request: tonic::Request<GetSavedPlaylistTracksRequest>, 208 + ) -> Result<tonic::Response<GetSavedPlaylistTracksResponse>, tonic::Status> { 209 + let playlist_id = request.into_inner().playlist_id; 210 + let track_ids = self 211 + .store 212 + .get_track_ids(&playlist_id) 213 + .await 214 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 215 + Ok(tonic::Response::new(GetSavedPlaylistTracksResponse { 216 + track_ids, 217 + })) 218 + } 219 + 220 + async fn add_tracks_to_saved_playlist( 221 + &self, 222 + request: tonic::Request<AddTracksToSavedPlaylistRequest>, 223 + ) -> Result<tonic::Response<AddTracksToSavedPlaylistResponse>, tonic::Status> { 224 + let req = request.into_inner(); 225 + self.store 226 + .add_tracks(&req.playlist_id, &req.track_ids) 227 + .await 228 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 229 + Ok(tonic::Response::new(AddTracksToSavedPlaylistResponse {})) 230 + } 231 + 232 + async fn remove_track_from_saved_playlist( 233 + &self, 234 + request: tonic::Request<RemoveTrackFromSavedPlaylistRequest>, 235 + ) -> Result<tonic::Response<RemoveTrackFromSavedPlaylistResponse>, tonic::Status> { 236 + let req = request.into_inner(); 237 + self.store 238 + .remove_track(&req.playlist_id, &req.track_id) 239 + .await 240 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 241 + Ok(tonic::Response::new( 242 + RemoveTrackFromSavedPlaylistResponse {}, 243 + )) 244 + } 245 + 246 + async fn play_saved_playlist( 247 + &self, 248 + request: tonic::Request<PlaySavedPlaylistRequest>, 249 + ) -> Result<tonic::Response<PlaySavedPlaylistResponse>, tonic::Status> { 250 + let playlist_id = request.into_inner().playlist_id; 251 + let url = format!("{}/saved-playlists/{}/play", rockbox_url(), playlist_id); 252 + self.client 253 + .post(&url) 254 + .send() 255 + .await 256 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 257 + Ok(tonic::Response::new(PlaySavedPlaylistResponse {})) 258 + } 259 + }
+12
crates/rpc/src/server.rs
··· 7 7 use crate::api::rockbox::v1alpha1::library_service_server::LibraryServiceServer; 8 8 use crate::api::rockbox::v1alpha1::playback_service_server::PlaybackServiceServer; 9 9 use crate::api::rockbox::v1alpha1::playlist_service_server::PlaylistServiceServer; 10 + use crate::api::rockbox::v1alpha1::saved_playlist_service_server::SavedPlaylistServiceServer; 10 11 use crate::api::rockbox::v1alpha1::settings_service_server::SettingsServiceServer; 12 + use crate::api::rockbox::v1alpha1::smart_playlist_service_server::SmartPlaylistServiceServer; 11 13 use crate::api::rockbox::v1alpha1::sound_service_server::SoundServiceServer; 12 14 use crate::api::rockbox::FILE_DESCRIPTOR_SET; 13 15 use crate::browse::Browse; ··· 15 17 use crate::library::Library; 16 18 use crate::playback::Playback; 17 19 use crate::playlist::Playlist; 20 + use crate::saved_playlist::SavedPlaylist; 18 21 use crate::settings::Settings; 22 + use crate::smart_playlist::SmartPlaylistRpc; 19 23 use crate::sound::Sound; 20 24 use crate::system::System; 21 25 use rockbox_library::create_connection_pool; 26 + use rockbox_playlists::PlaylistStore; 22 27 use rockbox_sys::events::RockboxCommand; 23 28 use tonic::transport::Server; 24 29 ··· 34 39 35 40 let client = reqwest::Client::new(); 36 41 let pool = create_connection_pool().await?; 42 + let playlist_store = PlaylistStore::new(pool.clone()); 37 43 38 44 Server::builder() 39 45 .accept_http1(true) ··· 69 75 System::new(client.clone()), 70 76 ), 71 77 )) 78 + .add_service(tonic_web::enable(SavedPlaylistServiceServer::new( 79 + SavedPlaylist::new(playlist_store.clone(), client.clone()), 80 + ))) 81 + .add_service(tonic_web::enable(SmartPlaylistServiceServer::new( 82 + SmartPlaylistRpc::new(playlist_store.clone(), pool.clone(), client.clone()), 83 + ))) 72 84 .serve(addr) 73 85 .await?; 74 86 Ok(())
+503
crates/rpc/src/smart_playlist.rs
··· 1 + use rockbox_playlists::{SmartPlaylist, TrackStats}; 2 + use rockbox_typesense::client::{delete_playlist as ts_delete_playlist, insert_playlists}; 3 + use rockbox_typesense::types::Playlist as TsPlaylist; 4 + 5 + use crate::api::rockbox::v1alpha1::{ 6 + smart_playlist_service_server::SmartPlaylistService, CreateSmartPlaylistRequest, 7 + CreateSmartPlaylistResponse, DeleteSmartPlaylistRequest, DeleteSmartPlaylistResponse, 8 + GetSmartPlaylistRequest, GetSmartPlaylistResponse, GetSmartPlaylistTracksRequest, 9 + GetSmartPlaylistTracksResponse, GetSmartPlaylistsRequest, GetSmartPlaylistsResponse, 10 + GetTrackStatsRequest, GetTrackStatsResponse, PlaySmartPlaylistRequest, 11 + PlaySmartPlaylistResponse, RecordTrackPlayedRequest, RecordTrackPlayedResponse, 12 + RecordTrackSkippedRequest, RecordTrackSkippedResponse, RuleCondition as ProtoRuleCondition, 13 + RuleCriteria as ProtoRuleCriteria, SmartPlaylist as ProtoSmartPlaylist, 14 + TrackStats as ProtoTrackStats, UpdateSmartPlaylistRequest, UpdateSmartPlaylistResponse, 15 + }; 16 + use rockbox_library::repo; 17 + use rockbox_playlists::rules::{Candidate, RuleCriteria}; 18 + use sqlx::{Pool, Sqlite}; 19 + use std::collections::HashMap; 20 + 21 + use crate::rockbox_url; 22 + 23 + pub struct SmartPlaylistRpc { 24 + store: rockbox_playlists::PlaylistStore, 25 + pool: Pool<Sqlite>, 26 + client: reqwest::Client, 27 + } 28 + 29 + impl SmartPlaylistRpc { 30 + pub fn new( 31 + store: rockbox_playlists::PlaylistStore, 32 + pool: Pool<Sqlite>, 33 + client: reqwest::Client, 34 + ) -> Self { 35 + Self { 36 + store, 37 + pool, 38 + client, 39 + } 40 + } 41 + } 42 + 43 + fn criteria_to_rules(c: &ProtoRuleCriteria) -> RuleCriteria { 44 + use rockbox_playlists::rules::{ 45 + Condition, MatchType, RuleField, RuleOperator, SortOrder, TimeUnit, 46 + }; 47 + 48 + let match_type = if c.match_type == "any" { 49 + MatchType::Any 50 + } else { 51 + MatchType::All 52 + }; 53 + 54 + let conditions = c 55 + .conditions 56 + .iter() 57 + .map(|cond| { 58 + let field = match cond.field.as_str() { 59 + "play_count" => RuleField::PlayCount, 60 + "skip_count" => RuleField::SkipCount, 61 + "last_played" => RuleField::LastPlayed, 62 + "last_skipped" => RuleField::LastSkipped, 63 + "date_added" => RuleField::DateAdded, 64 + "year" => RuleField::Year, 65 + "genre" => RuleField::Genre, 66 + "artist" => RuleField::Artist, 67 + "album" => RuleField::Album, 68 + "duration_ms" => RuleField::DurationMs, 69 + "bitrate" => RuleField::Bitrate, 70 + "is_liked" => RuleField::IsLiked, 71 + _ => RuleField::PlayCount, 72 + }; 73 + let operator = match cond.operator.as_str() { 74 + "is" => RuleOperator::Is, 75 + "is_not" => RuleOperator::IsNot, 76 + "contains" => RuleOperator::Contains, 77 + "not_contains" => RuleOperator::NotContains, 78 + "greater_than" => RuleOperator::GreaterThan, 79 + "less_than" => RuleOperator::LessThan, 80 + "greater_than_or_equal" => RuleOperator::GreaterThanOrEqual, 81 + "less_than_or_equal" => RuleOperator::LessThanOrEqual, 82 + "equals" => RuleOperator::Equals, 83 + "between" => RuleOperator::Between, 84 + "in_last" => RuleOperator::InLast, 85 + "not_in_last" => RuleOperator::NotInLast, 86 + "is_empty" => RuleOperator::IsEmpty, 87 + "is_not_empty" => RuleOperator::IsNotEmpty, 88 + _ => RuleOperator::Is, 89 + }; 90 + let unit = cond.unit.as_ref().and_then(|u| match u.as_str() { 91 + "days" => Some(TimeUnit::Days), 92 + "weeks" => Some(TimeUnit::Weeks), 93 + "months" => Some(TimeUnit::Months), 94 + "years" => Some(TimeUnit::Years), 95 + _ => None, 96 + }); 97 + let value = cond 98 + .value 99 + .as_ref() 100 + .map(|v| serde_json::Value::String(v.clone())); 101 + let value2 = cond 102 + .value2 103 + .as_ref() 104 + .map(|v| serde_json::Value::String(v.clone())); 105 + Condition { 106 + field, 107 + operator, 108 + value, 109 + value2, 110 + unit, 111 + } 112 + }) 113 + .collect(); 114 + 115 + let sort_by = c.sort_by.as_ref().and_then(|s| { 116 + use rockbox_playlists::rules::SortField; 117 + match s.as_str() { 118 + "random" => Some(SortField::Random), 119 + "play_count" => Some(SortField::PlayCount), 120 + "skip_count" => Some(SortField::SkipCount), 121 + "last_played" => Some(SortField::LastPlayed), 122 + "date_added" => Some(SortField::DateAdded), 123 + "year" => Some(SortField::Year), 124 + "title" => Some(SortField::Title), 125 + "artist" => Some(SortField::Artist), 126 + "album" => Some(SortField::Album), 127 + "duration_ms" => Some(SortField::DurationMs), 128 + _ => None, 129 + } 130 + }); 131 + 132 + let sort_order = c.sort_order.as_ref().and_then(|s| match s.as_str() { 133 + "ASC" => Some(SortOrder::Asc), 134 + "DESC" => Some(SortOrder::Desc), 135 + _ => None, 136 + }); 137 + 138 + RuleCriteria { 139 + match_type, 140 + conditions, 141 + limit: c.limit.map(|l| l as usize), 142 + sort_by, 143 + sort_order, 144 + } 145 + } 146 + 147 + fn to_proto_criteria(p: &rockbox_playlists::rules::RuleCriteria) -> ProtoRuleCriteria { 148 + use rockbox_playlists::rules::{RuleField, RuleOperator, SortField, SortOrder, TimeUnit}; 149 + 150 + let match_type = match p.match_type { 151 + rockbox_playlists::rules::MatchType::All => "all".to_string(), 152 + rockbox_playlists::rules::MatchType::Any => "any".to_string(), 153 + }; 154 + 155 + let conditions: Vec<ProtoRuleCondition> = p 156 + .conditions 157 + .iter() 158 + .map(|c| { 159 + let field = match c.field { 160 + RuleField::PlayCount => "play_count", 161 + RuleField::SkipCount => "skip_count", 162 + RuleField::LastPlayed => "last_played", 163 + RuleField::LastSkipped => "last_skipped", 164 + RuleField::DateAdded => "date_added", 165 + RuleField::Year => "year", 166 + RuleField::Genre => "genre", 167 + RuleField::Artist => "artist", 168 + RuleField::Album => "album", 169 + RuleField::DurationMs => "duration_ms", 170 + RuleField::Bitrate => "bitrate", 171 + RuleField::IsLiked => "is_liked", 172 + }; 173 + let operator = match c.operator { 174 + RuleOperator::Is => "is", 175 + RuleOperator::IsNot => "is_not", 176 + RuleOperator::Contains => "contains", 177 + RuleOperator::NotContains => "not_contains", 178 + RuleOperator::GreaterThan => "greater_than", 179 + RuleOperator::LessThan => "less_than", 180 + RuleOperator::GreaterThanOrEqual => "greater_than_or_equal", 181 + RuleOperator::LessThanOrEqual => "less_than_or_equal", 182 + RuleOperator::Equals => "equals", 183 + RuleOperator::Between => "between", 184 + RuleOperator::InLast => "in_last", 185 + RuleOperator::NotInLast => "not_in_last", 186 + RuleOperator::IsEmpty => "is_empty", 187 + RuleOperator::IsNotEmpty => "is_not_empty", 188 + }; 189 + let unit = c.unit.as_ref().map(|u| { 190 + match u { 191 + TimeUnit::Days => "days", 192 + TimeUnit::Weeks => "weeks", 193 + TimeUnit::Months => "months", 194 + TimeUnit::Years => "years", 195 + } 196 + .to_string() 197 + }); 198 + ProtoRuleCondition { 199 + field: field.to_string(), 200 + operator: operator.to_string(), 201 + value: c.value.as_ref().map(|v| v.to_string()), 202 + value2: c.value2.as_ref().map(|v| v.to_string()), 203 + unit, 204 + } 205 + }) 206 + .collect(); 207 + 208 + let sort_by = p.sort_by.as_ref().map(|s| { 209 + match s { 210 + SortField::Random => "random", 211 + SortField::PlayCount => "play_count", 212 + SortField::SkipCount => "skip_count", 213 + SortField::LastPlayed => "last_played", 214 + SortField::DateAdded => "date_added", 215 + SortField::Year => "year", 216 + SortField::Title => "title", 217 + SortField::Artist => "artist", 218 + SortField::Album => "album", 219 + SortField::DurationMs => "duration_ms", 220 + } 221 + .to_string() 222 + }); 223 + let sort_order = p.sort_order.as_ref().map(|s| { 224 + match s { 225 + SortOrder::Asc => "ASC", 226 + SortOrder::Desc => "DESC", 227 + } 228 + .to_string() 229 + }); 230 + 231 + ProtoRuleCriteria { 232 + match_type, 233 + conditions, 234 + limit: p.limit.map(|l| l as i32), 235 + sort_by, 236 + sort_order, 237 + } 238 + } 239 + 240 + fn to_proto_smart_playlist(p: SmartPlaylist) -> ProtoSmartPlaylist { 241 + ProtoSmartPlaylist { 242 + id: p.id, 243 + name: p.name, 244 + description: p.description, 245 + image: p.image, 246 + folder_id: p.folder_id, 247 + is_system: p.is_system, 248 + rules: Some(to_proto_criteria(&p.rules)), 249 + created_at: p.created_at, 250 + updated_at: p.updated_at, 251 + } 252 + } 253 + 254 + fn to_proto_stats(s: TrackStats) -> ProtoTrackStats { 255 + ProtoTrackStats { 256 + track_id: s.track_id, 257 + play_count: s.play_count, 258 + skip_count: s.skip_count, 259 + last_played: s.last_played, 260 + last_skipped: s.last_skipped, 261 + updated_at: s.updated_at, 262 + } 263 + } 264 + 265 + #[tonic::async_trait] 266 + impl SmartPlaylistService for SmartPlaylistRpc { 267 + async fn get_smart_playlists( 268 + &self, 269 + _request: tonic::Request<GetSmartPlaylistsRequest>, 270 + ) -> Result<tonic::Response<GetSmartPlaylistsResponse>, tonic::Status> { 271 + let playlists = self 272 + .store 273 + .list_smart_playlists() 274 + .await 275 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 276 + Ok(tonic::Response::new(GetSmartPlaylistsResponse { 277 + playlists: playlists.into_iter().map(to_proto_smart_playlist).collect(), 278 + })) 279 + } 280 + 281 + async fn get_smart_playlist( 282 + &self, 283 + request: tonic::Request<GetSmartPlaylistRequest>, 284 + ) -> Result<tonic::Response<GetSmartPlaylistResponse>, tonic::Status> { 285 + let id = request.into_inner().id; 286 + let playlist = self 287 + .store 288 + .get_smart_playlist(&id) 289 + .await 290 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 291 + Ok(tonic::Response::new(GetSmartPlaylistResponse { 292 + playlist: playlist.map(to_proto_smart_playlist), 293 + })) 294 + } 295 + 296 + async fn create_smart_playlist( 297 + &self, 298 + request: tonic::Request<CreateSmartPlaylistRequest>, 299 + ) -> Result<tonic::Response<CreateSmartPlaylistResponse>, tonic::Status> { 300 + let req = request.into_inner(); 301 + let rules = req 302 + .rules 303 + .as_ref() 304 + .map(criteria_to_rules) 305 + .unwrap_or_default(); 306 + let playlist = self 307 + .store 308 + .create_smart_playlist( 309 + &req.name, 310 + req.description.as_deref(), 311 + req.image.as_deref(), 312 + req.folder_id.as_deref(), 313 + &rules, 314 + ) 315 + .await 316 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 317 + let ts_p = TsPlaylist { 318 + id: playlist.id.clone(), 319 + name: playlist.name.clone(), 320 + description: playlist.description.clone(), 321 + image: playlist.image.clone(), 322 + is_smart: true, 323 + track_count: 0, 324 + }; 325 + let _ = insert_playlists(vec![ts_p]).await; 326 + Ok(tonic::Response::new(CreateSmartPlaylistResponse { 327 + playlist: Some(to_proto_smart_playlist(playlist)), 328 + })) 329 + } 330 + 331 + async fn update_smart_playlist( 332 + &self, 333 + request: tonic::Request<UpdateSmartPlaylistRequest>, 334 + ) -> Result<tonic::Response<UpdateSmartPlaylistResponse>, tonic::Status> { 335 + let req = request.into_inner(); 336 + let rules = req 337 + .rules 338 + .as_ref() 339 + .map(criteria_to_rules) 340 + .unwrap_or_default(); 341 + self.store 342 + .update_smart_playlist( 343 + &req.id, 344 + &req.name, 345 + req.description.as_deref(), 346 + req.image.as_deref(), 347 + req.folder_id.as_deref(), 348 + &rules, 349 + ) 350 + .await 351 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 352 + if let Ok(Some(updated)) = self.store.get_smart_playlist(&req.id).await { 353 + let ts_p = TsPlaylist { 354 + id: updated.id.clone(), 355 + name: updated.name.clone(), 356 + description: updated.description.clone(), 357 + image: updated.image.clone(), 358 + is_smart: true, 359 + track_count: 0, 360 + }; 361 + let _ = insert_playlists(vec![ts_p]).await; 362 + } 363 + Ok(tonic::Response::new(UpdateSmartPlaylistResponse {})) 364 + } 365 + 366 + async fn delete_smart_playlist( 367 + &self, 368 + request: tonic::Request<DeleteSmartPlaylistRequest>, 369 + ) -> Result<tonic::Response<DeleteSmartPlaylistResponse>, tonic::Status> { 370 + let id = request.into_inner().id; 371 + self.store 372 + .delete_smart_playlist(&id) 373 + .await 374 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 375 + let _ = ts_delete_playlist(&id).await; 376 + Ok(tonic::Response::new(DeleteSmartPlaylistResponse {})) 377 + } 378 + 379 + async fn get_smart_playlist_tracks( 380 + &self, 381 + request: tonic::Request<GetSmartPlaylistTracksRequest>, 382 + ) -> Result<tonic::Response<GetSmartPlaylistTracksResponse>, tonic::Status> { 383 + let id = request.into_inner().id; 384 + 385 + let criteria = match self 386 + .store 387 + .get_smart_playlist(&id) 388 + .await 389 + .map_err(|e| tonic::Status::internal(e.to_string()))? 390 + { 391 + Some(p) => p.rules, 392 + None => { 393 + return Ok(tonic::Response::new(GetSmartPlaylistTracksResponse { 394 + track_ids: vec![], 395 + })) 396 + } 397 + }; 398 + 399 + let all_tracks = repo::track::all(self.pool.clone()) 400 + .await 401 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 402 + 403 + let stats_map: HashMap<String, rockbox_playlists::TrackStats> = self 404 + .store 405 + .get_all_track_stats() 406 + .await 407 + .map_err(|e| tonic::Status::internal(e.to_string()))? 408 + .into_iter() 409 + .map(|s| (s.track_id.clone(), s)) 410 + .collect(); 411 + 412 + let liked_ids: std::collections::HashSet<String> = 413 + repo::favourites::all_tracks(self.pool.clone()) 414 + .await 415 + .map_err(|e| tonic::Status::internal(e.to_string()))? 416 + .into_iter() 417 + .map(|t| t.id) 418 + .collect(); 419 + 420 + let candidates: Vec<Candidate> = all_tracks 421 + .iter() 422 + .map(|t| { 423 + let stats = stats_map.get(&t.id); 424 + Candidate { 425 + id: t.id.clone(), 426 + title: t.title.clone(), 427 + artist: t.artist.clone(), 428 + album: t.album.clone(), 429 + year: t.year.map(|y| y as i64), 430 + genre: t.genre.clone(), 431 + duration_ms: t.length as i64 * 1000, 432 + bitrate: t.bitrate as i64, 433 + date_added_ts: t.created_at.timestamp(), 434 + play_count: stats.map(|s| s.play_count).unwrap_or(0), 435 + skip_count: stats.map(|s| s.skip_count).unwrap_or(0), 436 + last_played: stats.and_then(|s| s.last_played), 437 + last_skipped: stats.and_then(|s| s.last_skipped), 438 + is_liked: liked_ids.contains(&t.id), 439 + } 440 + }) 441 + .collect(); 442 + 443 + let resolved = rockbox_playlists::rules::resolve(&criteria, candidates); 444 + let track_ids = resolved.into_iter().map(|c| c.id).collect(); 445 + 446 + Ok(tonic::Response::new(GetSmartPlaylistTracksResponse { 447 + track_ids, 448 + })) 449 + } 450 + 451 + async fn play_smart_playlist( 452 + &self, 453 + request: tonic::Request<PlaySmartPlaylistRequest>, 454 + ) -> Result<tonic::Response<PlaySmartPlaylistResponse>, tonic::Status> { 455 + let id = request.into_inner().id; 456 + let url = format!("{}/smart-playlists/{}/play", rockbox_url(), id); 457 + self.client 458 + .post(&url) 459 + .send() 460 + .await 461 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 462 + Ok(tonic::Response::new(PlaySmartPlaylistResponse {})) 463 + } 464 + 465 + async fn record_track_played( 466 + &self, 467 + request: tonic::Request<RecordTrackPlayedRequest>, 468 + ) -> Result<tonic::Response<RecordTrackPlayedResponse>, tonic::Status> { 469 + let track_id = request.into_inner().track_id; 470 + self.store 471 + .record_play(&track_id) 472 + .await 473 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 474 + Ok(tonic::Response::new(RecordTrackPlayedResponse {})) 475 + } 476 + 477 + async fn record_track_skipped( 478 + &self, 479 + request: tonic::Request<RecordTrackSkippedRequest>, 480 + ) -> Result<tonic::Response<RecordTrackSkippedResponse>, tonic::Status> { 481 + let track_id = request.into_inner().track_id; 482 + self.store 483 + .record_skip(&track_id) 484 + .await 485 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 486 + Ok(tonic::Response::new(RecordTrackSkippedResponse {})) 487 + } 488 + 489 + async fn get_track_stats( 490 + &self, 491 + request: tonic::Request<GetTrackStatsRequest>, 492 + ) -> Result<tonic::Response<GetTrackStatsResponse>, tonic::Status> { 493 + let track_id = request.into_inner().track_id; 494 + let stats = self 495 + .store 496 + .get_track_stats(&track_id) 497 + .await 498 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 499 + Ok(tonic::Response::new(GetTrackStatsResponse { 500 + stats: stats.map(to_proto_stats), 501 + })) 502 + } 503 + }
+1
crates/server/Cargo.toml
··· 21 21 rockbox-discovery = {path = "../discovery"} 22 22 rockbox-graphql = {path = "../graphql"} 23 23 rockbox-library = {path = "../library"} 24 + rockbox-playlists = {path = "../playlists"} 24 25 rockbox-rocksky = {path = "../rocksky"} 25 26 rockbox-mpd = {path = "../mpd"} 26 27 rockbox-mpris = {path = "../mpris"}
+25
crates/server/src/handlers/mod.rs
··· 5 5 pub mod docs; 6 6 pub mod player; 7 7 pub mod playlists; 8 + pub mod saved_playlists; 8 9 pub mod search; 9 10 pub mod settings; 11 + pub mod smart_playlists; 10 12 pub mod system; 11 13 pub mod tracks; 12 14 ··· 60 62 async_handler!(playlists, insert_tracks); 61 63 async_handler!(playlists, remove_tracks); 62 64 async_handler!(playlists, get_playlist); 65 + async_handler!(saved_playlists, list_saved_playlists); 66 + async_handler!(saved_playlists, get_saved_playlist); 67 + async_handler!(saved_playlists, create_saved_playlist); 68 + async_handler!(saved_playlists, update_saved_playlist); 69 + async_handler!(saved_playlists, delete_saved_playlist); 70 + async_handler!(saved_playlists, get_saved_playlist_tracks); 71 + async_handler!(saved_playlists, get_saved_playlist_track_ids); 72 + async_handler!(saved_playlists, add_tracks_to_saved_playlist); 73 + async_handler!(saved_playlists, remove_track_from_saved_playlist); 74 + async_handler!(saved_playlists, play_saved_playlist); 75 + async_handler!(saved_playlists, list_playlist_folders); 76 + async_handler!(saved_playlists, create_playlist_folder); 77 + async_handler!(saved_playlists, delete_playlist_folder); 63 78 async_handler!(tracks, get_tracks); 64 79 async_handler!(tracks, get_track); 65 80 async_handler!(system, get_rockbox_version); ··· 74 89 async_handler!(devices, disconnect); 75 90 async_handler!(devices, get_devices); 76 91 async_handler!(devices, get_device); 92 + async_handler!(smart_playlists, list_smart_playlists); 93 + async_handler!(smart_playlists, get_smart_playlist); 94 + async_handler!(smart_playlists, create_smart_playlist); 95 + async_handler!(smart_playlists, update_smart_playlist); 96 + async_handler!(smart_playlists, delete_smart_playlist); 97 + async_handler!(smart_playlists, get_smart_playlist_tracks); 98 + async_handler!(smart_playlists, play_smart_playlist); 99 + async_handler!(smart_playlists, record_track_played); 100 + async_handler!(smart_playlists, record_track_skipped); 101 + async_handler!(smart_playlists, get_track_stats);
+298
crates/server/src/handlers/saved_playlists.rs
··· 1 + use crate::http::{Context, Request, Response}; 2 + use crate::PLAYER_MUTEX; 3 + use anyhow::Error; 4 + use rockbox_library::repo; 5 + use rockbox_sys::{self as rb}; 6 + use serde::Deserialize; 7 + 8 + #[derive(Deserialize)] 9 + struct CreatePlaylistBody { 10 + name: String, 11 + description: Option<String>, 12 + image: Option<String>, 13 + folder_id: Option<String>, 14 + track_ids: Option<Vec<String>>, 15 + } 16 + 17 + #[derive(Deserialize)] 18 + struct UpdatePlaylistBody { 19 + name: String, 20 + description: Option<String>, 21 + image: Option<String>, 22 + folder_id: Option<String>, 23 + } 24 + 25 + #[derive(Deserialize)] 26 + struct AddTracksBody { 27 + track_ids: Vec<String>, 28 + } 29 + 30 + #[derive(Deserialize)] 31 + struct CreateFolderBody { 32 + name: String, 33 + } 34 + 35 + pub async fn list_saved_playlists( 36 + ctx: &Context, 37 + req: &Request, 38 + res: &mut Response, 39 + ) -> Result<(), Error> { 40 + let folder_id = req 41 + .query_params 42 + .get("folder_id") 43 + .and_then(|v| v.as_str()) 44 + .map(|s| s.to_string()); 45 + 46 + let playlists = match folder_id.as_deref() { 47 + Some(fid) if !fid.is_empty() => ctx.playlist_store.list_by_folder(fid).await?, 48 + _ => ctx.playlist_store.list().await?, 49 + }; 50 + res.json(&playlists); 51 + Ok(()) 52 + } 53 + 54 + pub async fn get_saved_playlist( 55 + ctx: &Context, 56 + req: &Request, 57 + res: &mut Response, 58 + ) -> Result<(), Error> { 59 + let id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 60 + match ctx.playlist_store.get(id).await? { 61 + Some(p) => res.json(&p), 62 + None => res.set_status(404), 63 + } 64 + Ok(()) 65 + } 66 + 67 + pub async fn create_saved_playlist( 68 + ctx: &Context, 69 + req: &Request, 70 + res: &mut Response, 71 + ) -> Result<(), Error> { 72 + let body = match req.body.as_ref() { 73 + Some(b) => b, 74 + None => { 75 + res.set_status(400); 76 + return Ok(()); 77 + } 78 + }; 79 + let payload: CreatePlaylistBody = serde_json::from_str(body)?; 80 + if payload.name.is_empty() { 81 + res.set_status(400); 82 + return Ok(()); 83 + } 84 + 85 + let playlist = ctx 86 + .playlist_store 87 + .create( 88 + &payload.name, 89 + payload.description.as_deref(), 90 + payload.image.as_deref(), 91 + payload.folder_id.as_deref(), 92 + ) 93 + .await?; 94 + if let Some(ids) = payload.track_ids { 95 + if !ids.is_empty() { 96 + ctx.playlist_store.add_tracks(&playlist.id, &ids).await?; 97 + } 98 + } 99 + let playlist = ctx 100 + .playlist_store 101 + .get(&playlist.id) 102 + .await? 103 + .unwrap_or(playlist); 104 + res.set_status(201); 105 + res.json(&playlist); 106 + Ok(()) 107 + } 108 + 109 + pub async fn update_saved_playlist( 110 + ctx: &Context, 111 + req: &Request, 112 + res: &mut Response, 113 + ) -> Result<(), Error> { 114 + let id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 115 + let body = match req.body.as_ref() { 116 + Some(b) => b, 117 + None => { 118 + res.set_status(400); 119 + return Ok(()); 120 + } 121 + }; 122 + let payload: UpdatePlaylistBody = serde_json::from_str(body)?; 123 + ctx.playlist_store 124 + .update( 125 + id, 126 + &payload.name, 127 + payload.description.as_deref(), 128 + payload.image.as_deref(), 129 + payload.folder_id.as_deref(), 130 + ) 131 + .await?; 132 + res.set_status(204); 133 + Ok(()) 134 + } 135 + 136 + pub async fn delete_saved_playlist( 137 + ctx: &Context, 138 + req: &Request, 139 + res: &mut Response, 140 + ) -> Result<(), Error> { 141 + let id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 142 + ctx.playlist_store.delete(id).await?; 143 + res.set_status(204); 144 + Ok(()) 145 + } 146 + 147 + pub async fn get_saved_playlist_tracks( 148 + ctx: &Context, 149 + req: &Request, 150 + res: &mut Response, 151 + ) -> Result<(), Error> { 152 + let playlist_id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 153 + let track_ids = ctx.playlist_store.get_track_ids(playlist_id).await?; 154 + let mut tracks = Vec::with_capacity(track_ids.len()); 155 + for id in &track_ids { 156 + if let Some(track) = repo::track::find(ctx.pool.clone(), id).await? { 157 + tracks.push(track); 158 + } 159 + } 160 + res.json(&tracks); 161 + Ok(()) 162 + } 163 + 164 + pub async fn add_tracks_to_saved_playlist( 165 + ctx: &Context, 166 + req: &Request, 167 + res: &mut Response, 168 + ) -> Result<(), Error> { 169 + let playlist_id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 170 + let body = match req.body.as_ref() { 171 + Some(b) => b, 172 + None => { 173 + res.set_status(400); 174 + return Ok(()); 175 + } 176 + }; 177 + let payload: AddTracksBody = serde_json::from_str(body)?; 178 + ctx.playlist_store 179 + .add_tracks(playlist_id, &payload.track_ids) 180 + .await?; 181 + res.set_status(204); 182 + Ok(()) 183 + } 184 + 185 + pub async fn get_saved_playlist_track_ids( 186 + ctx: &Context, 187 + req: &Request, 188 + res: &mut Response, 189 + ) -> Result<(), Error> { 190 + let playlist_id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 191 + let track_ids = ctx.playlist_store.get_track_ids(playlist_id).await?; 192 + res.json(&track_ids); 193 + Ok(()) 194 + } 195 + 196 + pub async fn remove_track_from_saved_playlist( 197 + ctx: &Context, 198 + req: &Request, 199 + res: &mut Response, 200 + ) -> Result<(), Error> { 201 + let playlist_id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 202 + let track_id = req.params.get(1).map(|s| s.as_str()).unwrap_or(""); 203 + ctx.playlist_store 204 + .remove_track(playlist_id, track_id) 205 + .await?; 206 + res.set_status(204); 207 + Ok(()) 208 + } 209 + 210 + pub async fn play_saved_playlist( 211 + ctx: &Context, 212 + req: &Request, 213 + res: &mut Response, 214 + ) -> Result<(), Error> { 215 + let playlist_id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 216 + let track_ids = ctx.playlist_store.get_track_ids(playlist_id).await?; 217 + 218 + if track_ids.is_empty() { 219 + res.set_status(422); 220 + return Ok(()); 221 + } 222 + 223 + let mut paths = Vec::with_capacity(track_ids.len()); 224 + for id in &track_ids { 225 + if let Some(track) = repo::track::find(ctx.pool.clone(), id).await? { 226 + paths.push(track.path); 227 + } 228 + } 229 + 230 + if paths.is_empty() { 231 + res.set_status(422); 232 + return Ok(()); 233 + } 234 + 235 + let player_mutex = PLAYER_MUTEX.lock().unwrap(); 236 + let first = &paths[0]; 237 + let dir = { 238 + let parts: Vec<_> = first.split('/').collect(); 239 + parts[..parts.len().saturating_sub(1)].join("/") 240 + }; 241 + rb::playlist::create(&dir, None); 242 + rb::playlist::build_playlist( 243 + paths.iter().map(|p| p.as_str()).collect(), 244 + 0, 245 + paths.len() as i32, 246 + ); 247 + rb::playlist::start(0, 0, 0); 248 + drop(player_mutex); 249 + 250 + res.set_status(204); 251 + Ok(()) 252 + } 253 + 254 + // ── Folders ──────────────────────────────────────────────────────────────── 255 + 256 + pub async fn list_playlist_folders( 257 + ctx: &Context, 258 + _req: &Request, 259 + res: &mut Response, 260 + ) -> Result<(), Error> { 261 + let folders = ctx.playlist_store.list_folders().await?; 262 + res.json(&folders); 263 + Ok(()) 264 + } 265 + 266 + pub async fn create_playlist_folder( 267 + ctx: &Context, 268 + req: &Request, 269 + res: &mut Response, 270 + ) -> Result<(), Error> { 271 + let body = match req.body.as_ref() { 272 + Some(b) => b, 273 + None => { 274 + res.set_status(400); 275 + return Ok(()); 276 + } 277 + }; 278 + let payload: CreateFolderBody = serde_json::from_str(body)?; 279 + if payload.name.is_empty() { 280 + res.set_status(400); 281 + return Ok(()); 282 + } 283 + let folder = ctx.playlist_store.create_folder(&payload.name).await?; 284 + res.set_status(201); 285 + res.json(&folder); 286 + Ok(()) 287 + } 288 + 289 + pub async fn delete_playlist_folder( 290 + ctx: &Context, 291 + req: &Request, 292 + res: &mut Response, 293 + ) -> Result<(), Error> { 294 + let id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 295 + ctx.playlist_store.delete_folder(id).await?; 296 + res.set_status(204); 297 + Ok(()) 298 + }
+278
crates/server/src/handlers/smart_playlists.rs
··· 1 + use crate::http::{Context, Request, Response}; 2 + use crate::PLAYER_MUTEX; 3 + use anyhow::Error; 4 + use rockbox_library::repo; 5 + use rockbox_playlists::rules::{Candidate, RuleCriteria}; 6 + use rockbox_sys::{self as rb}; 7 + use serde::Deserialize; 8 + use std::collections::HashMap; 9 + 10 + #[derive(Deserialize)] 11 + struct CreateSmartPlaylistBody { 12 + name: String, 13 + description: Option<String>, 14 + image: Option<String>, 15 + folder_id: Option<String>, 16 + rules: RuleCriteria, 17 + } 18 + 19 + #[derive(Deserialize)] 20 + struct UpdateSmartPlaylistBody { 21 + name: String, 22 + description: Option<String>, 23 + image: Option<String>, 24 + folder_id: Option<String>, 25 + rules: RuleCriteria, 26 + } 27 + 28 + pub async fn list_smart_playlists( 29 + ctx: &Context, 30 + _req: &Request, 31 + res: &mut Response, 32 + ) -> Result<(), Error> { 33 + let playlists = ctx.playlist_store.list_smart_playlists().await?; 34 + res.json(&playlists); 35 + Ok(()) 36 + } 37 + 38 + pub async fn get_smart_playlist( 39 + ctx: &Context, 40 + req: &Request, 41 + res: &mut Response, 42 + ) -> Result<(), Error> { 43 + let id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 44 + match ctx.playlist_store.get_smart_playlist(id).await? { 45 + Some(p) => res.json(&p), 46 + None => res.set_status(404), 47 + } 48 + Ok(()) 49 + } 50 + 51 + pub async fn create_smart_playlist( 52 + ctx: &Context, 53 + req: &Request, 54 + res: &mut Response, 55 + ) -> Result<(), Error> { 56 + let body = match req.body.as_ref() { 57 + Some(b) => b, 58 + None => { 59 + res.set_status(400); 60 + return Ok(()); 61 + } 62 + }; 63 + let payload: CreateSmartPlaylistBody = serde_json::from_str(body)?; 64 + if payload.name.is_empty() { 65 + res.set_status(400); 66 + return Ok(()); 67 + } 68 + let playlist = ctx 69 + .playlist_store 70 + .create_smart_playlist( 71 + &payload.name, 72 + payload.description.as_deref(), 73 + payload.image.as_deref(), 74 + payload.folder_id.as_deref(), 75 + &payload.rules, 76 + ) 77 + .await?; 78 + res.set_status(201); 79 + res.json(&playlist); 80 + Ok(()) 81 + } 82 + 83 + pub async fn update_smart_playlist( 84 + ctx: &Context, 85 + req: &Request, 86 + res: &mut Response, 87 + ) -> Result<(), Error> { 88 + let id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 89 + let body = match req.body.as_ref() { 90 + Some(b) => b, 91 + None => { 92 + res.set_status(400); 93 + return Ok(()); 94 + } 95 + }; 96 + let payload: UpdateSmartPlaylistBody = serde_json::from_str(body)?; 97 + match ctx 98 + .playlist_store 99 + .update_smart_playlist( 100 + id, 101 + &payload.name, 102 + payload.description.as_deref(), 103 + payload.image.as_deref(), 104 + payload.folder_id.as_deref(), 105 + &payload.rules, 106 + ) 107 + .await 108 + { 109 + Ok(()) => res.set_status(204), 110 + Err(_) => res.set_status(404), 111 + } 112 + Ok(()) 113 + } 114 + 115 + pub async fn delete_smart_playlist( 116 + ctx: &Context, 117 + req: &Request, 118 + res: &mut Response, 119 + ) -> Result<(), Error> { 120 + let id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 121 + let deleted = ctx.playlist_store.delete_smart_playlist(id).await?; 122 + if deleted { 123 + res.set_status(204); 124 + } else { 125 + res.set_status(404); 126 + } 127 + Ok(()) 128 + } 129 + 130 + async fn resolve_smart_playlist_tracks( 131 + ctx: &Context, 132 + id: &str, 133 + ) -> Result<Option<(RuleCriteria, Vec<rockbox_library::entity::track::Track>)>, Error> { 134 + let criteria = match ctx.playlist_store.get_smart_playlist(id).await? { 135 + Some(p) => p.rules, 136 + None => return Ok(None), 137 + }; 138 + 139 + let all_tracks = repo::track::all(ctx.pool.clone()).await?; 140 + 141 + let stats_map: HashMap<String, rockbox_playlists::TrackStats> = ctx 142 + .playlist_store 143 + .get_all_track_stats() 144 + .await? 145 + .into_iter() 146 + .map(|s| (s.track_id.clone(), s)) 147 + .collect(); 148 + 149 + let liked_ids: std::collections::HashSet<String> = 150 + repo::favourites::all_tracks(ctx.pool.clone()) 151 + .await? 152 + .into_iter() 153 + .map(|t| t.id) 154 + .collect(); 155 + 156 + let candidates: Vec<Candidate> = all_tracks 157 + .iter() 158 + .map(|t| { 159 + let stats = stats_map.get(&t.id); 160 + Candidate { 161 + id: t.id.clone(), 162 + title: t.title.clone(), 163 + artist: t.artist.clone(), 164 + album: t.album.clone(), 165 + year: t.year.map(|y| y as i64), 166 + genre: t.genre.clone(), 167 + duration_ms: t.length as i64 * 1000, 168 + bitrate: t.bitrate as i64, 169 + date_added_ts: t.created_at.timestamp(), 170 + play_count: stats.map(|s| s.play_count).unwrap_or(0), 171 + skip_count: stats.map(|s| s.skip_count).unwrap_or(0), 172 + last_played: stats.and_then(|s| s.last_played), 173 + last_skipped: stats.and_then(|s| s.last_skipped), 174 + is_liked: liked_ids.contains(&t.id), 175 + } 176 + }) 177 + .collect(); 178 + 179 + let resolved = rockbox_playlists::rules::resolve(&criteria, candidates); 180 + 181 + let resolved_ids: Vec<&str> = resolved.iter().map(|c| c.id.as_str()).collect(); 182 + let track_map: HashMap<&str, &rockbox_library::entity::track::Track> = 183 + all_tracks.iter().map(|t| (t.id.as_str(), t)).collect(); 184 + 185 + let tracks: Vec<rockbox_library::entity::track::Track> = resolved_ids 186 + .iter() 187 + .filter_map(|id| track_map.get(id).map(|t| (*t).clone())) 188 + .collect(); 189 + 190 + Ok(Some((criteria, tracks))) 191 + } 192 + 193 + pub async fn get_smart_playlist_tracks( 194 + ctx: &Context, 195 + req: &Request, 196 + res: &mut Response, 197 + ) -> Result<(), Error> { 198 + let id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 199 + match resolve_smart_playlist_tracks(ctx, id).await? { 200 + Some((_, tracks)) => res.json(&tracks), 201 + None => res.set_status(404), 202 + } 203 + Ok(()) 204 + } 205 + 206 + pub async fn play_smart_playlist( 207 + ctx: &Context, 208 + req: &Request, 209 + res: &mut Response, 210 + ) -> Result<(), Error> { 211 + let id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 212 + let tracks = match resolve_smart_playlist_tracks(ctx, id).await? { 213 + Some((_, t)) => t, 214 + None => { 215 + res.set_status(404); 216 + return Ok(()); 217 + } 218 + }; 219 + 220 + if tracks.is_empty() { 221 + res.set_status(422); 222 + return Ok(()); 223 + } 224 + 225 + let paths: Vec<String> = tracks.iter().map(|t| t.path.clone()).collect(); 226 + let player_mutex = PLAYER_MUTEX.lock().unwrap(); 227 + let first = &paths[0]; 228 + let dir = { 229 + let parts: Vec<_> = first.split('/').collect(); 230 + parts[..parts.len().saturating_sub(1)].join("/") 231 + }; 232 + rb::playlist::create(&dir, None); 233 + rb::playlist::build_playlist( 234 + paths.iter().map(|p| p.as_str()).collect(), 235 + 0, 236 + paths.len() as i32, 237 + ); 238 + rb::playlist::start(0, 0, 0); 239 + drop(player_mutex); 240 + 241 + res.set_status(204); 242 + Ok(()) 243 + } 244 + 245 + pub async fn record_track_played( 246 + ctx: &Context, 247 + req: &Request, 248 + res: &mut Response, 249 + ) -> Result<(), Error> { 250 + let track_id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 251 + ctx.playlist_store.record_play(track_id).await?; 252 + res.set_status(204); 253 + Ok(()) 254 + } 255 + 256 + pub async fn record_track_skipped( 257 + ctx: &Context, 258 + req: &Request, 259 + res: &mut Response, 260 + ) -> Result<(), Error> { 261 + let track_id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 262 + ctx.playlist_store.record_skip(track_id).await?; 263 + res.set_status(204); 264 + Ok(()) 265 + } 266 + 267 + pub async fn get_track_stats( 268 + ctx: &Context, 269 + req: &Request, 270 + res: &mut Response, 271 + ) -> Result<(), Error> { 272 + let track_id = req.params.first().map(|s| s.as_str()).unwrap_or(""); 273 + match ctx.playlist_store.get_track_stats(track_id).await? { 274 + Some(s) => res.json(&s), 275 + None => res.set_status(404), 276 + } 277 + Ok(()) 278 + }
+10
crates/server/src/http.rs
··· 1 1 use anyhow::Error; 2 2 use rockbox_library::entity::track::Track; 3 + use rockbox_playlists::PlaylistStore; 3 4 use rockbox_sys::{ 4 5 self as rb, 5 6 types::{mp3_entry::Mp3Entry, tree::Entry}, ··· 36 37 pub current_device: Arc<Mutex<Option<Device>>>, 37 38 pub player: Arc<Mutex<Option<Box<dyn Player + Send>>>>, 38 39 pub kv: Arc<Mutex<KV<Track>>>, 40 + pub playlist_store: PlaylistStore, 39 41 } 40 42 41 43 #[derive(Debug)] ··· 254 256 let player = Arc::new(Mutex::new(None)); 255 257 let kv = Arc::new(Mutex::new(rt.block_on(build_tracks_kv(db_pool.clone()))?)); 256 258 259 + let playlist_store = PlaylistStore::new(db_pool.clone()); 260 + rt.block_on(playlist_store.seed()) 261 + .expect("Failed to seed playlist store"); 262 + 257 263 // Start scanning for devices 258 264 scan_chromecast_devices(devices.clone()); 259 265 listen_for_playback_changes(player.clone(), db_pool.clone()); ··· 274 280 let cloned_current_device = current_device.clone(); 275 281 let cloned_player = player.clone(); 276 282 let cloned_kv = kv.clone(); 283 + let cloned_playlist_store = playlist_store.clone(); 277 284 pool.execute(move || { 278 285 let mut buf_reader = BufReader::new(&stream); 279 286 let mut request = String::new(); ··· 343 350 cloned_current_device, 344 351 cloned_player, 345 352 cloned_kv, 353 + cloned_playlist_store, 346 354 ); 347 355 } 348 356 ··· 390 398 current_device: Arc<Mutex<Option<Device>>>, 391 399 player: Arc<Mutex<Option<Box<dyn Player + Send>>>>, 392 400 kv: Arc<Mutex<KV<Track>>>, 401 + playlist_store: PlaylistStore, 393 402 ) { 394 403 debug!("{} {}", method, path); 395 404 match self.router.route(method, path) { ··· 403 412 current_device, 404 413 player, 405 414 kv, 415 + playlist_store, 406 416 }; 407 417 let request = Request { 408 418 method: method.to_string(),
+55
crates/server/src/lib.rs
··· 119 119 app.delete("/playlists/:id/tracks", remove_tracks); 120 120 app.get("/playlists/:id", get_playlist); 121 121 122 + app.get("/saved-playlists/folders", list_playlist_folders); 123 + app.post("/saved-playlists/folders", create_playlist_folder); 124 + app.delete("/saved-playlists/folders/:id", delete_playlist_folder); 125 + app.get("/saved-playlists", list_saved_playlists); 126 + app.post("/saved-playlists", create_saved_playlist); 127 + app.get("/saved-playlists/:id/tracks", get_saved_playlist_tracks); 128 + app.get( 129 + "/saved-playlists/:id/track-ids", 130 + get_saved_playlist_track_ids, 131 + ); 132 + app.post("/saved-playlists/:id/tracks", add_tracks_to_saved_playlist); 133 + app.delete( 134 + "/saved-playlists/:id/tracks/:track_id", 135 + remove_track_from_saved_playlist, 136 + ); 137 + app.post("/saved-playlists/:id/play", play_saved_playlist); 138 + app.get("/saved-playlists/:id", get_saved_playlist); 139 + app.put("/saved-playlists/:id", update_saved_playlist); 140 + app.delete("/saved-playlists/:id", delete_saved_playlist); 141 + 142 + app.get("/smart-playlists", list_smart_playlists); 143 + app.post("/smart-playlists", create_smart_playlist); 144 + app.get("/smart-playlists/:id/tracks", get_smart_playlist_tracks); 145 + app.post("/smart-playlists/:id/play", play_smart_playlist); 146 + app.get("/smart-playlists/:id", get_smart_playlist); 147 + app.put("/smart-playlists/:id", update_smart_playlist); 148 + app.delete("/smart-playlists/:id", delete_smart_playlist); 149 + 150 + app.post("/track-stats/:id/played", record_track_played); 151 + app.post("/track-stats/:id/skipped", record_track_skipped); 152 + app.get("/track-stats/:id", get_track_stats); 153 + 122 154 app.get("/tracks", get_tracks); 123 155 app.get("/tracks/:id", get_track); 124 156 ··· 294 326 let mut current_scrobble_track: Option<Track> = None; // The track we are monitoring for scrobble 295 327 let mut scrobbled_tracks: HashSet<String> = HashSet::new(); // Simple unique ID to prevent duplicates (use track.id if available) 296 328 329 + // Track stats auto-recording: detect play/skip on track change 330 + let playlist_store = rockbox_playlists::PlaylistStore::new(pool.clone()); 331 + let mut last_stats_track_id: Option<String> = None; 332 + let mut last_stats_elapsed: u64 = 0; 333 + let mut last_stats_length: u64 = 0; 334 + 297 335 loop { 298 336 let mutex = GLOBAL_MUTEX.lock().unwrap(); 299 337 if *mutex == 1 { ··· 397 435 }; 398 436 399 437 if track_changed { 438 + // Auto-record play or skip for the previous track (direct DB write, 439 + // no HTTP roundtrip — avoids blocking the broker loop). 440 + if let Some(prev_id) = last_stats_track_id.take() { 441 + if last_stats_length > 10_000 && last_stats_elapsed > 2_000 { 442 + let ratio = last_stats_elapsed as f64 / last_stats_length as f64; 443 + if ratio >= 0.40 { 444 + let _ = rt.block_on(playlist_store.record_play(&prev_id)); 445 + } else { 446 + let _ = rt.block_on(playlist_store.record_skip(&prev_id)); 447 + } 448 + } 449 + } 400 450 current_scrobble_track = Some(track.clone()); 401 451 } 452 + 453 + // Update tracking state for the current track 454 + last_stats_track_id = Some(metadata.id.clone()); 455 + last_stats_elapsed = track.elapsed; 456 + last_stats_length = track.length; 402 457 403 458 // Check progress for scrobbling (only if we have a track to monitor) 404 459 if let Some(ref monitored_track) = current_scrobble_track {
+143
crates/typesense/src/client.rs
··· 314 314 Ok(Some(res.json::<AlbumResult>().await?)) 315 315 } 316 316 317 + pub async fn create_playlists_collection() -> Result<(), Error> { 318 + let client = Client::new(); 319 + let schema = serde_json::json!({ 320 + "name": "playlists", 321 + "fields": [ 322 + {"name": "name", "type": "string", "sort": true}, 323 + {"name": "description", "type": "string", "optional": true}, 324 + {"name": "image", "type": "string", "optional": true}, 325 + {"name": "is_smart", "type": "bool"}, 326 + {"name": "track_count", "type": "int64"}, 327 + ], 328 + "default_sorting_field": "name" 329 + }); 330 + 331 + let typesense_host = format!( 332 + "http://localhost:{}", 333 + std::env::var("RB_TYPESENSE_PORT").unwrap_or_else(|_| "8109".to_string()) 334 + ); 335 + 336 + let api_key = std::env::var("RB_TYPESENSE_API_KEY"); 337 + if api_key.is_err() { 338 + warn!("RB_TYPESENSE_API_KEY is not set."); 339 + return Ok(()); 340 + } 341 + let api_key = api_key.unwrap(); 342 + let res = client 343 + .post(format!("{}/collections", typesense_host)) 344 + .header("X-TYPESENSE-API-KEY", &api_key) 345 + .json(&schema) 346 + .send() 347 + .await?; 348 + 349 + debug!("Create playlists collection response: {}", res.status()); 350 + 351 + Ok(()) 352 + } 353 + 354 + pub async fn insert_playlists(playlists: Vec<Playlist>) -> Result<(), Error> { 355 + let client = Client::new(); 356 + 357 + let jsonl = playlists 358 + .into_iter() 359 + .map(|p| serde_json::to_string(&p).unwrap()) 360 + .collect::<Vec<String>>() 361 + .join("\n"); 362 + 363 + let typesense_host = format!( 364 + "http://localhost:{}", 365 + std::env::var("RB_TYPESENSE_PORT").unwrap_or_else(|_| "8109".to_string()) 366 + ); 367 + 368 + let api_key = std::env::var("RB_TYPESENSE_API_KEY"); 369 + if api_key.is_err() { 370 + warn!("RB_TYPESENSE_API_KEY is not set."); 371 + return Ok(()); 372 + } 373 + let api_key = api_key.unwrap(); 374 + let res = client 375 + .post(format!( 376 + "{}/collections/playlists/documents/import?action=upsert", 377 + typesense_host 378 + )) 379 + .header("X-TYPESENSE-API-KEY", &api_key) 380 + .header("Content-Type", "text/plain") 381 + .body(jsonl) 382 + .send() 383 + .await?; 384 + 385 + info!("Insert playlists response: {}", res.status()); 386 + 387 + Ok(()) 388 + } 389 + 390 + pub async fn delete_playlist(id: &str) -> Result<(), Error> { 391 + let client = Client::new(); 392 + 393 + let typesense_host = format!( 394 + "http://localhost:{}", 395 + std::env::var("RB_TYPESENSE_PORT").unwrap_or_else(|_| "8109".to_string()) 396 + ); 397 + 398 + let api_key = std::env::var("RB_TYPESENSE_API_KEY"); 399 + if api_key.is_err() { 400 + warn!("RB_TYPESENSE_API_KEY is not set."); 401 + return Ok(()); 402 + } 403 + let api_key = api_key.unwrap(); 404 + let res = client 405 + .delete(format!( 406 + "{}/collections/playlists/documents/{}", 407 + typesense_host, id 408 + )) 409 + .header("X-TYPESENSE-API-KEY", &api_key) 410 + .send() 411 + .await?; 412 + 413 + debug!("Delete playlist response: {}", res.status()); 414 + 415 + Ok(()) 416 + } 417 + 418 + pub async fn search_playlists(query: &str) -> Result<Option<PlaylistResult>, Error> { 419 + let client = Client::new(); 420 + 421 + let typesense_host = format!( 422 + "http://localhost:{}", 423 + std::env::var("RB_TYPESENSE_PORT").unwrap_or_else(|_| "8109".to_string()) 424 + ); 425 + 426 + let api_key = std::env::var("RB_TYPESENSE_API_KEY"); 427 + if api_key.is_err() { 428 + warn!("RB_TYPESENSE_API_KEY is not set."); 429 + return Ok(None); 430 + } 431 + let api_key = api_key.unwrap(); 432 + let res = client 433 + .get(format!( 434 + "{}/collections/playlists/documents/search", 435 + typesense_host, 436 + )) 437 + .query(&[ 438 + ("q", query), 439 + ("query_by", "name,description"), 440 + ( 441 + "include_fields", 442 + "id,name,description,image,is_smart,track_count", 443 + ), 444 + ]) 445 + .header("X-TYPESENSE-API-KEY", &api_key) 446 + .send() 447 + .await?; 448 + 449 + let text = res.text().await?; 450 + match serde_json::from_str::<PlaylistResult>(&text) { 451 + Ok(result) => Ok(Some(result)), 452 + Err(e) => { 453 + warn!("Failed to parse Typesense playlists response: {}", e); 454 + warn!("Response body: {}", text); 455 + Err(e.into()) 456 + } 457 + } 458 + } 459 + 317 460 pub async fn search_artists(query: &str) -> Result<Option<ArtistResult>, Error> { 318 461 let client = Client::new(); 319 462
+31
crates/typesense/src/types.rs
··· 169 169 pub search_cutoff: bool, 170 170 pub search_time_ms: i64, 171 171 } 172 + 173 + #[derive(Debug, Default, Serialize, Deserialize)] 174 + pub struct Playlist { 175 + pub id: String, 176 + pub name: String, 177 + pub description: Option<String>, 178 + pub image: Option<String>, 179 + pub is_smart: bool, 180 + pub track_count: i64, 181 + } 182 + 183 + #[derive(Debug, Default, Serialize, Deserialize)] 184 + pub struct PlaylistHit { 185 + pub document: Playlist, 186 + pub highlight: Option<serde_json::Value>, 187 + pub highlights: Vec<serde_json::Value>, 188 + pub text_match: i64, 189 + pub text_match_info: serde_json::Value, 190 + } 191 + 192 + #[derive(Debug, Default, Serialize, Deserialize)] 193 + pub struct PlaylistResult { 194 + pub facet_counts: Vec<serde_json::Value>, 195 + pub found: i64, 196 + pub hits: Vec<PlaylistHit>, 197 + pub out_of: i64, 198 + pub page: i64, 199 + pub request_params: serde_json::Value, 200 + pub search_cutoff: bool, 201 + pub search_time_ms: i64, 202 + }
+5
firmware/target/hosted/sdl/lcd-sdl.c
··· 95 95 void sdl_gui_update(SDL_Surface *surface, int x_start, int y_start, int width, 96 96 int height, int max_x, int max_y, int ui_x, int ui_y) 97 97 { 98 + extern SDL_Window *sdlWindow; 99 + extern SDL_Surface *sim_lcd_surface; 100 + if (!sdlWindow || !sim_lcd_surface) 101 + return; /* headless: no display — skip rendering */ 102 + 98 103 if (x_start + width > max_x) 99 104 width = max_x - x_start; 100 105 if (y_start + height > max_y)
+34 -6
firmware/target/hosted/sdl/window-sdl.c
··· 150 150 151 151 void sdl_window_render(void) 152 152 { 153 + if (!sdlWindow || !sdlRenderer) 154 + return; /* headless: no window available */ 155 + 153 156 if (new_gui_texture_needed) 154 157 { 155 158 new_gui_texture_needed = false; ··· 168 171 bool sdl_window_adjust(void) 169 172 { 170 173 int w, h; 174 + 175 + if (!sdlWindow) 176 + return false; /* headless: no window available */ 171 177 172 178 if (!window_adjustment_needed) 173 179 return false; ··· 225 231 226 232 get_window_dimensions(&width, &height); 227 233 228 - if ((sdlWindow = SDL_CreateWindow(UI_TITLE, SDL_WINDOWPOS_CENTERED, 229 - SDL_WINDOWPOS_CENTERED, width * display_zoom, 230 - height * display_zoom , flags)) == NULL) 231 - panicf("%s", SDL_GetError()); 234 + sdlWindow = SDL_CreateWindow(UI_TITLE, SDL_WINDOWPOS_CENTERED, 235 + SDL_WINDOWPOS_CENTERED, width * display_zoom, 236 + height * display_zoom, flags); 237 + if (sdlWindow == NULL) 238 + { 239 + /* Window creation failed (e.g., running headlessly without a display). 240 + * Continue without a GUI — the daemon still operates normally. */ 241 + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, 242 + "SDL_CreateWindow failed: %s — running headless", SDL_GetError()); 243 + return; 244 + } 245 + 232 246 if ((sdlRenderer = SDL_CreateRenderer(sdlWindow, -1, SDL_RENDERER_PRESENTVSYNC)) == NULL) 233 - panicf("%s", SDL_GetError()); 247 + { 248 + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, 249 + "SDL_CreateRenderer failed: %s — running headless", SDL_GetError()); 250 + SDL_DestroyWindow(sdlWindow); 251 + sdlWindow = NULL; 252 + return; 253 + } 234 254 235 255 /* Surface for LCD content only. Needs to fit largest LCD */ 236 256 if ((sim_lcd_surface = SDL_CreateRGBSurface(0, ··· 243 263 SIM_LCD_WIDTH, SIM_LCD_HEIGHT, 244 264 #endif 245 265 depth, 0, 0, 0, 0)) == NULL) 246 - panicf("%s", SDL_GetError()); 266 + { 267 + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, 268 + "SDL_CreateRGBSurface failed: %s — running headless", SDL_GetError()); 269 + SDL_DestroyRenderer(sdlRenderer); 270 + SDL_DestroyWindow(sdlWindow); 271 + sdlRenderer = NULL; 272 + sdlWindow = NULL; 273 + return; 274 + } 247 275 248 276 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, display_zoom == 1 ? "best" : "nearest"); 249 277 display_zoom = 0; /* reset to 0 unless/until user requests a scale level change */
+1
gpui/assets/icons/circle-plus.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-circle-plus"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4.929 4.929a10 10 0 1 1 14.141 14.141a10 10 0 0 1 -14.14 -14.14m8.071 4.071a1 1 0 1 0 -2 0v2h-2a1 1 0 1 0 0 2h2v2a1 1 0 1 0 2 0v-2h2a1 1 0 1 0 0 -2h-2v-2z" /></svg>
+1
gpui/assets/icons/pencil.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-pencil"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4" /><path d="M13.5 6.5l4 4" /></svg>
+1
gpui/assets/icons/playlist.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-playlist"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M11 17a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M17 17v-13h4" /><path d="M13 5h-10" /><path d="M3 9l10 0" /><path d="M9 13h-6" /></svg>
+1
gpui/assets/icons/trash.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-trash"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 7l16 0" /><path d="M10 11l0 6" /><path d="M14 11l0 6" /><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" /><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" /></svg>
+2
gpui/build.rs
··· 12 12 "proto/rockbox/v1alpha1/settings.proto", 13 13 "proto/rockbox/v1alpha1/sound.proto", 14 14 "proto/rockbox/v1alpha1/system.proto", 15 + "proto/rockbox/v1alpha1/saved_playlist.proto", 16 + "proto/rockbox/v1alpha1/smart_playlist.proto", 15 17 ], 16 18 &["proto"], 17 19 )?;
+2655
gpui/src/api/rockbox.v1alpha1.rs
··· 539 539 pub term: ::prost::alloc::string::String, 540 540 } 541 541 #[derive(Clone, PartialEq, ::prost::Message)] 542 + pub struct SearchPlaylist { 543 + #[prost(string, tag = "1")] 544 + pub id: ::prost::alloc::string::String, 545 + #[prost(string, tag = "2")] 546 + pub name: ::prost::alloc::string::String, 547 + #[prost(string, optional, tag = "3")] 548 + pub description: ::core::option::Option<::prost::alloc::string::String>, 549 + #[prost(string, optional, tag = "4")] 550 + pub image: ::core::option::Option<::prost::alloc::string::String>, 551 + #[prost(bool, tag = "5")] 552 + pub is_smart: bool, 553 + #[prost(int64, tag = "6")] 554 + pub track_count: i64, 555 + } 556 + #[derive(Clone, PartialEq, ::prost::Message)] 542 557 pub struct SearchResponse { 543 558 #[prost(message, repeated, tag = "1")] 544 559 pub tracks: ::prost::alloc::vec::Vec<Track>, ··· 546 561 pub albums: ::prost::alloc::vec::Vec<Album>, 547 562 #[prost(message, repeated, tag = "3")] 548 563 pub artists: ::prost::alloc::vec::Vec<Artist>, 564 + #[prost(message, repeated, tag = "4")] 565 + pub playlists: ::prost::alloc::vec::Vec<SearchPlaylist>, 549 566 } 550 567 /// Generated client implementations. 551 568 pub mod library_service_client { ··· 9337 9354 const NAME: &'static str = SERVICE_NAME; 9338 9355 } 9339 9356 } 9357 + #[derive(Clone, PartialEq, ::prost::Message)] 9358 + pub struct PlaylistFolder { 9359 + #[prost(string, tag = "1")] 9360 + pub id: ::prost::alloc::string::String, 9361 + #[prost(string, tag = "2")] 9362 + pub name: ::prost::alloc::string::String, 9363 + #[prost(int64, tag = "3")] 9364 + pub created_at: i64, 9365 + #[prost(int64, tag = "4")] 9366 + pub updated_at: i64, 9367 + } 9368 + #[derive(Clone, PartialEq, ::prost::Message)] 9369 + pub struct CreatePlaylistFolderRequest { 9370 + #[prost(string, tag = "1")] 9371 + pub name: ::prost::alloc::string::String, 9372 + } 9373 + #[derive(Clone, PartialEq, ::prost::Message)] 9374 + pub struct CreatePlaylistFolderResponse { 9375 + #[prost(message, optional, tag = "1")] 9376 + pub folder: ::core::option::Option<PlaylistFolder>, 9377 + } 9378 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 9379 + pub struct GetPlaylistFoldersRequest {} 9380 + #[derive(Clone, PartialEq, ::prost::Message)] 9381 + pub struct GetPlaylistFoldersResponse { 9382 + #[prost(message, repeated, tag = "1")] 9383 + pub folders: ::prost::alloc::vec::Vec<PlaylistFolder>, 9384 + } 9385 + #[derive(Clone, PartialEq, ::prost::Message)] 9386 + pub struct DeletePlaylistFolderRequest { 9387 + #[prost(string, tag = "1")] 9388 + pub id: ::prost::alloc::string::String, 9389 + } 9390 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 9391 + pub struct DeletePlaylistFolderResponse {} 9392 + #[derive(Clone, PartialEq, ::prost::Message)] 9393 + pub struct SavedPlaylist { 9394 + #[prost(string, tag = "1")] 9395 + pub id: ::prost::alloc::string::String, 9396 + #[prost(string, tag = "2")] 9397 + pub name: ::prost::alloc::string::String, 9398 + #[prost(string, optional, tag = "3")] 9399 + pub description: ::core::option::Option<::prost::alloc::string::String>, 9400 + #[prost(string, optional, tag = "4")] 9401 + pub image: ::core::option::Option<::prost::alloc::string::String>, 9402 + #[prost(string, optional, tag = "5")] 9403 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 9404 + #[prost(int64, tag = "6")] 9405 + pub track_count: i64, 9406 + #[prost(int64, tag = "7")] 9407 + pub created_at: i64, 9408 + #[prost(int64, tag = "8")] 9409 + pub updated_at: i64, 9410 + } 9411 + #[derive(Clone, PartialEq, ::prost::Message)] 9412 + pub struct GetSavedPlaylistsRequest { 9413 + #[prost(string, optional, tag = "1")] 9414 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 9415 + } 9416 + #[derive(Clone, PartialEq, ::prost::Message)] 9417 + pub struct GetSavedPlaylistsResponse { 9418 + #[prost(message, repeated, tag = "1")] 9419 + pub playlists: ::prost::alloc::vec::Vec<SavedPlaylist>, 9420 + } 9421 + #[derive(Clone, PartialEq, ::prost::Message)] 9422 + pub struct GetSavedPlaylistRequest { 9423 + #[prost(string, tag = "1")] 9424 + pub id: ::prost::alloc::string::String, 9425 + } 9426 + #[derive(Clone, PartialEq, ::prost::Message)] 9427 + pub struct GetSavedPlaylistResponse { 9428 + #[prost(message, optional, tag = "1")] 9429 + pub playlist: ::core::option::Option<SavedPlaylist>, 9430 + } 9431 + #[derive(Clone, PartialEq, ::prost::Message)] 9432 + pub struct CreateSavedPlaylistRequest { 9433 + #[prost(string, tag = "1")] 9434 + pub name: ::prost::alloc::string::String, 9435 + #[prost(string, optional, tag = "2")] 9436 + pub description: ::core::option::Option<::prost::alloc::string::String>, 9437 + #[prost(string, optional, tag = "3")] 9438 + pub image: ::core::option::Option<::prost::alloc::string::String>, 9439 + #[prost(string, optional, tag = "4")] 9440 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 9441 + #[prost(string, repeated, tag = "5")] 9442 + pub track_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 9443 + } 9444 + #[derive(Clone, PartialEq, ::prost::Message)] 9445 + pub struct CreateSavedPlaylistResponse { 9446 + #[prost(message, optional, tag = "1")] 9447 + pub playlist: ::core::option::Option<SavedPlaylist>, 9448 + } 9449 + #[derive(Clone, PartialEq, ::prost::Message)] 9450 + pub struct UpdateSavedPlaylistRequest { 9451 + #[prost(string, tag = "1")] 9452 + pub id: ::prost::alloc::string::String, 9453 + #[prost(string, tag = "2")] 9454 + pub name: ::prost::alloc::string::String, 9455 + #[prost(string, optional, tag = "3")] 9456 + pub description: ::core::option::Option<::prost::alloc::string::String>, 9457 + #[prost(string, optional, tag = "4")] 9458 + pub image: ::core::option::Option<::prost::alloc::string::String>, 9459 + #[prost(string, optional, tag = "5")] 9460 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 9461 + } 9462 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 9463 + pub struct UpdateSavedPlaylistResponse {} 9464 + #[derive(Clone, PartialEq, ::prost::Message)] 9465 + pub struct DeleteSavedPlaylistRequest { 9466 + #[prost(string, tag = "1")] 9467 + pub id: ::prost::alloc::string::String, 9468 + } 9469 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 9470 + pub struct DeleteSavedPlaylistResponse {} 9471 + #[derive(Clone, PartialEq, ::prost::Message)] 9472 + pub struct GetSavedPlaylistTracksRequest { 9473 + #[prost(string, tag = "1")] 9474 + pub playlist_id: ::prost::alloc::string::String, 9475 + } 9476 + #[derive(Clone, PartialEq, ::prost::Message)] 9477 + pub struct GetSavedPlaylistTracksResponse { 9478 + #[prost(string, repeated, tag = "1")] 9479 + pub track_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 9480 + } 9481 + #[derive(Clone, PartialEq, ::prost::Message)] 9482 + pub struct AddTracksToSavedPlaylistRequest { 9483 + #[prost(string, tag = "1")] 9484 + pub playlist_id: ::prost::alloc::string::String, 9485 + #[prost(string, repeated, tag = "2")] 9486 + pub track_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 9487 + } 9488 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 9489 + pub struct AddTracksToSavedPlaylistResponse {} 9490 + #[derive(Clone, PartialEq, ::prost::Message)] 9491 + pub struct RemoveTrackFromSavedPlaylistRequest { 9492 + #[prost(string, tag = "1")] 9493 + pub playlist_id: ::prost::alloc::string::String, 9494 + #[prost(string, tag = "2")] 9495 + pub track_id: ::prost::alloc::string::String, 9496 + } 9497 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 9498 + pub struct RemoveTrackFromSavedPlaylistResponse {} 9499 + #[derive(Clone, PartialEq, ::prost::Message)] 9500 + pub struct PlaySavedPlaylistRequest { 9501 + #[prost(string, tag = "1")] 9502 + pub playlist_id: ::prost::alloc::string::String, 9503 + } 9504 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 9505 + pub struct PlaySavedPlaylistResponse {} 9506 + /// Generated client implementations. 9507 + pub mod saved_playlist_service_client { 9508 + #![allow( 9509 + unused_variables, 9510 + dead_code, 9511 + missing_docs, 9512 + clippy::wildcard_imports, 9513 + clippy::let_unit_value, 9514 + )] 9515 + use tonic::codegen::*; 9516 + use tonic::codegen::http::Uri; 9517 + #[derive(Debug, Clone)] 9518 + pub struct SavedPlaylistServiceClient<T> { 9519 + inner: tonic::client::Grpc<T>, 9520 + } 9521 + impl SavedPlaylistServiceClient<tonic::transport::Channel> { 9522 + /// Attempt to create a new client by connecting to a given endpoint. 9523 + pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> 9524 + where 9525 + D: TryInto<tonic::transport::Endpoint>, 9526 + D::Error: Into<StdError>, 9527 + { 9528 + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; 9529 + Ok(Self::new(conn)) 9530 + } 9531 + } 9532 + impl<T> SavedPlaylistServiceClient<T> 9533 + where 9534 + T: tonic::client::GrpcService<tonic::body::BoxBody>, 9535 + T::Error: Into<StdError>, 9536 + T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static, 9537 + <T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send, 9538 + { 9539 + pub fn new(inner: T) -> Self { 9540 + let inner = tonic::client::Grpc::new(inner); 9541 + Self { inner } 9542 + } 9543 + pub fn with_origin(inner: T, origin: Uri) -> Self { 9544 + let inner = tonic::client::Grpc::with_origin(inner, origin); 9545 + Self { inner } 9546 + } 9547 + pub fn with_interceptor<F>( 9548 + inner: T, 9549 + interceptor: F, 9550 + ) -> SavedPlaylistServiceClient<InterceptedService<T, F>> 9551 + where 9552 + F: tonic::service::Interceptor, 9553 + T::ResponseBody: Default, 9554 + T: tonic::codegen::Service< 9555 + http::Request<tonic::body::BoxBody>, 9556 + Response = http::Response< 9557 + <T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody, 9558 + >, 9559 + >, 9560 + <T as tonic::codegen::Service< 9561 + http::Request<tonic::body::BoxBody>, 9562 + >>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, 9563 + { 9564 + SavedPlaylistServiceClient::new(InterceptedService::new(inner, interceptor)) 9565 + } 9566 + /// Compress requests with the given encoding. 9567 + /// 9568 + /// This requires the server to support it otherwise it might respond with an 9569 + /// error. 9570 + #[must_use] 9571 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 9572 + self.inner = self.inner.send_compressed(encoding); 9573 + self 9574 + } 9575 + /// Enable decompressing responses. 9576 + #[must_use] 9577 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 9578 + self.inner = self.inner.accept_compressed(encoding); 9579 + self 9580 + } 9581 + /// Limits the maximum size of a decoded message. 9582 + /// 9583 + /// Default: `4MB` 9584 + #[must_use] 9585 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 9586 + self.inner = self.inner.max_decoding_message_size(limit); 9587 + self 9588 + } 9589 + /// Limits the maximum size of an encoded message. 9590 + /// 9591 + /// Default: `usize::MAX` 9592 + #[must_use] 9593 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 9594 + self.inner = self.inner.max_encoding_message_size(limit); 9595 + self 9596 + } 9597 + pub async fn create_playlist_folder( 9598 + &mut self, 9599 + request: impl tonic::IntoRequest<super::CreatePlaylistFolderRequest>, 9600 + ) -> std::result::Result< 9601 + tonic::Response<super::CreatePlaylistFolderResponse>, 9602 + tonic::Status, 9603 + > { 9604 + self.inner 9605 + .ready() 9606 + .await 9607 + .map_err(|e| { 9608 + tonic::Status::unknown( 9609 + format!("Service was not ready: {}", e.into()), 9610 + ) 9611 + })?; 9612 + let codec = tonic::codec::ProstCodec::default(); 9613 + let path = http::uri::PathAndQuery::from_static( 9614 + "/rockbox.v1alpha1.SavedPlaylistService/CreatePlaylistFolder", 9615 + ); 9616 + let mut req = request.into_request(); 9617 + req.extensions_mut() 9618 + .insert( 9619 + GrpcMethod::new( 9620 + "rockbox.v1alpha1.SavedPlaylistService", 9621 + "CreatePlaylistFolder", 9622 + ), 9623 + ); 9624 + self.inner.unary(req, path, codec).await 9625 + } 9626 + pub async fn get_playlist_folders( 9627 + &mut self, 9628 + request: impl tonic::IntoRequest<super::GetPlaylistFoldersRequest>, 9629 + ) -> std::result::Result< 9630 + tonic::Response<super::GetPlaylistFoldersResponse>, 9631 + tonic::Status, 9632 + > { 9633 + self.inner 9634 + .ready() 9635 + .await 9636 + .map_err(|e| { 9637 + tonic::Status::unknown( 9638 + format!("Service was not ready: {}", e.into()), 9639 + ) 9640 + })?; 9641 + let codec = tonic::codec::ProstCodec::default(); 9642 + let path = http::uri::PathAndQuery::from_static( 9643 + "/rockbox.v1alpha1.SavedPlaylistService/GetPlaylistFolders", 9644 + ); 9645 + let mut req = request.into_request(); 9646 + req.extensions_mut() 9647 + .insert( 9648 + GrpcMethod::new( 9649 + "rockbox.v1alpha1.SavedPlaylistService", 9650 + "GetPlaylistFolders", 9651 + ), 9652 + ); 9653 + self.inner.unary(req, path, codec).await 9654 + } 9655 + pub async fn delete_playlist_folder( 9656 + &mut self, 9657 + request: impl tonic::IntoRequest<super::DeletePlaylistFolderRequest>, 9658 + ) -> std::result::Result< 9659 + tonic::Response<super::DeletePlaylistFolderResponse>, 9660 + tonic::Status, 9661 + > { 9662 + self.inner 9663 + .ready() 9664 + .await 9665 + .map_err(|e| { 9666 + tonic::Status::unknown( 9667 + format!("Service was not ready: {}", e.into()), 9668 + ) 9669 + })?; 9670 + let codec = tonic::codec::ProstCodec::default(); 9671 + let path = http::uri::PathAndQuery::from_static( 9672 + "/rockbox.v1alpha1.SavedPlaylistService/DeletePlaylistFolder", 9673 + ); 9674 + let mut req = request.into_request(); 9675 + req.extensions_mut() 9676 + .insert( 9677 + GrpcMethod::new( 9678 + "rockbox.v1alpha1.SavedPlaylistService", 9679 + "DeletePlaylistFolder", 9680 + ), 9681 + ); 9682 + self.inner.unary(req, path, codec).await 9683 + } 9684 + pub async fn get_saved_playlists( 9685 + &mut self, 9686 + request: impl tonic::IntoRequest<super::GetSavedPlaylistsRequest>, 9687 + ) -> std::result::Result< 9688 + tonic::Response<super::GetSavedPlaylistsResponse>, 9689 + tonic::Status, 9690 + > { 9691 + self.inner 9692 + .ready() 9693 + .await 9694 + .map_err(|e| { 9695 + tonic::Status::unknown( 9696 + format!("Service was not ready: {}", e.into()), 9697 + ) 9698 + })?; 9699 + let codec = tonic::codec::ProstCodec::default(); 9700 + let path = http::uri::PathAndQuery::from_static( 9701 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylists", 9702 + ); 9703 + let mut req = request.into_request(); 9704 + req.extensions_mut() 9705 + .insert( 9706 + GrpcMethod::new( 9707 + "rockbox.v1alpha1.SavedPlaylistService", 9708 + "GetSavedPlaylists", 9709 + ), 9710 + ); 9711 + self.inner.unary(req, path, codec).await 9712 + } 9713 + pub async fn get_saved_playlist( 9714 + &mut self, 9715 + request: impl tonic::IntoRequest<super::GetSavedPlaylistRequest>, 9716 + ) -> std::result::Result< 9717 + tonic::Response<super::GetSavedPlaylistResponse>, 9718 + tonic::Status, 9719 + > { 9720 + self.inner 9721 + .ready() 9722 + .await 9723 + .map_err(|e| { 9724 + tonic::Status::unknown( 9725 + format!("Service was not ready: {}", e.into()), 9726 + ) 9727 + })?; 9728 + let codec = tonic::codec::ProstCodec::default(); 9729 + let path = http::uri::PathAndQuery::from_static( 9730 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylist", 9731 + ); 9732 + let mut req = request.into_request(); 9733 + req.extensions_mut() 9734 + .insert( 9735 + GrpcMethod::new( 9736 + "rockbox.v1alpha1.SavedPlaylistService", 9737 + "GetSavedPlaylist", 9738 + ), 9739 + ); 9740 + self.inner.unary(req, path, codec).await 9741 + } 9742 + pub async fn create_saved_playlist( 9743 + &mut self, 9744 + request: impl tonic::IntoRequest<super::CreateSavedPlaylistRequest>, 9745 + ) -> std::result::Result< 9746 + tonic::Response<super::CreateSavedPlaylistResponse>, 9747 + tonic::Status, 9748 + > { 9749 + self.inner 9750 + .ready() 9751 + .await 9752 + .map_err(|e| { 9753 + tonic::Status::unknown( 9754 + format!("Service was not ready: {}", e.into()), 9755 + ) 9756 + })?; 9757 + let codec = tonic::codec::ProstCodec::default(); 9758 + let path = http::uri::PathAndQuery::from_static( 9759 + "/rockbox.v1alpha1.SavedPlaylistService/CreateSavedPlaylist", 9760 + ); 9761 + let mut req = request.into_request(); 9762 + req.extensions_mut() 9763 + .insert( 9764 + GrpcMethod::new( 9765 + "rockbox.v1alpha1.SavedPlaylistService", 9766 + "CreateSavedPlaylist", 9767 + ), 9768 + ); 9769 + self.inner.unary(req, path, codec).await 9770 + } 9771 + pub async fn update_saved_playlist( 9772 + &mut self, 9773 + request: impl tonic::IntoRequest<super::UpdateSavedPlaylistRequest>, 9774 + ) -> std::result::Result< 9775 + tonic::Response<super::UpdateSavedPlaylistResponse>, 9776 + tonic::Status, 9777 + > { 9778 + self.inner 9779 + .ready() 9780 + .await 9781 + .map_err(|e| { 9782 + tonic::Status::unknown( 9783 + format!("Service was not ready: {}", e.into()), 9784 + ) 9785 + })?; 9786 + let codec = tonic::codec::ProstCodec::default(); 9787 + let path = http::uri::PathAndQuery::from_static( 9788 + "/rockbox.v1alpha1.SavedPlaylistService/UpdateSavedPlaylist", 9789 + ); 9790 + let mut req = request.into_request(); 9791 + req.extensions_mut() 9792 + .insert( 9793 + GrpcMethod::new( 9794 + "rockbox.v1alpha1.SavedPlaylistService", 9795 + "UpdateSavedPlaylist", 9796 + ), 9797 + ); 9798 + self.inner.unary(req, path, codec).await 9799 + } 9800 + pub async fn delete_saved_playlist( 9801 + &mut self, 9802 + request: impl tonic::IntoRequest<super::DeleteSavedPlaylistRequest>, 9803 + ) -> std::result::Result< 9804 + tonic::Response<super::DeleteSavedPlaylistResponse>, 9805 + tonic::Status, 9806 + > { 9807 + self.inner 9808 + .ready() 9809 + .await 9810 + .map_err(|e| { 9811 + tonic::Status::unknown( 9812 + format!("Service was not ready: {}", e.into()), 9813 + ) 9814 + })?; 9815 + let codec = tonic::codec::ProstCodec::default(); 9816 + let path = http::uri::PathAndQuery::from_static( 9817 + "/rockbox.v1alpha1.SavedPlaylistService/DeleteSavedPlaylist", 9818 + ); 9819 + let mut req = request.into_request(); 9820 + req.extensions_mut() 9821 + .insert( 9822 + GrpcMethod::new( 9823 + "rockbox.v1alpha1.SavedPlaylistService", 9824 + "DeleteSavedPlaylist", 9825 + ), 9826 + ); 9827 + self.inner.unary(req, path, codec).await 9828 + } 9829 + pub async fn get_saved_playlist_tracks( 9830 + &mut self, 9831 + request: impl tonic::IntoRequest<super::GetSavedPlaylistTracksRequest>, 9832 + ) -> std::result::Result< 9833 + tonic::Response<super::GetSavedPlaylistTracksResponse>, 9834 + tonic::Status, 9835 + > { 9836 + self.inner 9837 + .ready() 9838 + .await 9839 + .map_err(|e| { 9840 + tonic::Status::unknown( 9841 + format!("Service was not ready: {}", e.into()), 9842 + ) 9843 + })?; 9844 + let codec = tonic::codec::ProstCodec::default(); 9845 + let path = http::uri::PathAndQuery::from_static( 9846 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylistTracks", 9847 + ); 9848 + let mut req = request.into_request(); 9849 + req.extensions_mut() 9850 + .insert( 9851 + GrpcMethod::new( 9852 + "rockbox.v1alpha1.SavedPlaylistService", 9853 + "GetSavedPlaylistTracks", 9854 + ), 9855 + ); 9856 + self.inner.unary(req, path, codec).await 9857 + } 9858 + pub async fn add_tracks_to_saved_playlist( 9859 + &mut self, 9860 + request: impl tonic::IntoRequest<super::AddTracksToSavedPlaylistRequest>, 9861 + ) -> std::result::Result< 9862 + tonic::Response<super::AddTracksToSavedPlaylistResponse>, 9863 + tonic::Status, 9864 + > { 9865 + self.inner 9866 + .ready() 9867 + .await 9868 + .map_err(|e| { 9869 + tonic::Status::unknown( 9870 + format!("Service was not ready: {}", e.into()), 9871 + ) 9872 + })?; 9873 + let codec = tonic::codec::ProstCodec::default(); 9874 + let path = http::uri::PathAndQuery::from_static( 9875 + "/rockbox.v1alpha1.SavedPlaylistService/AddTracksToSavedPlaylist", 9876 + ); 9877 + let mut req = request.into_request(); 9878 + req.extensions_mut() 9879 + .insert( 9880 + GrpcMethod::new( 9881 + "rockbox.v1alpha1.SavedPlaylistService", 9882 + "AddTracksToSavedPlaylist", 9883 + ), 9884 + ); 9885 + self.inner.unary(req, path, codec).await 9886 + } 9887 + pub async fn remove_track_from_saved_playlist( 9888 + &mut self, 9889 + request: impl tonic::IntoRequest<super::RemoveTrackFromSavedPlaylistRequest>, 9890 + ) -> std::result::Result< 9891 + tonic::Response<super::RemoveTrackFromSavedPlaylistResponse>, 9892 + tonic::Status, 9893 + > { 9894 + self.inner 9895 + .ready() 9896 + .await 9897 + .map_err(|e| { 9898 + tonic::Status::unknown( 9899 + format!("Service was not ready: {}", e.into()), 9900 + ) 9901 + })?; 9902 + let codec = tonic::codec::ProstCodec::default(); 9903 + let path = http::uri::PathAndQuery::from_static( 9904 + "/rockbox.v1alpha1.SavedPlaylistService/RemoveTrackFromSavedPlaylist", 9905 + ); 9906 + let mut req = request.into_request(); 9907 + req.extensions_mut() 9908 + .insert( 9909 + GrpcMethod::new( 9910 + "rockbox.v1alpha1.SavedPlaylistService", 9911 + "RemoveTrackFromSavedPlaylist", 9912 + ), 9913 + ); 9914 + self.inner.unary(req, path, codec).await 9915 + } 9916 + pub async fn play_saved_playlist( 9917 + &mut self, 9918 + request: impl tonic::IntoRequest<super::PlaySavedPlaylistRequest>, 9919 + ) -> std::result::Result< 9920 + tonic::Response<super::PlaySavedPlaylistResponse>, 9921 + tonic::Status, 9922 + > { 9923 + self.inner 9924 + .ready() 9925 + .await 9926 + .map_err(|e| { 9927 + tonic::Status::unknown( 9928 + format!("Service was not ready: {}", e.into()), 9929 + ) 9930 + })?; 9931 + let codec = tonic::codec::ProstCodec::default(); 9932 + let path = http::uri::PathAndQuery::from_static( 9933 + "/rockbox.v1alpha1.SavedPlaylistService/PlaySavedPlaylist", 9934 + ); 9935 + let mut req = request.into_request(); 9936 + req.extensions_mut() 9937 + .insert( 9938 + GrpcMethod::new( 9939 + "rockbox.v1alpha1.SavedPlaylistService", 9940 + "PlaySavedPlaylist", 9941 + ), 9942 + ); 9943 + self.inner.unary(req, path, codec).await 9944 + } 9945 + } 9946 + } 9947 + /// Generated server implementations. 9948 + pub mod saved_playlist_service_server { 9949 + #![allow( 9950 + unused_variables, 9951 + dead_code, 9952 + missing_docs, 9953 + clippy::wildcard_imports, 9954 + clippy::let_unit_value, 9955 + )] 9956 + use tonic::codegen::*; 9957 + /// Generated trait containing gRPC methods that should be implemented for use with SavedPlaylistServiceServer. 9958 + #[async_trait] 9959 + pub trait SavedPlaylistService: std::marker::Send + std::marker::Sync + 'static { 9960 + async fn create_playlist_folder( 9961 + &self, 9962 + request: tonic::Request<super::CreatePlaylistFolderRequest>, 9963 + ) -> std::result::Result< 9964 + tonic::Response<super::CreatePlaylistFolderResponse>, 9965 + tonic::Status, 9966 + >; 9967 + async fn get_playlist_folders( 9968 + &self, 9969 + request: tonic::Request<super::GetPlaylistFoldersRequest>, 9970 + ) -> std::result::Result< 9971 + tonic::Response<super::GetPlaylistFoldersResponse>, 9972 + tonic::Status, 9973 + >; 9974 + async fn delete_playlist_folder( 9975 + &self, 9976 + request: tonic::Request<super::DeletePlaylistFolderRequest>, 9977 + ) -> std::result::Result< 9978 + tonic::Response<super::DeletePlaylistFolderResponse>, 9979 + tonic::Status, 9980 + >; 9981 + async fn get_saved_playlists( 9982 + &self, 9983 + request: tonic::Request<super::GetSavedPlaylistsRequest>, 9984 + ) -> std::result::Result< 9985 + tonic::Response<super::GetSavedPlaylistsResponse>, 9986 + tonic::Status, 9987 + >; 9988 + async fn get_saved_playlist( 9989 + &self, 9990 + request: tonic::Request<super::GetSavedPlaylistRequest>, 9991 + ) -> std::result::Result< 9992 + tonic::Response<super::GetSavedPlaylistResponse>, 9993 + tonic::Status, 9994 + >; 9995 + async fn create_saved_playlist( 9996 + &self, 9997 + request: tonic::Request<super::CreateSavedPlaylistRequest>, 9998 + ) -> std::result::Result< 9999 + tonic::Response<super::CreateSavedPlaylistResponse>, 10000 + tonic::Status, 10001 + >; 10002 + async fn update_saved_playlist( 10003 + &self, 10004 + request: tonic::Request<super::UpdateSavedPlaylistRequest>, 10005 + ) -> std::result::Result< 10006 + tonic::Response<super::UpdateSavedPlaylistResponse>, 10007 + tonic::Status, 10008 + >; 10009 + async fn delete_saved_playlist( 10010 + &self, 10011 + request: tonic::Request<super::DeleteSavedPlaylistRequest>, 10012 + ) -> std::result::Result< 10013 + tonic::Response<super::DeleteSavedPlaylistResponse>, 10014 + tonic::Status, 10015 + >; 10016 + async fn get_saved_playlist_tracks( 10017 + &self, 10018 + request: tonic::Request<super::GetSavedPlaylistTracksRequest>, 10019 + ) -> std::result::Result< 10020 + tonic::Response<super::GetSavedPlaylistTracksResponse>, 10021 + tonic::Status, 10022 + >; 10023 + async fn add_tracks_to_saved_playlist( 10024 + &self, 10025 + request: tonic::Request<super::AddTracksToSavedPlaylistRequest>, 10026 + ) -> std::result::Result< 10027 + tonic::Response<super::AddTracksToSavedPlaylistResponse>, 10028 + tonic::Status, 10029 + >; 10030 + async fn remove_track_from_saved_playlist( 10031 + &self, 10032 + request: tonic::Request<super::RemoveTrackFromSavedPlaylistRequest>, 10033 + ) -> std::result::Result< 10034 + tonic::Response<super::RemoveTrackFromSavedPlaylistResponse>, 10035 + tonic::Status, 10036 + >; 10037 + async fn play_saved_playlist( 10038 + &self, 10039 + request: tonic::Request<super::PlaySavedPlaylistRequest>, 10040 + ) -> std::result::Result< 10041 + tonic::Response<super::PlaySavedPlaylistResponse>, 10042 + tonic::Status, 10043 + >; 10044 + } 10045 + #[derive(Debug)] 10046 + pub struct SavedPlaylistServiceServer<T> { 10047 + inner: Arc<T>, 10048 + accept_compression_encodings: EnabledCompressionEncodings, 10049 + send_compression_encodings: EnabledCompressionEncodings, 10050 + max_decoding_message_size: Option<usize>, 10051 + max_encoding_message_size: Option<usize>, 10052 + } 10053 + impl<T> SavedPlaylistServiceServer<T> { 10054 + pub fn new(inner: T) -> Self { 10055 + Self::from_arc(Arc::new(inner)) 10056 + } 10057 + pub fn from_arc(inner: Arc<T>) -> Self { 10058 + Self { 10059 + inner, 10060 + accept_compression_encodings: Default::default(), 10061 + send_compression_encodings: Default::default(), 10062 + max_decoding_message_size: None, 10063 + max_encoding_message_size: None, 10064 + } 10065 + } 10066 + pub fn with_interceptor<F>( 10067 + inner: T, 10068 + interceptor: F, 10069 + ) -> InterceptedService<Self, F> 10070 + where 10071 + F: tonic::service::Interceptor, 10072 + { 10073 + InterceptedService::new(Self::new(inner), interceptor) 10074 + } 10075 + /// Enable decompressing requests with the given encoding. 10076 + #[must_use] 10077 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 10078 + self.accept_compression_encodings.enable(encoding); 10079 + self 10080 + } 10081 + /// Compress responses with the given encoding, if the client supports it. 10082 + #[must_use] 10083 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 10084 + self.send_compression_encodings.enable(encoding); 10085 + self 10086 + } 10087 + /// Limits the maximum size of a decoded message. 10088 + /// 10089 + /// Default: `4MB` 10090 + #[must_use] 10091 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 10092 + self.max_decoding_message_size = Some(limit); 10093 + self 10094 + } 10095 + /// Limits the maximum size of an encoded message. 10096 + /// 10097 + /// Default: `usize::MAX` 10098 + #[must_use] 10099 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 10100 + self.max_encoding_message_size = Some(limit); 10101 + self 10102 + } 10103 + } 10104 + impl<T, B> tonic::codegen::Service<http::Request<B>> 10105 + for SavedPlaylistServiceServer<T> 10106 + where 10107 + T: SavedPlaylistService, 10108 + B: Body + std::marker::Send + 'static, 10109 + B::Error: Into<StdError> + std::marker::Send + 'static, 10110 + { 10111 + type Response = http::Response<tonic::body::BoxBody>; 10112 + type Error = std::convert::Infallible; 10113 + type Future = BoxFuture<Self::Response, Self::Error>; 10114 + fn poll_ready( 10115 + &mut self, 10116 + _cx: &mut Context<'_>, 10117 + ) -> Poll<std::result::Result<(), Self::Error>> { 10118 + Poll::Ready(Ok(())) 10119 + } 10120 + fn call(&mut self, req: http::Request<B>) -> Self::Future { 10121 + match req.uri().path() { 10122 + "/rockbox.v1alpha1.SavedPlaylistService/CreatePlaylistFolder" => { 10123 + #[allow(non_camel_case_types)] 10124 + struct CreatePlaylistFolderSvc<T: SavedPlaylistService>(pub Arc<T>); 10125 + impl< 10126 + T: SavedPlaylistService, 10127 + > tonic::server::UnaryService<super::CreatePlaylistFolderRequest> 10128 + for CreatePlaylistFolderSvc<T> { 10129 + type Response = super::CreatePlaylistFolderResponse; 10130 + type Future = BoxFuture< 10131 + tonic::Response<Self::Response>, 10132 + tonic::Status, 10133 + >; 10134 + fn call( 10135 + &mut self, 10136 + request: tonic::Request<super::CreatePlaylistFolderRequest>, 10137 + ) -> Self::Future { 10138 + let inner = Arc::clone(&self.0); 10139 + let fut = async move { 10140 + <T as SavedPlaylistService>::create_playlist_folder( 10141 + &inner, 10142 + request, 10143 + ) 10144 + .await 10145 + }; 10146 + Box::pin(fut) 10147 + } 10148 + } 10149 + let accept_compression_encodings = self.accept_compression_encodings; 10150 + let send_compression_encodings = self.send_compression_encodings; 10151 + let max_decoding_message_size = self.max_decoding_message_size; 10152 + let max_encoding_message_size = self.max_encoding_message_size; 10153 + let inner = self.inner.clone(); 10154 + let fut = async move { 10155 + let method = CreatePlaylistFolderSvc(inner); 10156 + let codec = tonic::codec::ProstCodec::default(); 10157 + let mut grpc = tonic::server::Grpc::new(codec) 10158 + .apply_compression_config( 10159 + accept_compression_encodings, 10160 + send_compression_encodings, 10161 + ) 10162 + .apply_max_message_size_config( 10163 + max_decoding_message_size, 10164 + max_encoding_message_size, 10165 + ); 10166 + let res = grpc.unary(method, req).await; 10167 + Ok(res) 10168 + }; 10169 + Box::pin(fut) 10170 + } 10171 + "/rockbox.v1alpha1.SavedPlaylistService/GetPlaylistFolders" => { 10172 + #[allow(non_camel_case_types)] 10173 + struct GetPlaylistFoldersSvc<T: SavedPlaylistService>(pub Arc<T>); 10174 + impl< 10175 + T: SavedPlaylistService, 10176 + > tonic::server::UnaryService<super::GetPlaylistFoldersRequest> 10177 + for GetPlaylistFoldersSvc<T> { 10178 + type Response = super::GetPlaylistFoldersResponse; 10179 + type Future = BoxFuture< 10180 + tonic::Response<Self::Response>, 10181 + tonic::Status, 10182 + >; 10183 + fn call( 10184 + &mut self, 10185 + request: tonic::Request<super::GetPlaylistFoldersRequest>, 10186 + ) -> Self::Future { 10187 + let inner = Arc::clone(&self.0); 10188 + let fut = async move { 10189 + <T as SavedPlaylistService>::get_playlist_folders( 10190 + &inner, 10191 + request, 10192 + ) 10193 + .await 10194 + }; 10195 + Box::pin(fut) 10196 + } 10197 + } 10198 + let accept_compression_encodings = self.accept_compression_encodings; 10199 + let send_compression_encodings = self.send_compression_encodings; 10200 + let max_decoding_message_size = self.max_decoding_message_size; 10201 + let max_encoding_message_size = self.max_encoding_message_size; 10202 + let inner = self.inner.clone(); 10203 + let fut = async move { 10204 + let method = GetPlaylistFoldersSvc(inner); 10205 + let codec = tonic::codec::ProstCodec::default(); 10206 + let mut grpc = tonic::server::Grpc::new(codec) 10207 + .apply_compression_config( 10208 + accept_compression_encodings, 10209 + send_compression_encodings, 10210 + ) 10211 + .apply_max_message_size_config( 10212 + max_decoding_message_size, 10213 + max_encoding_message_size, 10214 + ); 10215 + let res = grpc.unary(method, req).await; 10216 + Ok(res) 10217 + }; 10218 + Box::pin(fut) 10219 + } 10220 + "/rockbox.v1alpha1.SavedPlaylistService/DeletePlaylistFolder" => { 10221 + #[allow(non_camel_case_types)] 10222 + struct DeletePlaylistFolderSvc<T: SavedPlaylistService>(pub Arc<T>); 10223 + impl< 10224 + T: SavedPlaylistService, 10225 + > tonic::server::UnaryService<super::DeletePlaylistFolderRequest> 10226 + for DeletePlaylistFolderSvc<T> { 10227 + type Response = super::DeletePlaylistFolderResponse; 10228 + type Future = BoxFuture< 10229 + tonic::Response<Self::Response>, 10230 + tonic::Status, 10231 + >; 10232 + fn call( 10233 + &mut self, 10234 + request: tonic::Request<super::DeletePlaylistFolderRequest>, 10235 + ) -> Self::Future { 10236 + let inner = Arc::clone(&self.0); 10237 + let fut = async move { 10238 + <T as SavedPlaylistService>::delete_playlist_folder( 10239 + &inner, 10240 + request, 10241 + ) 10242 + .await 10243 + }; 10244 + Box::pin(fut) 10245 + } 10246 + } 10247 + let accept_compression_encodings = self.accept_compression_encodings; 10248 + let send_compression_encodings = self.send_compression_encodings; 10249 + let max_decoding_message_size = self.max_decoding_message_size; 10250 + let max_encoding_message_size = self.max_encoding_message_size; 10251 + let inner = self.inner.clone(); 10252 + let fut = async move { 10253 + let method = DeletePlaylistFolderSvc(inner); 10254 + let codec = tonic::codec::ProstCodec::default(); 10255 + let mut grpc = tonic::server::Grpc::new(codec) 10256 + .apply_compression_config( 10257 + accept_compression_encodings, 10258 + send_compression_encodings, 10259 + ) 10260 + .apply_max_message_size_config( 10261 + max_decoding_message_size, 10262 + max_encoding_message_size, 10263 + ); 10264 + let res = grpc.unary(method, req).await; 10265 + Ok(res) 10266 + }; 10267 + Box::pin(fut) 10268 + } 10269 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylists" => { 10270 + #[allow(non_camel_case_types)] 10271 + struct GetSavedPlaylistsSvc<T: SavedPlaylistService>(pub Arc<T>); 10272 + impl< 10273 + T: SavedPlaylistService, 10274 + > tonic::server::UnaryService<super::GetSavedPlaylistsRequest> 10275 + for GetSavedPlaylistsSvc<T> { 10276 + type Response = super::GetSavedPlaylistsResponse; 10277 + type Future = BoxFuture< 10278 + tonic::Response<Self::Response>, 10279 + tonic::Status, 10280 + >; 10281 + fn call( 10282 + &mut self, 10283 + request: tonic::Request<super::GetSavedPlaylistsRequest>, 10284 + ) -> Self::Future { 10285 + let inner = Arc::clone(&self.0); 10286 + let fut = async move { 10287 + <T as SavedPlaylistService>::get_saved_playlists( 10288 + &inner, 10289 + request, 10290 + ) 10291 + .await 10292 + }; 10293 + Box::pin(fut) 10294 + } 10295 + } 10296 + let accept_compression_encodings = self.accept_compression_encodings; 10297 + let send_compression_encodings = self.send_compression_encodings; 10298 + let max_decoding_message_size = self.max_decoding_message_size; 10299 + let max_encoding_message_size = self.max_encoding_message_size; 10300 + let inner = self.inner.clone(); 10301 + let fut = async move { 10302 + let method = GetSavedPlaylistsSvc(inner); 10303 + let codec = tonic::codec::ProstCodec::default(); 10304 + let mut grpc = tonic::server::Grpc::new(codec) 10305 + .apply_compression_config( 10306 + accept_compression_encodings, 10307 + send_compression_encodings, 10308 + ) 10309 + .apply_max_message_size_config( 10310 + max_decoding_message_size, 10311 + max_encoding_message_size, 10312 + ); 10313 + let res = grpc.unary(method, req).await; 10314 + Ok(res) 10315 + }; 10316 + Box::pin(fut) 10317 + } 10318 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylist" => { 10319 + #[allow(non_camel_case_types)] 10320 + struct GetSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 10321 + impl< 10322 + T: SavedPlaylistService, 10323 + > tonic::server::UnaryService<super::GetSavedPlaylistRequest> 10324 + for GetSavedPlaylistSvc<T> { 10325 + type Response = super::GetSavedPlaylistResponse; 10326 + type Future = BoxFuture< 10327 + tonic::Response<Self::Response>, 10328 + tonic::Status, 10329 + >; 10330 + fn call( 10331 + &mut self, 10332 + request: tonic::Request<super::GetSavedPlaylistRequest>, 10333 + ) -> Self::Future { 10334 + let inner = Arc::clone(&self.0); 10335 + let fut = async move { 10336 + <T as SavedPlaylistService>::get_saved_playlist( 10337 + &inner, 10338 + request, 10339 + ) 10340 + .await 10341 + }; 10342 + Box::pin(fut) 10343 + } 10344 + } 10345 + let accept_compression_encodings = self.accept_compression_encodings; 10346 + let send_compression_encodings = self.send_compression_encodings; 10347 + let max_decoding_message_size = self.max_decoding_message_size; 10348 + let max_encoding_message_size = self.max_encoding_message_size; 10349 + let inner = self.inner.clone(); 10350 + let fut = async move { 10351 + let method = GetSavedPlaylistSvc(inner); 10352 + let codec = tonic::codec::ProstCodec::default(); 10353 + let mut grpc = tonic::server::Grpc::new(codec) 10354 + .apply_compression_config( 10355 + accept_compression_encodings, 10356 + send_compression_encodings, 10357 + ) 10358 + .apply_max_message_size_config( 10359 + max_decoding_message_size, 10360 + max_encoding_message_size, 10361 + ); 10362 + let res = grpc.unary(method, req).await; 10363 + Ok(res) 10364 + }; 10365 + Box::pin(fut) 10366 + } 10367 + "/rockbox.v1alpha1.SavedPlaylistService/CreateSavedPlaylist" => { 10368 + #[allow(non_camel_case_types)] 10369 + struct CreateSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 10370 + impl< 10371 + T: SavedPlaylistService, 10372 + > tonic::server::UnaryService<super::CreateSavedPlaylistRequest> 10373 + for CreateSavedPlaylistSvc<T> { 10374 + type Response = super::CreateSavedPlaylistResponse; 10375 + type Future = BoxFuture< 10376 + tonic::Response<Self::Response>, 10377 + tonic::Status, 10378 + >; 10379 + fn call( 10380 + &mut self, 10381 + request: tonic::Request<super::CreateSavedPlaylistRequest>, 10382 + ) -> Self::Future { 10383 + let inner = Arc::clone(&self.0); 10384 + let fut = async move { 10385 + <T as SavedPlaylistService>::create_saved_playlist( 10386 + &inner, 10387 + request, 10388 + ) 10389 + .await 10390 + }; 10391 + Box::pin(fut) 10392 + } 10393 + } 10394 + let accept_compression_encodings = self.accept_compression_encodings; 10395 + let send_compression_encodings = self.send_compression_encodings; 10396 + let max_decoding_message_size = self.max_decoding_message_size; 10397 + let max_encoding_message_size = self.max_encoding_message_size; 10398 + let inner = self.inner.clone(); 10399 + let fut = async move { 10400 + let method = CreateSavedPlaylistSvc(inner); 10401 + let codec = tonic::codec::ProstCodec::default(); 10402 + let mut grpc = tonic::server::Grpc::new(codec) 10403 + .apply_compression_config( 10404 + accept_compression_encodings, 10405 + send_compression_encodings, 10406 + ) 10407 + .apply_max_message_size_config( 10408 + max_decoding_message_size, 10409 + max_encoding_message_size, 10410 + ); 10411 + let res = grpc.unary(method, req).await; 10412 + Ok(res) 10413 + }; 10414 + Box::pin(fut) 10415 + } 10416 + "/rockbox.v1alpha1.SavedPlaylistService/UpdateSavedPlaylist" => { 10417 + #[allow(non_camel_case_types)] 10418 + struct UpdateSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 10419 + impl< 10420 + T: SavedPlaylistService, 10421 + > tonic::server::UnaryService<super::UpdateSavedPlaylistRequest> 10422 + for UpdateSavedPlaylistSvc<T> { 10423 + type Response = super::UpdateSavedPlaylistResponse; 10424 + type Future = BoxFuture< 10425 + tonic::Response<Self::Response>, 10426 + tonic::Status, 10427 + >; 10428 + fn call( 10429 + &mut self, 10430 + request: tonic::Request<super::UpdateSavedPlaylistRequest>, 10431 + ) -> Self::Future { 10432 + let inner = Arc::clone(&self.0); 10433 + let fut = async move { 10434 + <T as SavedPlaylistService>::update_saved_playlist( 10435 + &inner, 10436 + request, 10437 + ) 10438 + .await 10439 + }; 10440 + Box::pin(fut) 10441 + } 10442 + } 10443 + let accept_compression_encodings = self.accept_compression_encodings; 10444 + let send_compression_encodings = self.send_compression_encodings; 10445 + let max_decoding_message_size = self.max_decoding_message_size; 10446 + let max_encoding_message_size = self.max_encoding_message_size; 10447 + let inner = self.inner.clone(); 10448 + let fut = async move { 10449 + let method = UpdateSavedPlaylistSvc(inner); 10450 + let codec = tonic::codec::ProstCodec::default(); 10451 + let mut grpc = tonic::server::Grpc::new(codec) 10452 + .apply_compression_config( 10453 + accept_compression_encodings, 10454 + send_compression_encodings, 10455 + ) 10456 + .apply_max_message_size_config( 10457 + max_decoding_message_size, 10458 + max_encoding_message_size, 10459 + ); 10460 + let res = grpc.unary(method, req).await; 10461 + Ok(res) 10462 + }; 10463 + Box::pin(fut) 10464 + } 10465 + "/rockbox.v1alpha1.SavedPlaylistService/DeleteSavedPlaylist" => { 10466 + #[allow(non_camel_case_types)] 10467 + struct DeleteSavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 10468 + impl< 10469 + T: SavedPlaylistService, 10470 + > tonic::server::UnaryService<super::DeleteSavedPlaylistRequest> 10471 + for DeleteSavedPlaylistSvc<T> { 10472 + type Response = super::DeleteSavedPlaylistResponse; 10473 + type Future = BoxFuture< 10474 + tonic::Response<Self::Response>, 10475 + tonic::Status, 10476 + >; 10477 + fn call( 10478 + &mut self, 10479 + request: tonic::Request<super::DeleteSavedPlaylistRequest>, 10480 + ) -> Self::Future { 10481 + let inner = Arc::clone(&self.0); 10482 + let fut = async move { 10483 + <T as SavedPlaylistService>::delete_saved_playlist( 10484 + &inner, 10485 + request, 10486 + ) 10487 + .await 10488 + }; 10489 + Box::pin(fut) 10490 + } 10491 + } 10492 + let accept_compression_encodings = self.accept_compression_encodings; 10493 + let send_compression_encodings = self.send_compression_encodings; 10494 + let max_decoding_message_size = self.max_decoding_message_size; 10495 + let max_encoding_message_size = self.max_encoding_message_size; 10496 + let inner = self.inner.clone(); 10497 + let fut = async move { 10498 + let method = DeleteSavedPlaylistSvc(inner); 10499 + let codec = tonic::codec::ProstCodec::default(); 10500 + let mut grpc = tonic::server::Grpc::new(codec) 10501 + .apply_compression_config( 10502 + accept_compression_encodings, 10503 + send_compression_encodings, 10504 + ) 10505 + .apply_max_message_size_config( 10506 + max_decoding_message_size, 10507 + max_encoding_message_size, 10508 + ); 10509 + let res = grpc.unary(method, req).await; 10510 + Ok(res) 10511 + }; 10512 + Box::pin(fut) 10513 + } 10514 + "/rockbox.v1alpha1.SavedPlaylistService/GetSavedPlaylistTracks" => { 10515 + #[allow(non_camel_case_types)] 10516 + struct GetSavedPlaylistTracksSvc<T: SavedPlaylistService>( 10517 + pub Arc<T>, 10518 + ); 10519 + impl< 10520 + T: SavedPlaylistService, 10521 + > tonic::server::UnaryService<super::GetSavedPlaylistTracksRequest> 10522 + for GetSavedPlaylistTracksSvc<T> { 10523 + type Response = super::GetSavedPlaylistTracksResponse; 10524 + type Future = BoxFuture< 10525 + tonic::Response<Self::Response>, 10526 + tonic::Status, 10527 + >; 10528 + fn call( 10529 + &mut self, 10530 + request: tonic::Request<super::GetSavedPlaylistTracksRequest>, 10531 + ) -> Self::Future { 10532 + let inner = Arc::clone(&self.0); 10533 + let fut = async move { 10534 + <T as SavedPlaylistService>::get_saved_playlist_tracks( 10535 + &inner, 10536 + request, 10537 + ) 10538 + .await 10539 + }; 10540 + Box::pin(fut) 10541 + } 10542 + } 10543 + let accept_compression_encodings = self.accept_compression_encodings; 10544 + let send_compression_encodings = self.send_compression_encodings; 10545 + let max_decoding_message_size = self.max_decoding_message_size; 10546 + let max_encoding_message_size = self.max_encoding_message_size; 10547 + let inner = self.inner.clone(); 10548 + let fut = async move { 10549 + let method = GetSavedPlaylistTracksSvc(inner); 10550 + let codec = tonic::codec::ProstCodec::default(); 10551 + let mut grpc = tonic::server::Grpc::new(codec) 10552 + .apply_compression_config( 10553 + accept_compression_encodings, 10554 + send_compression_encodings, 10555 + ) 10556 + .apply_max_message_size_config( 10557 + max_decoding_message_size, 10558 + max_encoding_message_size, 10559 + ); 10560 + let res = grpc.unary(method, req).await; 10561 + Ok(res) 10562 + }; 10563 + Box::pin(fut) 10564 + } 10565 + "/rockbox.v1alpha1.SavedPlaylistService/AddTracksToSavedPlaylist" => { 10566 + #[allow(non_camel_case_types)] 10567 + struct AddTracksToSavedPlaylistSvc<T: SavedPlaylistService>( 10568 + pub Arc<T>, 10569 + ); 10570 + impl< 10571 + T: SavedPlaylistService, 10572 + > tonic::server::UnaryService<super::AddTracksToSavedPlaylistRequest> 10573 + for AddTracksToSavedPlaylistSvc<T> { 10574 + type Response = super::AddTracksToSavedPlaylistResponse; 10575 + type Future = BoxFuture< 10576 + tonic::Response<Self::Response>, 10577 + tonic::Status, 10578 + >; 10579 + fn call( 10580 + &mut self, 10581 + request: tonic::Request< 10582 + super::AddTracksToSavedPlaylistRequest, 10583 + >, 10584 + ) -> Self::Future { 10585 + let inner = Arc::clone(&self.0); 10586 + let fut = async move { 10587 + <T as SavedPlaylistService>::add_tracks_to_saved_playlist( 10588 + &inner, 10589 + request, 10590 + ) 10591 + .await 10592 + }; 10593 + Box::pin(fut) 10594 + } 10595 + } 10596 + let accept_compression_encodings = self.accept_compression_encodings; 10597 + let send_compression_encodings = self.send_compression_encodings; 10598 + let max_decoding_message_size = self.max_decoding_message_size; 10599 + let max_encoding_message_size = self.max_encoding_message_size; 10600 + let inner = self.inner.clone(); 10601 + let fut = async move { 10602 + let method = AddTracksToSavedPlaylistSvc(inner); 10603 + let codec = tonic::codec::ProstCodec::default(); 10604 + let mut grpc = tonic::server::Grpc::new(codec) 10605 + .apply_compression_config( 10606 + accept_compression_encodings, 10607 + send_compression_encodings, 10608 + ) 10609 + .apply_max_message_size_config( 10610 + max_decoding_message_size, 10611 + max_encoding_message_size, 10612 + ); 10613 + let res = grpc.unary(method, req).await; 10614 + Ok(res) 10615 + }; 10616 + Box::pin(fut) 10617 + } 10618 + "/rockbox.v1alpha1.SavedPlaylistService/RemoveTrackFromSavedPlaylist" => { 10619 + #[allow(non_camel_case_types)] 10620 + struct RemoveTrackFromSavedPlaylistSvc<T: SavedPlaylistService>( 10621 + pub Arc<T>, 10622 + ); 10623 + impl< 10624 + T: SavedPlaylistService, 10625 + > tonic::server::UnaryService< 10626 + super::RemoveTrackFromSavedPlaylistRequest, 10627 + > for RemoveTrackFromSavedPlaylistSvc<T> { 10628 + type Response = super::RemoveTrackFromSavedPlaylistResponse; 10629 + type Future = BoxFuture< 10630 + tonic::Response<Self::Response>, 10631 + tonic::Status, 10632 + >; 10633 + fn call( 10634 + &mut self, 10635 + request: tonic::Request< 10636 + super::RemoveTrackFromSavedPlaylistRequest, 10637 + >, 10638 + ) -> Self::Future { 10639 + let inner = Arc::clone(&self.0); 10640 + let fut = async move { 10641 + <T as SavedPlaylistService>::remove_track_from_saved_playlist( 10642 + &inner, 10643 + request, 10644 + ) 10645 + .await 10646 + }; 10647 + Box::pin(fut) 10648 + } 10649 + } 10650 + let accept_compression_encodings = self.accept_compression_encodings; 10651 + let send_compression_encodings = self.send_compression_encodings; 10652 + let max_decoding_message_size = self.max_decoding_message_size; 10653 + let max_encoding_message_size = self.max_encoding_message_size; 10654 + let inner = self.inner.clone(); 10655 + let fut = async move { 10656 + let method = RemoveTrackFromSavedPlaylistSvc(inner); 10657 + let codec = tonic::codec::ProstCodec::default(); 10658 + let mut grpc = tonic::server::Grpc::new(codec) 10659 + .apply_compression_config( 10660 + accept_compression_encodings, 10661 + send_compression_encodings, 10662 + ) 10663 + .apply_max_message_size_config( 10664 + max_decoding_message_size, 10665 + max_encoding_message_size, 10666 + ); 10667 + let res = grpc.unary(method, req).await; 10668 + Ok(res) 10669 + }; 10670 + Box::pin(fut) 10671 + } 10672 + "/rockbox.v1alpha1.SavedPlaylistService/PlaySavedPlaylist" => { 10673 + #[allow(non_camel_case_types)] 10674 + struct PlaySavedPlaylistSvc<T: SavedPlaylistService>(pub Arc<T>); 10675 + impl< 10676 + T: SavedPlaylistService, 10677 + > tonic::server::UnaryService<super::PlaySavedPlaylistRequest> 10678 + for PlaySavedPlaylistSvc<T> { 10679 + type Response = super::PlaySavedPlaylistResponse; 10680 + type Future = BoxFuture< 10681 + tonic::Response<Self::Response>, 10682 + tonic::Status, 10683 + >; 10684 + fn call( 10685 + &mut self, 10686 + request: tonic::Request<super::PlaySavedPlaylistRequest>, 10687 + ) -> Self::Future { 10688 + let inner = Arc::clone(&self.0); 10689 + let fut = async move { 10690 + <T as SavedPlaylistService>::play_saved_playlist( 10691 + &inner, 10692 + request, 10693 + ) 10694 + .await 10695 + }; 10696 + Box::pin(fut) 10697 + } 10698 + } 10699 + let accept_compression_encodings = self.accept_compression_encodings; 10700 + let send_compression_encodings = self.send_compression_encodings; 10701 + let max_decoding_message_size = self.max_decoding_message_size; 10702 + let max_encoding_message_size = self.max_encoding_message_size; 10703 + let inner = self.inner.clone(); 10704 + let fut = async move { 10705 + let method = PlaySavedPlaylistSvc(inner); 10706 + let codec = tonic::codec::ProstCodec::default(); 10707 + let mut grpc = tonic::server::Grpc::new(codec) 10708 + .apply_compression_config( 10709 + accept_compression_encodings, 10710 + send_compression_encodings, 10711 + ) 10712 + .apply_max_message_size_config( 10713 + max_decoding_message_size, 10714 + max_encoding_message_size, 10715 + ); 10716 + let res = grpc.unary(method, req).await; 10717 + Ok(res) 10718 + }; 10719 + Box::pin(fut) 10720 + } 10721 + _ => { 10722 + Box::pin(async move { 10723 + let mut response = http::Response::new(empty_body()); 10724 + let headers = response.headers_mut(); 10725 + headers 10726 + .insert( 10727 + tonic::Status::GRPC_STATUS, 10728 + (tonic::Code::Unimplemented as i32).into(), 10729 + ); 10730 + headers 10731 + .insert( 10732 + http::header::CONTENT_TYPE, 10733 + tonic::metadata::GRPC_CONTENT_TYPE, 10734 + ); 10735 + Ok(response) 10736 + }) 10737 + } 10738 + } 10739 + } 10740 + } 10741 + impl<T> Clone for SavedPlaylistServiceServer<T> { 10742 + fn clone(&self) -> Self { 10743 + let inner = self.inner.clone(); 10744 + Self { 10745 + inner, 10746 + accept_compression_encodings: self.accept_compression_encodings, 10747 + send_compression_encodings: self.send_compression_encodings, 10748 + max_decoding_message_size: self.max_decoding_message_size, 10749 + max_encoding_message_size: self.max_encoding_message_size, 10750 + } 10751 + } 10752 + } 10753 + /// Generated gRPC service name 10754 + pub const SERVICE_NAME: &str = "rockbox.v1alpha1.SavedPlaylistService"; 10755 + impl<T> tonic::server::NamedService for SavedPlaylistServiceServer<T> { 10756 + const NAME: &'static str = SERVICE_NAME; 10757 + } 10758 + } 10759 + #[derive(Clone, PartialEq, ::prost::Message)] 10760 + pub struct RuleCondition { 10761 + #[prost(string, tag = "1")] 10762 + pub field: ::prost::alloc::string::String, 10763 + #[prost(string, tag = "2")] 10764 + pub operator: ::prost::alloc::string::String, 10765 + #[prost(string, optional, tag = "3")] 10766 + pub value: ::core::option::Option<::prost::alloc::string::String>, 10767 + #[prost(string, optional, tag = "4")] 10768 + pub value2: ::core::option::Option<::prost::alloc::string::String>, 10769 + #[prost(string, optional, tag = "5")] 10770 + pub unit: ::core::option::Option<::prost::alloc::string::String>, 10771 + } 10772 + #[derive(Clone, PartialEq, ::prost::Message)] 10773 + pub struct RuleCriteria { 10774 + #[prost(string, tag = "1")] 10775 + pub match_type: ::prost::alloc::string::String, 10776 + #[prost(message, repeated, tag = "2")] 10777 + pub conditions: ::prost::alloc::vec::Vec<RuleCondition>, 10778 + #[prost(int32, optional, tag = "3")] 10779 + pub limit: ::core::option::Option<i32>, 10780 + #[prost(string, optional, tag = "4")] 10781 + pub sort_by: ::core::option::Option<::prost::alloc::string::String>, 10782 + #[prost(string, optional, tag = "5")] 10783 + pub sort_order: ::core::option::Option<::prost::alloc::string::String>, 10784 + } 10785 + #[derive(Clone, PartialEq, ::prost::Message)] 10786 + pub struct SmartPlaylist { 10787 + #[prost(string, tag = "1")] 10788 + pub id: ::prost::alloc::string::String, 10789 + #[prost(string, tag = "2")] 10790 + pub name: ::prost::alloc::string::String, 10791 + #[prost(string, optional, tag = "3")] 10792 + pub description: ::core::option::Option<::prost::alloc::string::String>, 10793 + #[prost(string, optional, tag = "4")] 10794 + pub image: ::core::option::Option<::prost::alloc::string::String>, 10795 + #[prost(string, optional, tag = "5")] 10796 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 10797 + #[prost(bool, tag = "6")] 10798 + pub is_system: bool, 10799 + #[prost(message, optional, tag = "7")] 10800 + pub rules: ::core::option::Option<RuleCriteria>, 10801 + #[prost(int64, tag = "8")] 10802 + pub created_at: i64, 10803 + #[prost(int64, tag = "9")] 10804 + pub updated_at: i64, 10805 + } 10806 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 10807 + pub struct GetSmartPlaylistsRequest {} 10808 + #[derive(Clone, PartialEq, ::prost::Message)] 10809 + pub struct GetSmartPlaylistsResponse { 10810 + #[prost(message, repeated, tag = "1")] 10811 + pub playlists: ::prost::alloc::vec::Vec<SmartPlaylist>, 10812 + } 10813 + #[derive(Clone, PartialEq, ::prost::Message)] 10814 + pub struct GetSmartPlaylistRequest { 10815 + #[prost(string, tag = "1")] 10816 + pub id: ::prost::alloc::string::String, 10817 + } 10818 + #[derive(Clone, PartialEq, ::prost::Message)] 10819 + pub struct GetSmartPlaylistResponse { 10820 + #[prost(message, optional, tag = "1")] 10821 + pub playlist: ::core::option::Option<SmartPlaylist>, 10822 + } 10823 + #[derive(Clone, PartialEq, ::prost::Message)] 10824 + pub struct CreateSmartPlaylistRequest { 10825 + #[prost(string, tag = "1")] 10826 + pub name: ::prost::alloc::string::String, 10827 + #[prost(string, optional, tag = "2")] 10828 + pub description: ::core::option::Option<::prost::alloc::string::String>, 10829 + #[prost(string, optional, tag = "3")] 10830 + pub image: ::core::option::Option<::prost::alloc::string::String>, 10831 + #[prost(string, optional, tag = "4")] 10832 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 10833 + #[prost(message, optional, tag = "5")] 10834 + pub rules: ::core::option::Option<RuleCriteria>, 10835 + } 10836 + #[derive(Clone, PartialEq, ::prost::Message)] 10837 + pub struct CreateSmartPlaylistResponse { 10838 + #[prost(message, optional, tag = "1")] 10839 + pub playlist: ::core::option::Option<SmartPlaylist>, 10840 + } 10841 + #[derive(Clone, PartialEq, ::prost::Message)] 10842 + pub struct UpdateSmartPlaylistRequest { 10843 + #[prost(string, tag = "1")] 10844 + pub id: ::prost::alloc::string::String, 10845 + #[prost(string, tag = "2")] 10846 + pub name: ::prost::alloc::string::String, 10847 + #[prost(string, optional, tag = "3")] 10848 + pub description: ::core::option::Option<::prost::alloc::string::String>, 10849 + #[prost(string, optional, tag = "4")] 10850 + pub image: ::core::option::Option<::prost::alloc::string::String>, 10851 + #[prost(string, optional, tag = "5")] 10852 + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, 10853 + #[prost(message, optional, tag = "6")] 10854 + pub rules: ::core::option::Option<RuleCriteria>, 10855 + } 10856 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 10857 + pub struct UpdateSmartPlaylistResponse {} 10858 + #[derive(Clone, PartialEq, ::prost::Message)] 10859 + pub struct DeleteSmartPlaylistRequest { 10860 + #[prost(string, tag = "1")] 10861 + pub id: ::prost::alloc::string::String, 10862 + } 10863 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 10864 + pub struct DeleteSmartPlaylistResponse {} 10865 + #[derive(Clone, PartialEq, ::prost::Message)] 10866 + pub struct GetSmartPlaylistTracksRequest { 10867 + #[prost(string, tag = "1")] 10868 + pub id: ::prost::alloc::string::String, 10869 + } 10870 + #[derive(Clone, PartialEq, ::prost::Message)] 10871 + pub struct GetSmartPlaylistTracksResponse { 10872 + #[prost(string, repeated, tag = "1")] 10873 + pub track_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 10874 + } 10875 + #[derive(Clone, PartialEq, ::prost::Message)] 10876 + pub struct PlaySmartPlaylistRequest { 10877 + #[prost(string, tag = "1")] 10878 + pub id: ::prost::alloc::string::String, 10879 + } 10880 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 10881 + pub struct PlaySmartPlaylistResponse {} 10882 + #[derive(Clone, PartialEq, ::prost::Message)] 10883 + pub struct TrackStats { 10884 + #[prost(string, tag = "1")] 10885 + pub track_id: ::prost::alloc::string::String, 10886 + #[prost(int64, tag = "2")] 10887 + pub play_count: i64, 10888 + #[prost(int64, tag = "3")] 10889 + pub skip_count: i64, 10890 + #[prost(int64, optional, tag = "4")] 10891 + pub last_played: ::core::option::Option<i64>, 10892 + #[prost(int64, optional, tag = "5")] 10893 + pub last_skipped: ::core::option::Option<i64>, 10894 + #[prost(int64, tag = "6")] 10895 + pub updated_at: i64, 10896 + } 10897 + #[derive(Clone, PartialEq, ::prost::Message)] 10898 + pub struct RecordTrackPlayedRequest { 10899 + #[prost(string, tag = "1")] 10900 + pub track_id: ::prost::alloc::string::String, 10901 + } 10902 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 10903 + pub struct RecordTrackPlayedResponse {} 10904 + #[derive(Clone, PartialEq, ::prost::Message)] 10905 + pub struct RecordTrackSkippedRequest { 10906 + #[prost(string, tag = "1")] 10907 + pub track_id: ::prost::alloc::string::String, 10908 + } 10909 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 10910 + pub struct RecordTrackSkippedResponse {} 10911 + #[derive(Clone, PartialEq, ::prost::Message)] 10912 + pub struct GetTrackStatsRequest { 10913 + #[prost(string, tag = "1")] 10914 + pub track_id: ::prost::alloc::string::String, 10915 + } 10916 + #[derive(Clone, PartialEq, ::prost::Message)] 10917 + pub struct GetTrackStatsResponse { 10918 + #[prost(message, optional, tag = "1")] 10919 + pub stats: ::core::option::Option<TrackStats>, 10920 + } 10921 + /// Generated client implementations. 10922 + pub mod smart_playlist_service_client { 10923 + #![allow( 10924 + unused_variables, 10925 + dead_code, 10926 + missing_docs, 10927 + clippy::wildcard_imports, 10928 + clippy::let_unit_value, 10929 + )] 10930 + use tonic::codegen::*; 10931 + use tonic::codegen::http::Uri; 10932 + #[derive(Debug, Clone)] 10933 + pub struct SmartPlaylistServiceClient<T> { 10934 + inner: tonic::client::Grpc<T>, 10935 + } 10936 + impl SmartPlaylistServiceClient<tonic::transport::Channel> { 10937 + /// Attempt to create a new client by connecting to a given endpoint. 10938 + pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> 10939 + where 10940 + D: TryInto<tonic::transport::Endpoint>, 10941 + D::Error: Into<StdError>, 10942 + { 10943 + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; 10944 + Ok(Self::new(conn)) 10945 + } 10946 + } 10947 + impl<T> SmartPlaylistServiceClient<T> 10948 + where 10949 + T: tonic::client::GrpcService<tonic::body::BoxBody>, 10950 + T::Error: Into<StdError>, 10951 + T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static, 10952 + <T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send, 10953 + { 10954 + pub fn new(inner: T) -> Self { 10955 + let inner = tonic::client::Grpc::new(inner); 10956 + Self { inner } 10957 + } 10958 + pub fn with_origin(inner: T, origin: Uri) -> Self { 10959 + let inner = tonic::client::Grpc::with_origin(inner, origin); 10960 + Self { inner } 10961 + } 10962 + pub fn with_interceptor<F>( 10963 + inner: T, 10964 + interceptor: F, 10965 + ) -> SmartPlaylistServiceClient<InterceptedService<T, F>> 10966 + where 10967 + F: tonic::service::Interceptor, 10968 + T::ResponseBody: Default, 10969 + T: tonic::codegen::Service< 10970 + http::Request<tonic::body::BoxBody>, 10971 + Response = http::Response< 10972 + <T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody, 10973 + >, 10974 + >, 10975 + <T as tonic::codegen::Service< 10976 + http::Request<tonic::body::BoxBody>, 10977 + >>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, 10978 + { 10979 + SmartPlaylistServiceClient::new(InterceptedService::new(inner, interceptor)) 10980 + } 10981 + /// Compress requests with the given encoding. 10982 + /// 10983 + /// This requires the server to support it otherwise it might respond with an 10984 + /// error. 10985 + #[must_use] 10986 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 10987 + self.inner = self.inner.send_compressed(encoding); 10988 + self 10989 + } 10990 + /// Enable decompressing responses. 10991 + #[must_use] 10992 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 10993 + self.inner = self.inner.accept_compressed(encoding); 10994 + self 10995 + } 10996 + /// Limits the maximum size of a decoded message. 10997 + /// 10998 + /// Default: `4MB` 10999 + #[must_use] 11000 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 11001 + self.inner = self.inner.max_decoding_message_size(limit); 11002 + self 11003 + } 11004 + /// Limits the maximum size of an encoded message. 11005 + /// 11006 + /// Default: `usize::MAX` 11007 + #[must_use] 11008 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 11009 + self.inner = self.inner.max_encoding_message_size(limit); 11010 + self 11011 + } 11012 + pub async fn get_smart_playlists( 11013 + &mut self, 11014 + request: impl tonic::IntoRequest<super::GetSmartPlaylistsRequest>, 11015 + ) -> std::result::Result< 11016 + tonic::Response<super::GetSmartPlaylistsResponse>, 11017 + tonic::Status, 11018 + > { 11019 + self.inner 11020 + .ready() 11021 + .await 11022 + .map_err(|e| { 11023 + tonic::Status::unknown( 11024 + format!("Service was not ready: {}", e.into()), 11025 + ) 11026 + })?; 11027 + let codec = tonic::codec::ProstCodec::default(); 11028 + let path = http::uri::PathAndQuery::from_static( 11029 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylists", 11030 + ); 11031 + let mut req = request.into_request(); 11032 + req.extensions_mut() 11033 + .insert( 11034 + GrpcMethod::new( 11035 + "rockbox.v1alpha1.SmartPlaylistService", 11036 + "GetSmartPlaylists", 11037 + ), 11038 + ); 11039 + self.inner.unary(req, path, codec).await 11040 + } 11041 + pub async fn get_smart_playlist( 11042 + &mut self, 11043 + request: impl tonic::IntoRequest<super::GetSmartPlaylistRequest>, 11044 + ) -> std::result::Result< 11045 + tonic::Response<super::GetSmartPlaylistResponse>, 11046 + tonic::Status, 11047 + > { 11048 + self.inner 11049 + .ready() 11050 + .await 11051 + .map_err(|e| { 11052 + tonic::Status::unknown( 11053 + format!("Service was not ready: {}", e.into()), 11054 + ) 11055 + })?; 11056 + let codec = tonic::codec::ProstCodec::default(); 11057 + let path = http::uri::PathAndQuery::from_static( 11058 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylist", 11059 + ); 11060 + let mut req = request.into_request(); 11061 + req.extensions_mut() 11062 + .insert( 11063 + GrpcMethod::new( 11064 + "rockbox.v1alpha1.SmartPlaylistService", 11065 + "GetSmartPlaylist", 11066 + ), 11067 + ); 11068 + self.inner.unary(req, path, codec).await 11069 + } 11070 + pub async fn create_smart_playlist( 11071 + &mut self, 11072 + request: impl tonic::IntoRequest<super::CreateSmartPlaylistRequest>, 11073 + ) -> std::result::Result< 11074 + tonic::Response<super::CreateSmartPlaylistResponse>, 11075 + tonic::Status, 11076 + > { 11077 + self.inner 11078 + .ready() 11079 + .await 11080 + .map_err(|e| { 11081 + tonic::Status::unknown( 11082 + format!("Service was not ready: {}", e.into()), 11083 + ) 11084 + })?; 11085 + let codec = tonic::codec::ProstCodec::default(); 11086 + let path = http::uri::PathAndQuery::from_static( 11087 + "/rockbox.v1alpha1.SmartPlaylistService/CreateSmartPlaylist", 11088 + ); 11089 + let mut req = request.into_request(); 11090 + req.extensions_mut() 11091 + .insert( 11092 + GrpcMethod::new( 11093 + "rockbox.v1alpha1.SmartPlaylistService", 11094 + "CreateSmartPlaylist", 11095 + ), 11096 + ); 11097 + self.inner.unary(req, path, codec).await 11098 + } 11099 + pub async fn update_smart_playlist( 11100 + &mut self, 11101 + request: impl tonic::IntoRequest<super::UpdateSmartPlaylistRequest>, 11102 + ) -> std::result::Result< 11103 + tonic::Response<super::UpdateSmartPlaylistResponse>, 11104 + tonic::Status, 11105 + > { 11106 + self.inner 11107 + .ready() 11108 + .await 11109 + .map_err(|e| { 11110 + tonic::Status::unknown( 11111 + format!("Service was not ready: {}", e.into()), 11112 + ) 11113 + })?; 11114 + let codec = tonic::codec::ProstCodec::default(); 11115 + let path = http::uri::PathAndQuery::from_static( 11116 + "/rockbox.v1alpha1.SmartPlaylistService/UpdateSmartPlaylist", 11117 + ); 11118 + let mut req = request.into_request(); 11119 + req.extensions_mut() 11120 + .insert( 11121 + GrpcMethod::new( 11122 + "rockbox.v1alpha1.SmartPlaylistService", 11123 + "UpdateSmartPlaylist", 11124 + ), 11125 + ); 11126 + self.inner.unary(req, path, codec).await 11127 + } 11128 + pub async fn delete_smart_playlist( 11129 + &mut self, 11130 + request: impl tonic::IntoRequest<super::DeleteSmartPlaylistRequest>, 11131 + ) -> std::result::Result< 11132 + tonic::Response<super::DeleteSmartPlaylistResponse>, 11133 + tonic::Status, 11134 + > { 11135 + self.inner 11136 + .ready() 11137 + .await 11138 + .map_err(|e| { 11139 + tonic::Status::unknown( 11140 + format!("Service was not ready: {}", e.into()), 11141 + ) 11142 + })?; 11143 + let codec = tonic::codec::ProstCodec::default(); 11144 + let path = http::uri::PathAndQuery::from_static( 11145 + "/rockbox.v1alpha1.SmartPlaylistService/DeleteSmartPlaylist", 11146 + ); 11147 + let mut req = request.into_request(); 11148 + req.extensions_mut() 11149 + .insert( 11150 + GrpcMethod::new( 11151 + "rockbox.v1alpha1.SmartPlaylistService", 11152 + "DeleteSmartPlaylist", 11153 + ), 11154 + ); 11155 + self.inner.unary(req, path, codec).await 11156 + } 11157 + pub async fn get_smart_playlist_tracks( 11158 + &mut self, 11159 + request: impl tonic::IntoRequest<super::GetSmartPlaylistTracksRequest>, 11160 + ) -> std::result::Result< 11161 + tonic::Response<super::GetSmartPlaylistTracksResponse>, 11162 + tonic::Status, 11163 + > { 11164 + self.inner 11165 + .ready() 11166 + .await 11167 + .map_err(|e| { 11168 + tonic::Status::unknown( 11169 + format!("Service was not ready: {}", e.into()), 11170 + ) 11171 + })?; 11172 + let codec = tonic::codec::ProstCodec::default(); 11173 + let path = http::uri::PathAndQuery::from_static( 11174 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylistTracks", 11175 + ); 11176 + let mut req = request.into_request(); 11177 + req.extensions_mut() 11178 + .insert( 11179 + GrpcMethod::new( 11180 + "rockbox.v1alpha1.SmartPlaylistService", 11181 + "GetSmartPlaylistTracks", 11182 + ), 11183 + ); 11184 + self.inner.unary(req, path, codec).await 11185 + } 11186 + pub async fn play_smart_playlist( 11187 + &mut self, 11188 + request: impl tonic::IntoRequest<super::PlaySmartPlaylistRequest>, 11189 + ) -> std::result::Result< 11190 + tonic::Response<super::PlaySmartPlaylistResponse>, 11191 + tonic::Status, 11192 + > { 11193 + self.inner 11194 + .ready() 11195 + .await 11196 + .map_err(|e| { 11197 + tonic::Status::unknown( 11198 + format!("Service was not ready: {}", e.into()), 11199 + ) 11200 + })?; 11201 + let codec = tonic::codec::ProstCodec::default(); 11202 + let path = http::uri::PathAndQuery::from_static( 11203 + "/rockbox.v1alpha1.SmartPlaylistService/PlaySmartPlaylist", 11204 + ); 11205 + let mut req = request.into_request(); 11206 + req.extensions_mut() 11207 + .insert( 11208 + GrpcMethod::new( 11209 + "rockbox.v1alpha1.SmartPlaylistService", 11210 + "PlaySmartPlaylist", 11211 + ), 11212 + ); 11213 + self.inner.unary(req, path, codec).await 11214 + } 11215 + pub async fn record_track_played( 11216 + &mut self, 11217 + request: impl tonic::IntoRequest<super::RecordTrackPlayedRequest>, 11218 + ) -> std::result::Result< 11219 + tonic::Response<super::RecordTrackPlayedResponse>, 11220 + tonic::Status, 11221 + > { 11222 + self.inner 11223 + .ready() 11224 + .await 11225 + .map_err(|e| { 11226 + tonic::Status::unknown( 11227 + format!("Service was not ready: {}", e.into()), 11228 + ) 11229 + })?; 11230 + let codec = tonic::codec::ProstCodec::default(); 11231 + let path = http::uri::PathAndQuery::from_static( 11232 + "/rockbox.v1alpha1.SmartPlaylistService/RecordTrackPlayed", 11233 + ); 11234 + let mut req = request.into_request(); 11235 + req.extensions_mut() 11236 + .insert( 11237 + GrpcMethod::new( 11238 + "rockbox.v1alpha1.SmartPlaylistService", 11239 + "RecordTrackPlayed", 11240 + ), 11241 + ); 11242 + self.inner.unary(req, path, codec).await 11243 + } 11244 + pub async fn record_track_skipped( 11245 + &mut self, 11246 + request: impl tonic::IntoRequest<super::RecordTrackSkippedRequest>, 11247 + ) -> std::result::Result< 11248 + tonic::Response<super::RecordTrackSkippedResponse>, 11249 + tonic::Status, 11250 + > { 11251 + self.inner 11252 + .ready() 11253 + .await 11254 + .map_err(|e| { 11255 + tonic::Status::unknown( 11256 + format!("Service was not ready: {}", e.into()), 11257 + ) 11258 + })?; 11259 + let codec = tonic::codec::ProstCodec::default(); 11260 + let path = http::uri::PathAndQuery::from_static( 11261 + "/rockbox.v1alpha1.SmartPlaylistService/RecordTrackSkipped", 11262 + ); 11263 + let mut req = request.into_request(); 11264 + req.extensions_mut() 11265 + .insert( 11266 + GrpcMethod::new( 11267 + "rockbox.v1alpha1.SmartPlaylistService", 11268 + "RecordTrackSkipped", 11269 + ), 11270 + ); 11271 + self.inner.unary(req, path, codec).await 11272 + } 11273 + pub async fn get_track_stats( 11274 + &mut self, 11275 + request: impl tonic::IntoRequest<super::GetTrackStatsRequest>, 11276 + ) -> std::result::Result< 11277 + tonic::Response<super::GetTrackStatsResponse>, 11278 + tonic::Status, 11279 + > { 11280 + self.inner 11281 + .ready() 11282 + .await 11283 + .map_err(|e| { 11284 + tonic::Status::unknown( 11285 + format!("Service was not ready: {}", e.into()), 11286 + ) 11287 + })?; 11288 + let codec = tonic::codec::ProstCodec::default(); 11289 + let path = http::uri::PathAndQuery::from_static( 11290 + "/rockbox.v1alpha1.SmartPlaylistService/GetTrackStats", 11291 + ); 11292 + let mut req = request.into_request(); 11293 + req.extensions_mut() 11294 + .insert( 11295 + GrpcMethod::new( 11296 + "rockbox.v1alpha1.SmartPlaylistService", 11297 + "GetTrackStats", 11298 + ), 11299 + ); 11300 + self.inner.unary(req, path, codec).await 11301 + } 11302 + } 11303 + } 11304 + /// Generated server implementations. 11305 + pub mod smart_playlist_service_server { 11306 + #![allow( 11307 + unused_variables, 11308 + dead_code, 11309 + missing_docs, 11310 + clippy::wildcard_imports, 11311 + clippy::let_unit_value, 11312 + )] 11313 + use tonic::codegen::*; 11314 + /// Generated trait containing gRPC methods that should be implemented for use with SmartPlaylistServiceServer. 11315 + #[async_trait] 11316 + pub trait SmartPlaylistService: std::marker::Send + std::marker::Sync + 'static { 11317 + async fn get_smart_playlists( 11318 + &self, 11319 + request: tonic::Request<super::GetSmartPlaylistsRequest>, 11320 + ) -> std::result::Result< 11321 + tonic::Response<super::GetSmartPlaylistsResponse>, 11322 + tonic::Status, 11323 + >; 11324 + async fn get_smart_playlist( 11325 + &self, 11326 + request: tonic::Request<super::GetSmartPlaylistRequest>, 11327 + ) -> std::result::Result< 11328 + tonic::Response<super::GetSmartPlaylistResponse>, 11329 + tonic::Status, 11330 + >; 11331 + async fn create_smart_playlist( 11332 + &self, 11333 + request: tonic::Request<super::CreateSmartPlaylistRequest>, 11334 + ) -> std::result::Result< 11335 + tonic::Response<super::CreateSmartPlaylistResponse>, 11336 + tonic::Status, 11337 + >; 11338 + async fn update_smart_playlist( 11339 + &self, 11340 + request: tonic::Request<super::UpdateSmartPlaylistRequest>, 11341 + ) -> std::result::Result< 11342 + tonic::Response<super::UpdateSmartPlaylistResponse>, 11343 + tonic::Status, 11344 + >; 11345 + async fn delete_smart_playlist( 11346 + &self, 11347 + request: tonic::Request<super::DeleteSmartPlaylistRequest>, 11348 + ) -> std::result::Result< 11349 + tonic::Response<super::DeleteSmartPlaylistResponse>, 11350 + tonic::Status, 11351 + >; 11352 + async fn get_smart_playlist_tracks( 11353 + &self, 11354 + request: tonic::Request<super::GetSmartPlaylistTracksRequest>, 11355 + ) -> std::result::Result< 11356 + tonic::Response<super::GetSmartPlaylistTracksResponse>, 11357 + tonic::Status, 11358 + >; 11359 + async fn play_smart_playlist( 11360 + &self, 11361 + request: tonic::Request<super::PlaySmartPlaylistRequest>, 11362 + ) -> std::result::Result< 11363 + tonic::Response<super::PlaySmartPlaylistResponse>, 11364 + tonic::Status, 11365 + >; 11366 + async fn record_track_played( 11367 + &self, 11368 + request: tonic::Request<super::RecordTrackPlayedRequest>, 11369 + ) -> std::result::Result< 11370 + tonic::Response<super::RecordTrackPlayedResponse>, 11371 + tonic::Status, 11372 + >; 11373 + async fn record_track_skipped( 11374 + &self, 11375 + request: tonic::Request<super::RecordTrackSkippedRequest>, 11376 + ) -> std::result::Result< 11377 + tonic::Response<super::RecordTrackSkippedResponse>, 11378 + tonic::Status, 11379 + >; 11380 + async fn get_track_stats( 11381 + &self, 11382 + request: tonic::Request<super::GetTrackStatsRequest>, 11383 + ) -> std::result::Result< 11384 + tonic::Response<super::GetTrackStatsResponse>, 11385 + tonic::Status, 11386 + >; 11387 + } 11388 + #[derive(Debug)] 11389 + pub struct SmartPlaylistServiceServer<T> { 11390 + inner: Arc<T>, 11391 + accept_compression_encodings: EnabledCompressionEncodings, 11392 + send_compression_encodings: EnabledCompressionEncodings, 11393 + max_decoding_message_size: Option<usize>, 11394 + max_encoding_message_size: Option<usize>, 11395 + } 11396 + impl<T> SmartPlaylistServiceServer<T> { 11397 + pub fn new(inner: T) -> Self { 11398 + Self::from_arc(Arc::new(inner)) 11399 + } 11400 + pub fn from_arc(inner: Arc<T>) -> Self { 11401 + Self { 11402 + inner, 11403 + accept_compression_encodings: Default::default(), 11404 + send_compression_encodings: Default::default(), 11405 + max_decoding_message_size: None, 11406 + max_encoding_message_size: None, 11407 + } 11408 + } 11409 + pub fn with_interceptor<F>( 11410 + inner: T, 11411 + interceptor: F, 11412 + ) -> InterceptedService<Self, F> 11413 + where 11414 + F: tonic::service::Interceptor, 11415 + { 11416 + InterceptedService::new(Self::new(inner), interceptor) 11417 + } 11418 + /// Enable decompressing requests with the given encoding. 11419 + #[must_use] 11420 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 11421 + self.accept_compression_encodings.enable(encoding); 11422 + self 11423 + } 11424 + /// Compress responses with the given encoding, if the client supports it. 11425 + #[must_use] 11426 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 11427 + self.send_compression_encodings.enable(encoding); 11428 + self 11429 + } 11430 + /// Limits the maximum size of a decoded message. 11431 + /// 11432 + /// Default: `4MB` 11433 + #[must_use] 11434 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 11435 + self.max_decoding_message_size = Some(limit); 11436 + self 11437 + } 11438 + /// Limits the maximum size of an encoded message. 11439 + /// 11440 + /// Default: `usize::MAX` 11441 + #[must_use] 11442 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 11443 + self.max_encoding_message_size = Some(limit); 11444 + self 11445 + } 11446 + } 11447 + impl<T, B> tonic::codegen::Service<http::Request<B>> 11448 + for SmartPlaylistServiceServer<T> 11449 + where 11450 + T: SmartPlaylistService, 11451 + B: Body + std::marker::Send + 'static, 11452 + B::Error: Into<StdError> + std::marker::Send + 'static, 11453 + { 11454 + type Response = http::Response<tonic::body::BoxBody>; 11455 + type Error = std::convert::Infallible; 11456 + type Future = BoxFuture<Self::Response, Self::Error>; 11457 + fn poll_ready( 11458 + &mut self, 11459 + _cx: &mut Context<'_>, 11460 + ) -> Poll<std::result::Result<(), Self::Error>> { 11461 + Poll::Ready(Ok(())) 11462 + } 11463 + fn call(&mut self, req: http::Request<B>) -> Self::Future { 11464 + match req.uri().path() { 11465 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylists" => { 11466 + #[allow(non_camel_case_types)] 11467 + struct GetSmartPlaylistsSvc<T: SmartPlaylistService>(pub Arc<T>); 11468 + impl< 11469 + T: SmartPlaylistService, 11470 + > tonic::server::UnaryService<super::GetSmartPlaylistsRequest> 11471 + for GetSmartPlaylistsSvc<T> { 11472 + type Response = super::GetSmartPlaylistsResponse; 11473 + type Future = BoxFuture< 11474 + tonic::Response<Self::Response>, 11475 + tonic::Status, 11476 + >; 11477 + fn call( 11478 + &mut self, 11479 + request: tonic::Request<super::GetSmartPlaylistsRequest>, 11480 + ) -> Self::Future { 11481 + let inner = Arc::clone(&self.0); 11482 + let fut = async move { 11483 + <T as SmartPlaylistService>::get_smart_playlists( 11484 + &inner, 11485 + request, 11486 + ) 11487 + .await 11488 + }; 11489 + Box::pin(fut) 11490 + } 11491 + } 11492 + let accept_compression_encodings = self.accept_compression_encodings; 11493 + let send_compression_encodings = self.send_compression_encodings; 11494 + let max_decoding_message_size = self.max_decoding_message_size; 11495 + let max_encoding_message_size = self.max_encoding_message_size; 11496 + let inner = self.inner.clone(); 11497 + let fut = async move { 11498 + let method = GetSmartPlaylistsSvc(inner); 11499 + let codec = tonic::codec::ProstCodec::default(); 11500 + let mut grpc = tonic::server::Grpc::new(codec) 11501 + .apply_compression_config( 11502 + accept_compression_encodings, 11503 + send_compression_encodings, 11504 + ) 11505 + .apply_max_message_size_config( 11506 + max_decoding_message_size, 11507 + max_encoding_message_size, 11508 + ); 11509 + let res = grpc.unary(method, req).await; 11510 + Ok(res) 11511 + }; 11512 + Box::pin(fut) 11513 + } 11514 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylist" => { 11515 + #[allow(non_camel_case_types)] 11516 + struct GetSmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 11517 + impl< 11518 + T: SmartPlaylistService, 11519 + > tonic::server::UnaryService<super::GetSmartPlaylistRequest> 11520 + for GetSmartPlaylistSvc<T> { 11521 + type Response = super::GetSmartPlaylistResponse; 11522 + type Future = BoxFuture< 11523 + tonic::Response<Self::Response>, 11524 + tonic::Status, 11525 + >; 11526 + fn call( 11527 + &mut self, 11528 + request: tonic::Request<super::GetSmartPlaylistRequest>, 11529 + ) -> Self::Future { 11530 + let inner = Arc::clone(&self.0); 11531 + let fut = async move { 11532 + <T as SmartPlaylistService>::get_smart_playlist( 11533 + &inner, 11534 + request, 11535 + ) 11536 + .await 11537 + }; 11538 + Box::pin(fut) 11539 + } 11540 + } 11541 + let accept_compression_encodings = self.accept_compression_encodings; 11542 + let send_compression_encodings = self.send_compression_encodings; 11543 + let max_decoding_message_size = self.max_decoding_message_size; 11544 + let max_encoding_message_size = self.max_encoding_message_size; 11545 + let inner = self.inner.clone(); 11546 + let fut = async move { 11547 + let method = GetSmartPlaylistSvc(inner); 11548 + let codec = tonic::codec::ProstCodec::default(); 11549 + let mut grpc = tonic::server::Grpc::new(codec) 11550 + .apply_compression_config( 11551 + accept_compression_encodings, 11552 + send_compression_encodings, 11553 + ) 11554 + .apply_max_message_size_config( 11555 + max_decoding_message_size, 11556 + max_encoding_message_size, 11557 + ); 11558 + let res = grpc.unary(method, req).await; 11559 + Ok(res) 11560 + }; 11561 + Box::pin(fut) 11562 + } 11563 + "/rockbox.v1alpha1.SmartPlaylistService/CreateSmartPlaylist" => { 11564 + #[allow(non_camel_case_types)] 11565 + struct CreateSmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 11566 + impl< 11567 + T: SmartPlaylistService, 11568 + > tonic::server::UnaryService<super::CreateSmartPlaylistRequest> 11569 + for CreateSmartPlaylistSvc<T> { 11570 + type Response = super::CreateSmartPlaylistResponse; 11571 + type Future = BoxFuture< 11572 + tonic::Response<Self::Response>, 11573 + tonic::Status, 11574 + >; 11575 + fn call( 11576 + &mut self, 11577 + request: tonic::Request<super::CreateSmartPlaylistRequest>, 11578 + ) -> Self::Future { 11579 + let inner = Arc::clone(&self.0); 11580 + let fut = async move { 11581 + <T as SmartPlaylistService>::create_smart_playlist( 11582 + &inner, 11583 + request, 11584 + ) 11585 + .await 11586 + }; 11587 + Box::pin(fut) 11588 + } 11589 + } 11590 + let accept_compression_encodings = self.accept_compression_encodings; 11591 + let send_compression_encodings = self.send_compression_encodings; 11592 + let max_decoding_message_size = self.max_decoding_message_size; 11593 + let max_encoding_message_size = self.max_encoding_message_size; 11594 + let inner = self.inner.clone(); 11595 + let fut = async move { 11596 + let method = CreateSmartPlaylistSvc(inner); 11597 + let codec = tonic::codec::ProstCodec::default(); 11598 + let mut grpc = tonic::server::Grpc::new(codec) 11599 + .apply_compression_config( 11600 + accept_compression_encodings, 11601 + send_compression_encodings, 11602 + ) 11603 + .apply_max_message_size_config( 11604 + max_decoding_message_size, 11605 + max_encoding_message_size, 11606 + ); 11607 + let res = grpc.unary(method, req).await; 11608 + Ok(res) 11609 + }; 11610 + Box::pin(fut) 11611 + } 11612 + "/rockbox.v1alpha1.SmartPlaylistService/UpdateSmartPlaylist" => { 11613 + #[allow(non_camel_case_types)] 11614 + struct UpdateSmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 11615 + impl< 11616 + T: SmartPlaylistService, 11617 + > tonic::server::UnaryService<super::UpdateSmartPlaylistRequest> 11618 + for UpdateSmartPlaylistSvc<T> { 11619 + type Response = super::UpdateSmartPlaylistResponse; 11620 + type Future = BoxFuture< 11621 + tonic::Response<Self::Response>, 11622 + tonic::Status, 11623 + >; 11624 + fn call( 11625 + &mut self, 11626 + request: tonic::Request<super::UpdateSmartPlaylistRequest>, 11627 + ) -> Self::Future { 11628 + let inner = Arc::clone(&self.0); 11629 + let fut = async move { 11630 + <T as SmartPlaylistService>::update_smart_playlist( 11631 + &inner, 11632 + request, 11633 + ) 11634 + .await 11635 + }; 11636 + Box::pin(fut) 11637 + } 11638 + } 11639 + let accept_compression_encodings = self.accept_compression_encodings; 11640 + let send_compression_encodings = self.send_compression_encodings; 11641 + let max_decoding_message_size = self.max_decoding_message_size; 11642 + let max_encoding_message_size = self.max_encoding_message_size; 11643 + let inner = self.inner.clone(); 11644 + let fut = async move { 11645 + let method = UpdateSmartPlaylistSvc(inner); 11646 + let codec = tonic::codec::ProstCodec::default(); 11647 + let mut grpc = tonic::server::Grpc::new(codec) 11648 + .apply_compression_config( 11649 + accept_compression_encodings, 11650 + send_compression_encodings, 11651 + ) 11652 + .apply_max_message_size_config( 11653 + max_decoding_message_size, 11654 + max_encoding_message_size, 11655 + ); 11656 + let res = grpc.unary(method, req).await; 11657 + Ok(res) 11658 + }; 11659 + Box::pin(fut) 11660 + } 11661 + "/rockbox.v1alpha1.SmartPlaylistService/DeleteSmartPlaylist" => { 11662 + #[allow(non_camel_case_types)] 11663 + struct DeleteSmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 11664 + impl< 11665 + T: SmartPlaylistService, 11666 + > tonic::server::UnaryService<super::DeleteSmartPlaylistRequest> 11667 + for DeleteSmartPlaylistSvc<T> { 11668 + type Response = super::DeleteSmartPlaylistResponse; 11669 + type Future = BoxFuture< 11670 + tonic::Response<Self::Response>, 11671 + tonic::Status, 11672 + >; 11673 + fn call( 11674 + &mut self, 11675 + request: tonic::Request<super::DeleteSmartPlaylistRequest>, 11676 + ) -> Self::Future { 11677 + let inner = Arc::clone(&self.0); 11678 + let fut = async move { 11679 + <T as SmartPlaylistService>::delete_smart_playlist( 11680 + &inner, 11681 + request, 11682 + ) 11683 + .await 11684 + }; 11685 + Box::pin(fut) 11686 + } 11687 + } 11688 + let accept_compression_encodings = self.accept_compression_encodings; 11689 + let send_compression_encodings = self.send_compression_encodings; 11690 + let max_decoding_message_size = self.max_decoding_message_size; 11691 + let max_encoding_message_size = self.max_encoding_message_size; 11692 + let inner = self.inner.clone(); 11693 + let fut = async move { 11694 + let method = DeleteSmartPlaylistSvc(inner); 11695 + let codec = tonic::codec::ProstCodec::default(); 11696 + let mut grpc = tonic::server::Grpc::new(codec) 11697 + .apply_compression_config( 11698 + accept_compression_encodings, 11699 + send_compression_encodings, 11700 + ) 11701 + .apply_max_message_size_config( 11702 + max_decoding_message_size, 11703 + max_encoding_message_size, 11704 + ); 11705 + let res = grpc.unary(method, req).await; 11706 + Ok(res) 11707 + }; 11708 + Box::pin(fut) 11709 + } 11710 + "/rockbox.v1alpha1.SmartPlaylistService/GetSmartPlaylistTracks" => { 11711 + #[allow(non_camel_case_types)] 11712 + struct GetSmartPlaylistTracksSvc<T: SmartPlaylistService>( 11713 + pub Arc<T>, 11714 + ); 11715 + impl< 11716 + T: SmartPlaylistService, 11717 + > tonic::server::UnaryService<super::GetSmartPlaylistTracksRequest> 11718 + for GetSmartPlaylistTracksSvc<T> { 11719 + type Response = super::GetSmartPlaylistTracksResponse; 11720 + type Future = BoxFuture< 11721 + tonic::Response<Self::Response>, 11722 + tonic::Status, 11723 + >; 11724 + fn call( 11725 + &mut self, 11726 + request: tonic::Request<super::GetSmartPlaylistTracksRequest>, 11727 + ) -> Self::Future { 11728 + let inner = Arc::clone(&self.0); 11729 + let fut = async move { 11730 + <T as SmartPlaylistService>::get_smart_playlist_tracks( 11731 + &inner, 11732 + request, 11733 + ) 11734 + .await 11735 + }; 11736 + Box::pin(fut) 11737 + } 11738 + } 11739 + let accept_compression_encodings = self.accept_compression_encodings; 11740 + let send_compression_encodings = self.send_compression_encodings; 11741 + let max_decoding_message_size = self.max_decoding_message_size; 11742 + let max_encoding_message_size = self.max_encoding_message_size; 11743 + let inner = self.inner.clone(); 11744 + let fut = async move { 11745 + let method = GetSmartPlaylistTracksSvc(inner); 11746 + let codec = tonic::codec::ProstCodec::default(); 11747 + let mut grpc = tonic::server::Grpc::new(codec) 11748 + .apply_compression_config( 11749 + accept_compression_encodings, 11750 + send_compression_encodings, 11751 + ) 11752 + .apply_max_message_size_config( 11753 + max_decoding_message_size, 11754 + max_encoding_message_size, 11755 + ); 11756 + let res = grpc.unary(method, req).await; 11757 + Ok(res) 11758 + }; 11759 + Box::pin(fut) 11760 + } 11761 + "/rockbox.v1alpha1.SmartPlaylistService/PlaySmartPlaylist" => { 11762 + #[allow(non_camel_case_types)] 11763 + struct PlaySmartPlaylistSvc<T: SmartPlaylistService>(pub Arc<T>); 11764 + impl< 11765 + T: SmartPlaylistService, 11766 + > tonic::server::UnaryService<super::PlaySmartPlaylistRequest> 11767 + for PlaySmartPlaylistSvc<T> { 11768 + type Response = super::PlaySmartPlaylistResponse; 11769 + type Future = BoxFuture< 11770 + tonic::Response<Self::Response>, 11771 + tonic::Status, 11772 + >; 11773 + fn call( 11774 + &mut self, 11775 + request: tonic::Request<super::PlaySmartPlaylistRequest>, 11776 + ) -> Self::Future { 11777 + let inner = Arc::clone(&self.0); 11778 + let fut = async move { 11779 + <T as SmartPlaylistService>::play_smart_playlist( 11780 + &inner, 11781 + request, 11782 + ) 11783 + .await 11784 + }; 11785 + Box::pin(fut) 11786 + } 11787 + } 11788 + let accept_compression_encodings = self.accept_compression_encodings; 11789 + let send_compression_encodings = self.send_compression_encodings; 11790 + let max_decoding_message_size = self.max_decoding_message_size; 11791 + let max_encoding_message_size = self.max_encoding_message_size; 11792 + let inner = self.inner.clone(); 11793 + let fut = async move { 11794 + let method = PlaySmartPlaylistSvc(inner); 11795 + let codec = tonic::codec::ProstCodec::default(); 11796 + let mut grpc = tonic::server::Grpc::new(codec) 11797 + .apply_compression_config( 11798 + accept_compression_encodings, 11799 + send_compression_encodings, 11800 + ) 11801 + .apply_max_message_size_config( 11802 + max_decoding_message_size, 11803 + max_encoding_message_size, 11804 + ); 11805 + let res = grpc.unary(method, req).await; 11806 + Ok(res) 11807 + }; 11808 + Box::pin(fut) 11809 + } 11810 + "/rockbox.v1alpha1.SmartPlaylistService/RecordTrackPlayed" => { 11811 + #[allow(non_camel_case_types)] 11812 + struct RecordTrackPlayedSvc<T: SmartPlaylistService>(pub Arc<T>); 11813 + impl< 11814 + T: SmartPlaylistService, 11815 + > tonic::server::UnaryService<super::RecordTrackPlayedRequest> 11816 + for RecordTrackPlayedSvc<T> { 11817 + type Response = super::RecordTrackPlayedResponse; 11818 + type Future = BoxFuture< 11819 + tonic::Response<Self::Response>, 11820 + tonic::Status, 11821 + >; 11822 + fn call( 11823 + &mut self, 11824 + request: tonic::Request<super::RecordTrackPlayedRequest>, 11825 + ) -> Self::Future { 11826 + let inner = Arc::clone(&self.0); 11827 + let fut = async move { 11828 + <T as SmartPlaylistService>::record_track_played( 11829 + &inner, 11830 + request, 11831 + ) 11832 + .await 11833 + }; 11834 + Box::pin(fut) 11835 + } 11836 + } 11837 + let accept_compression_encodings = self.accept_compression_encodings; 11838 + let send_compression_encodings = self.send_compression_encodings; 11839 + let max_decoding_message_size = self.max_decoding_message_size; 11840 + let max_encoding_message_size = self.max_encoding_message_size; 11841 + let inner = self.inner.clone(); 11842 + let fut = async move { 11843 + let method = RecordTrackPlayedSvc(inner); 11844 + let codec = tonic::codec::ProstCodec::default(); 11845 + let mut grpc = tonic::server::Grpc::new(codec) 11846 + .apply_compression_config( 11847 + accept_compression_encodings, 11848 + send_compression_encodings, 11849 + ) 11850 + .apply_max_message_size_config( 11851 + max_decoding_message_size, 11852 + max_encoding_message_size, 11853 + ); 11854 + let res = grpc.unary(method, req).await; 11855 + Ok(res) 11856 + }; 11857 + Box::pin(fut) 11858 + } 11859 + "/rockbox.v1alpha1.SmartPlaylistService/RecordTrackSkipped" => { 11860 + #[allow(non_camel_case_types)] 11861 + struct RecordTrackSkippedSvc<T: SmartPlaylistService>(pub Arc<T>); 11862 + impl< 11863 + T: SmartPlaylistService, 11864 + > tonic::server::UnaryService<super::RecordTrackSkippedRequest> 11865 + for RecordTrackSkippedSvc<T> { 11866 + type Response = super::RecordTrackSkippedResponse; 11867 + type Future = BoxFuture< 11868 + tonic::Response<Self::Response>, 11869 + tonic::Status, 11870 + >; 11871 + fn call( 11872 + &mut self, 11873 + request: tonic::Request<super::RecordTrackSkippedRequest>, 11874 + ) -> Self::Future { 11875 + let inner = Arc::clone(&self.0); 11876 + let fut = async move { 11877 + <T as SmartPlaylistService>::record_track_skipped( 11878 + &inner, 11879 + request, 11880 + ) 11881 + .await 11882 + }; 11883 + Box::pin(fut) 11884 + } 11885 + } 11886 + let accept_compression_encodings = self.accept_compression_encodings; 11887 + let send_compression_encodings = self.send_compression_encodings; 11888 + let max_decoding_message_size = self.max_decoding_message_size; 11889 + let max_encoding_message_size = self.max_encoding_message_size; 11890 + let inner = self.inner.clone(); 11891 + let fut = async move { 11892 + let method = RecordTrackSkippedSvc(inner); 11893 + let codec = tonic::codec::ProstCodec::default(); 11894 + let mut grpc = tonic::server::Grpc::new(codec) 11895 + .apply_compression_config( 11896 + accept_compression_encodings, 11897 + send_compression_encodings, 11898 + ) 11899 + .apply_max_message_size_config( 11900 + max_decoding_message_size, 11901 + max_encoding_message_size, 11902 + ); 11903 + let res = grpc.unary(method, req).await; 11904 + Ok(res) 11905 + }; 11906 + Box::pin(fut) 11907 + } 11908 + "/rockbox.v1alpha1.SmartPlaylistService/GetTrackStats" => { 11909 + #[allow(non_camel_case_types)] 11910 + struct GetTrackStatsSvc<T: SmartPlaylistService>(pub Arc<T>); 11911 + impl< 11912 + T: SmartPlaylistService, 11913 + > tonic::server::UnaryService<super::GetTrackStatsRequest> 11914 + for GetTrackStatsSvc<T> { 11915 + type Response = super::GetTrackStatsResponse; 11916 + type Future = BoxFuture< 11917 + tonic::Response<Self::Response>, 11918 + tonic::Status, 11919 + >; 11920 + fn call( 11921 + &mut self, 11922 + request: tonic::Request<super::GetTrackStatsRequest>, 11923 + ) -> Self::Future { 11924 + let inner = Arc::clone(&self.0); 11925 + let fut = async move { 11926 + <T as SmartPlaylistService>::get_track_stats( 11927 + &inner, 11928 + request, 11929 + ) 11930 + .await 11931 + }; 11932 + Box::pin(fut) 11933 + } 11934 + } 11935 + let accept_compression_encodings = self.accept_compression_encodings; 11936 + let send_compression_encodings = self.send_compression_encodings; 11937 + let max_decoding_message_size = self.max_decoding_message_size; 11938 + let max_encoding_message_size = self.max_encoding_message_size; 11939 + let inner = self.inner.clone(); 11940 + let fut = async move { 11941 + let method = GetTrackStatsSvc(inner); 11942 + let codec = tonic::codec::ProstCodec::default(); 11943 + let mut grpc = tonic::server::Grpc::new(codec) 11944 + .apply_compression_config( 11945 + accept_compression_encodings, 11946 + send_compression_encodings, 11947 + ) 11948 + .apply_max_message_size_config( 11949 + max_decoding_message_size, 11950 + max_encoding_message_size, 11951 + ); 11952 + let res = grpc.unary(method, req).await; 11953 + Ok(res) 11954 + }; 11955 + Box::pin(fut) 11956 + } 11957 + _ => { 11958 + Box::pin(async move { 11959 + let mut response = http::Response::new(empty_body()); 11960 + let headers = response.headers_mut(); 11961 + headers 11962 + .insert( 11963 + tonic::Status::GRPC_STATUS, 11964 + (tonic::Code::Unimplemented as i32).into(), 11965 + ); 11966 + headers 11967 + .insert( 11968 + http::header::CONTENT_TYPE, 11969 + tonic::metadata::GRPC_CONTENT_TYPE, 11970 + ); 11971 + Ok(response) 11972 + }) 11973 + } 11974 + } 11975 + } 11976 + } 11977 + impl<T> Clone for SmartPlaylistServiceServer<T> { 11978 + fn clone(&self) -> Self { 11979 + let inner = self.inner.clone(); 11980 + Self { 11981 + inner, 11982 + accept_compression_encodings: self.accept_compression_encodings, 11983 + send_compression_encodings: self.send_compression_encodings, 11984 + max_decoding_message_size: self.max_decoding_message_size, 11985 + max_encoding_message_size: self.max_encoding_message_size, 11986 + } 11987 + } 11988 + } 11989 + /// Generated gRPC service name 11990 + pub const SERVICE_NAME: &str = "rockbox.v1alpha1.SmartPlaylistService"; 11991 + impl<T> tonic::server::NamedService for SmartPlaylistServiceServer<T> { 11992 + const NAME: &'static str = SERVICE_NAME; 11993 + } 11994 + }
gpui/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+3 -1
gpui/src/app.rs
··· 14 14 .enable_all() 15 15 .build() 16 16 .expect("tokio runtime for http"); 17 - let http_client = crate::http_client::ReqwestHttpClient::new(http_rt.handle().clone()); 17 + let tokio_handle = http_rt.handle().clone(); 18 + let http_client = crate::http_client::ReqwestHttpClient::new(tokio_handle.clone()); 18 19 std::mem::forget(http_rt); 19 20 20 21 Application::new() 21 22 .with_http_client(http_client) 22 23 .with_assets(assets.clone()) 23 24 .run(move |cx| { 25 + cx.set_global(crate::state::TokioHandle(tokio_handle)); 24 26 let bounds = Bounds::centered(None, size(px(1280.0), px(760.0)), cx); 25 27 assets.load_fonts(cx).expect("failed to load fonts"); 26 28 // Theme is set as a global inside StartupGate / Rockbox::new.
+228 -2
gpui/src/client.rs
··· 4 4 settings_service_client::SettingsServiceClient, sound_service_client::SoundServiceClient, 5 5 system_service_client::SystemServiceClient, AdjustVolumeRequest, FastForwardRewindRequest, 6 6 GetArtistsRequest, GetCurrentRequest, GetGlobalSettingsRequest, GetGlobalStatusRequest, 7 - GetLikedTracksRequest, GetTracksRequest, InsertDirectoryRequest, InsertTracksRequest, 7 + GetAlbumRequest, GetLikedTracksRequest, GetTracksRequest, InsertDirectoryRequest, 8 + InsertTracksRequest, 8 9 LikeTrackRequest, NextRequest, PauseRequest, PlayAlbumRequest, PlayAllTracksRequest, 9 10 PlayArtistTracksRequest, PlayDirectoryRequest, PlayTrackRequest, PlaylistResumeRequest, 10 11 PreviousRequest, RemoveTracksRequest, ResumeRequest, ResumeTrackRequest, SaveSettingsRequest, ··· 12 13 StreamLibraryRequest, StreamPlaylistRequest, StreamStatusRequest, TreeGetEntriesRequest, 13 14 UnlikeTrackRequest, 14 15 }; 15 - use crate::state::{SearchAlbum, SearchArtist, SearchResults}; 16 + use crate::state::{SearchAlbum, SearchArtist, SearchPlaylist, SearchResults}; 16 17 17 18 // Matches apps/playlist.h PLAYLIST_INSERT_* constants 18 19 pub const INSERT_FIRST: i32 = -4; // play next (after current) ··· 38 39 .collect()) 39 40 } 40 41 42 + pub async fn get_album( 43 + id: &str, 44 + ) -> Result<(String, Option<String>)> { 45 + let mut c = LibraryServiceClient::connect(URL).await?; 46 + let resp = c.get_album(GetAlbumRequest { id: id.to_string() }).await?; 47 + let album = resp.into_inner().album; 48 + Ok(album 49 + .map(|a| (a.year_string, a.copyright_message)) 50 + .unwrap_or_default()) 51 + } 52 + 41 53 fn track_from_proto(t: crate::api::v1alpha1::Track) -> Track { 42 54 Track { 43 55 id: t.id, ··· 53 65 track_number: t.track_number, 54 66 disc_number: t.disc_number, 55 67 year: t.year, 68 + year_string: t.year_string, 56 69 album_art: t.album_art.filter(|s| !s.is_empty()), 57 70 } 58 71 } ··· 217 230 id: a.id, 218 231 name: a.name, 219 232 image: a.image, 233 + }) 234 + .collect(); 235 + let playlists = resp 236 + .playlists 237 + .into_iter() 238 + .map(|p| SearchPlaylist { 239 + id: p.id, 240 + name: p.name, 241 + description: p.description, 242 + image: p.image, 243 + is_smart: p.is_smart, 244 + track_count: p.track_count, 220 245 }) 221 246 .collect(); 222 247 Ok(SearchResults { 223 248 tracks, 224 249 albums, 225 250 artists, 251 + playlists, 226 252 }) 227 253 } 228 254 ··· 253 279 track_number: t.tracknum as u32, 254 280 disc_number: 0, 255 281 year: t.year as u32, 282 + year_string: String::new(), 256 283 album_art: t.album_art.filter(|s| !s.is_empty()), 257 284 }) 258 285 .collect(); ··· 598 625 track_number: t.tracknum as u32, 599 626 disc_number: 0, 600 627 year: t.year as u32, 628 + year_string: String::new(), 601 629 album_art: t.album_art.filter(|s| !s.is_empty()), 602 630 }) 603 631 .collect(); ··· 687 715 .await?; 688 716 Ok(()) 689 717 } 718 + 719 + // ── Saved Playlist API (gRPC) ───────────────────────────────────────────────── 720 + 721 + pub async fn fetch_saved_playlists() -> Result<Vec<crate::ui::components::SavedPlaylistItem>> { 722 + use crate::api::v1alpha1::{ 723 + saved_playlist_service_client::SavedPlaylistServiceClient, GetSavedPlaylistsRequest, 724 + }; 725 + let mut c = SavedPlaylistServiceClient::connect(URL).await?; 726 + let resp = c 727 + .get_saved_playlists(GetSavedPlaylistsRequest { folder_id: None }) 728 + .await?; 729 + Ok(resp 730 + .into_inner() 731 + .playlists 732 + .into_iter() 733 + .map(|p| crate::ui::components::SavedPlaylistItem { 734 + id: p.id, 735 + name: p.name, 736 + description: p.description, 737 + image: p.image, 738 + folder_id: p.folder_id, 739 + track_count: p.track_count, 740 + created_at: p.created_at, 741 + updated_at: p.updated_at, 742 + }) 743 + .collect()) 744 + } 745 + 746 + pub async fn fetch_smart_playlists() -> Result<Vec<crate::ui::components::SmartPlaylistItem>> { 747 + use crate::api::v1alpha1::{ 748 + smart_playlist_service_client::SmartPlaylistServiceClient, GetSmartPlaylistsRequest, 749 + }; 750 + let mut c = SmartPlaylistServiceClient::connect(URL).await?; 751 + let resp = c 752 + .get_smart_playlists(GetSmartPlaylistsRequest {}) 753 + .await?; 754 + Ok(resp 755 + .into_inner() 756 + .playlists 757 + .into_iter() 758 + .map(|p| crate::ui::components::SmartPlaylistItem { 759 + id: p.id, 760 + name: p.name, 761 + description: p.description, 762 + is_system: p.is_system, 763 + rules: p 764 + .rules 765 + .map(|r| format!("{} conditions", r.conditions.len())) 766 + .unwrap_or_default(), 767 + created_at: p.created_at, 768 + updated_at: p.updated_at, 769 + }) 770 + .collect()) 771 + } 772 + 773 + pub async fn create_saved_playlist( 774 + name: String, 775 + description: Option<String>, 776 + track_ids: Vec<String>, 777 + ) -> Result<()> { 778 + use crate::api::v1alpha1::{ 779 + saved_playlist_service_client::SavedPlaylistServiceClient, CreateSavedPlaylistRequest, 780 + }; 781 + let mut c = SavedPlaylistServiceClient::connect(URL).await?; 782 + c.create_saved_playlist(CreateSavedPlaylistRequest { 783 + name, 784 + description, 785 + image: None, 786 + folder_id: None, 787 + track_ids, 788 + }) 789 + .await?; 790 + Ok(()) 791 + } 792 + 793 + pub async fn add_track_to_playlist(playlist_id: String, track_id: String) -> Result<()> { 794 + use crate::api::v1alpha1::{ 795 + saved_playlist_service_client::SavedPlaylistServiceClient, 796 + AddTracksToSavedPlaylistRequest, 797 + }; 798 + let mut c = SavedPlaylistServiceClient::connect(URL).await?; 799 + c.add_tracks_to_saved_playlist(AddTracksToSavedPlaylistRequest { 800 + playlist_id, 801 + track_ids: vec![track_id], 802 + }) 803 + .await?; 804 + Ok(()) 805 + } 806 + 807 + /// Fetch track IDs for a saved playlist and resolve them from the provided tracks map. 808 + pub async fn fetch_saved_playlist_track_ids(playlist_id: String) -> Result<Vec<String>> { 809 + use crate::api::v1alpha1::{ 810 + saved_playlist_service_client::SavedPlaylistServiceClient, 811 + GetSavedPlaylistTracksRequest, 812 + }; 813 + let mut c = SavedPlaylistServiceClient::connect(URL).await?; 814 + let resp = c 815 + .get_saved_playlist_tracks(GetSavedPlaylistTracksRequest { playlist_id }) 816 + .await?; 817 + Ok(resp.into_inner().track_ids) 818 + } 819 + 820 + /// Fetch track IDs for a smart playlist. 821 + pub async fn fetch_smart_playlist_track_ids(playlist_id: String) -> Result<Vec<String>> { 822 + use crate::api::v1alpha1::{ 823 + smart_playlist_service_client::SmartPlaylistServiceClient, 824 + GetSmartPlaylistTracksRequest, 825 + }; 826 + let mut c = SmartPlaylistServiceClient::connect(URL).await?; 827 + let resp = c 828 + .get_smart_playlist_tracks(GetSmartPlaylistTracksRequest { id: playlist_id }) 829 + .await?; 830 + Ok(resp.into_inner().track_ids) 831 + } 832 + 833 + pub async fn play_saved_playlist(playlist_id: String) -> Result<()> { 834 + use crate::api::v1alpha1::{ 835 + saved_playlist_service_client::SavedPlaylistServiceClient, PlaySavedPlaylistRequest, 836 + }; 837 + let mut c = SavedPlaylistServiceClient::connect(URL).await?; 838 + c.play_saved_playlist(PlaySavedPlaylistRequest { playlist_id }) 839 + .await?; 840 + Ok(()) 841 + } 842 + 843 + pub async fn play_smart_playlist(playlist_id: String) -> Result<()> { 844 + use crate::api::v1alpha1::{ 845 + smart_playlist_service_client::SmartPlaylistServiceClient, PlaySmartPlaylistRequest, 846 + }; 847 + let mut c = SmartPlaylistServiceClient::connect(URL).await?; 848 + c.play_smart_playlist(PlaySmartPlaylistRequest { id: playlist_id }) 849 + .await?; 850 + Ok(()) 851 + } 852 + 853 + pub async fn delete_saved_playlist(playlist_id: String) -> Result<()> { 854 + use crate::api::v1alpha1::{ 855 + saved_playlist_service_client::SavedPlaylistServiceClient, DeleteSavedPlaylistRequest, 856 + }; 857 + let mut c = SavedPlaylistServiceClient::connect(URL).await?; 858 + c.delete_saved_playlist(DeleteSavedPlaylistRequest { id: playlist_id }) 859 + .await?; 860 + Ok(()) 861 + } 862 + 863 + pub async fn update_saved_playlist( 864 + id: String, 865 + name: String, 866 + description: Option<String>, 867 + ) -> Result<()> { 868 + use crate::api::v1alpha1::{ 869 + saved_playlist_service_client::SavedPlaylistServiceClient, UpdateSavedPlaylistRequest, 870 + }; 871 + let mut c = SavedPlaylistServiceClient::connect(URL).await?; 872 + c.update_saved_playlist(UpdateSavedPlaylistRequest { 873 + id, 874 + name, 875 + description, 876 + image: None, 877 + folder_id: None, 878 + }) 879 + .await?; 880 + Ok(()) 881 + } 882 + 883 + pub async fn remove_track_from_saved_playlist( 884 + playlist_id: String, 885 + track_id: String, 886 + ) -> Result<()> { 887 + use crate::api::v1alpha1::{ 888 + saved_playlist_service_client::SavedPlaylistServiceClient, 889 + RemoveTrackFromSavedPlaylistRequest, 890 + }; 891 + let mut c = SavedPlaylistServiceClient::connect(URL).await?; 892 + c.remove_track_from_saved_playlist(RemoveTrackFromSavedPlaylistRequest { 893 + playlist_id, 894 + track_id, 895 + }) 896 + .await?; 897 + Ok(()) 898 + } 899 + 900 + pub async fn play_saved_playlist_shuffled(playlist_id: String) -> Result<()> { 901 + use crate::api::v1alpha1::{ 902 + saved_playlist_service_client::SavedPlaylistServiceClient, PlaySavedPlaylistRequest, 903 + }; 904 + let mut c = SavedPlaylistServiceClient::connect(URL).await?; 905 + c.play_saved_playlist(PlaySavedPlaylistRequest { playlist_id }) 906 + .await?; 907 + // After loading, shuffle 908 + use crate::api::v1alpha1::{ 909 + playlist_service_client::PlaylistServiceClient, ShufflePlaylistRequest, 910 + }; 911 + let mut pc = PlaylistServiceClient::connect(URL).await?; 912 + pc.shuffle_playlist(ShufflePlaylistRequest { start_index: 0 }) 913 + .await?; 914 + Ok(()) 915 + }
+17
gpui/src/state.rs
··· 13 13 pub track_number: u32, 14 14 pub disc_number: u32, 15 15 pub year: u32, 16 + pub year_string: String, 16 17 pub album_art: Option<String>, 17 18 } 18 19 ··· 36 37 } 37 38 38 39 #[derive(Clone, Default)] 40 + pub struct SearchPlaylist { 41 + pub id: String, 42 + pub name: String, 43 + pub description: Option<String>, 44 + pub image: Option<String>, 45 + pub is_smart: bool, 46 + pub track_count: i64, 47 + } 48 + 49 + #[derive(Clone, Default)] 39 50 pub struct SearchResults { 40 51 pub tracks: Vec<Track>, 41 52 pub albums: Vec<SearchAlbum>, 42 53 pub artists: Vec<SearchArtist>, 54 + pub playlists: Vec<SearchPlaylist>, 43 55 } 44 56 45 57 #[derive(Clone, Copy, Debug, PartialEq)] ··· 149 161 pub fn format_duration(secs: u64) -> String { 150 162 format!("{}:{:02}", secs / 60, secs % 60) 151 163 } 164 + 165 + /// Stores the Tokio runtime handle so GPUI code can run async tasks that require a Tokio reactor. 166 + #[derive(Clone)] 167 + pub struct TokioHandle(pub tokio::runtime::Handle); 168 + impl gpui::Global for TokioHandle {}
+8
gpui/src/ui/components/icons.rs
··· 171 171 HardDrive, 172 172 Directory, 173 173 ChevronLeft, 174 + Playlist, 175 + CirclePlus, 176 + Pencil, 177 + Trash, 174 178 } 175 179 176 180 impl IconNamed for Icons { ··· 200 204 Icons::HardDrive => "icons/harddrive.svg", 201 205 Icons::Directory => "icons/directory.svg", 202 206 Icons::ChevronLeft => "icons/chevron-left.svg", 207 + Icons::Playlist => "icons/playlist.svg", 208 + Icons::CirclePlus => "icons/circle-plus.svg", 209 + Icons::Pencil => "icons/pencil.svg", 210 + Icons::Trash => "icons/trash.svg", 203 211 } 204 212 .into() 205 213 }
+101
gpui/src/ui/components/mod.rs
··· 4 4 pub mod navbar; 5 5 pub mod pages; 6 6 pub mod search_input; 7 + pub mod text_input; 7 8 pub mod seek_bar; 8 9 pub mod titlebar; 9 10 ··· 24 25 ArtistDetail, 25 26 Likes, 26 27 Files, 28 + Playlists, 29 + PlaylistDetail, 30 + SmartPlaylistDetail, 27 31 } 28 32 impl gpui::Global for LibrarySection {} 29 33 ··· 60 64 pub struct SelectedAlbum(pub String); 61 65 impl gpui::Global for SelectedAlbum {} 62 66 67 + #[derive(Clone, Default)] 68 + pub struct SelectedAlbumMeta { 69 + pub album_id: String, 70 + pub year_string: String, 71 + pub copyright_message: Option<String>, 72 + } 73 + impl gpui::Global for SelectedAlbumMeta {} 74 + 63 75 #[derive(Clone, PartialEq)] 64 76 pub struct SelectedArtist(pub String); 65 77 impl gpui::Global for SelectedArtist {} ··· 77 89 pub artist: String, 78 90 pub album: String, 79 91 pub album_art: Option<String>, 92 + pub track_id: String, 80 93 } 81 94 82 95 #[derive(Clone, Default)] ··· 100 113 #[derive(Clone, Default)] 101 114 pub struct HoveredAlbumIdx(pub Option<usize>); 102 115 impl gpui::Global for HoveredAlbumIdx {} 116 + 117 + // ── Playlist state types ────────────────────────────────────────────────────── 118 + 119 + #[derive(Clone, Default)] 120 + pub struct SavedPlaylistItem { 121 + pub id: String, 122 + pub name: String, 123 + pub description: Option<String>, 124 + pub image: Option<String>, 125 + pub folder_id: Option<String>, 126 + pub track_count: i64, 127 + pub created_at: i64, 128 + pub updated_at: i64, 129 + } 130 + 131 + #[derive(Clone, Default)] 132 + pub struct SmartPlaylistItem { 133 + pub id: String, 134 + pub name: String, 135 + pub description: Option<String>, 136 + pub is_system: bool, 137 + pub rules: String, 138 + pub created_at: i64, 139 + pub updated_at: i64, 140 + } 141 + 142 + #[derive(Clone, Default)] 143 + pub struct PlaylistsState { 144 + pub saved: Vec<SavedPlaylistItem>, 145 + pub smart: Vec<SmartPlaylistItem>, 146 + pub playlist_tracks: Vec<crate::state::Track>, 147 + } 148 + impl gpui::Global for PlaylistsState {} 149 + 150 + #[derive(Clone, Default)] 151 + pub struct SelectedPlaylist { 152 + pub id: String, 153 + pub name: String, 154 + pub is_smart: bool, 155 + } 156 + impl gpui::Global for SelectedPlaylist {} 157 + 158 + #[derive(Clone, Default)] 159 + pub struct PlaylistsSidebarCollapsed(pub bool); 160 + impl gpui::Global for PlaylistsSidebarCollapsed {} 161 + 162 + #[derive(Clone, Default)] 163 + pub struct CreatePlaylistModal { 164 + pub open: bool, 165 + pub name: String, 166 + pub description: String, 167 + /// Track to add to the newly created playlist (set when opened from "Add to Playlist" submenu). 168 + pub pending_track_id: Option<String>, 169 + } 170 + impl gpui::Global for CreatePlaylistModal {} 171 + 172 + #[derive(Clone, Default)] 173 + pub struct EditPlaylistModal { 174 + pub open: bool, 175 + pub id: String, 176 + pub name: String, 177 + pub description: String, 178 + } 179 + impl gpui::Global for EditPlaylistModal {} 180 + 181 + #[derive(Clone, Default)] 182 + pub struct DeletePlaylistModal { 183 + pub open: bool, 184 + pub id: String, 185 + pub name: String, 186 + } 187 + impl gpui::Global for DeletePlaylistModal {} 188 + 189 + #[derive(Clone)] 190 + pub struct AddToPlaylistMenu { 191 + /// Right edge of the parent context menu (flyout opens here by default). 192 + pub anchor_x: gpui::Pixels, 193 + /// Left edge of parent context menu (used when flipping left on overflow). 194 + pub flip_x: gpui::Pixels, 195 + /// Y of the "Add to Playlist" row (submenu aligns here). 196 + pub anchor_y: gpui::Pixels, 197 + pub track_path: String, 198 + pub track_id: String, 199 + } 200 + 201 + #[derive(Clone, Default)] 202 + pub struct AddToPlaylistMenuState(pub Option<AddToPlaylistMenu>); 203 + impl gpui::Global for AddToPlaylistMenuState {}
+1897 -18
gpui/src/ui/components/pages/library.rs
··· 9 9 use crate::ui::components::miniplayer::MiniPlayer; 10 10 use crate::ui::components::pages::files::{menu_item, FilesView}; 11 11 use crate::ui::components::search_input::SearchInput; 12 + use crate::ui::components::text_input::TextInput; 12 13 use crate::ui::components::{ 13 - AlbumContextMenu, AlbumContextMenuState, BackSection, FileContextMenuState, HoveredAlbumIdx, 14 - LibraryContextMenu, LibraryContextMenuState, LibrarySection, LikedOrder, LikedSongs, 15 - SelectedAlbum, SelectedArtist, 14 + AddToPlaylistMenuState, AlbumContextMenu, AlbumContextMenuState, BackSection, 15 + CreatePlaylistModal, DeletePlaylistModal, EditPlaylistModal, FileContextMenuState, 16 + HoveredAlbumIdx, LibraryContextMenu, LibraryContextMenuState, LibrarySection, LikedOrder, 17 + LikedSongs, PlaylistsSidebarCollapsed, PlaylistsState, SelectedAlbum, SelectedAlbumMeta, 18 + SelectedArtist, SelectedPlaylist, 16 19 }; 17 20 use crate::ui::theme::Theme; 18 21 use gpui::prelude::FluentBuilder; ··· 23 26 }; 24 27 25 28 const COVERS_BASE: &str = "http://localhost:6062/covers/"; 29 + 30 + /// Parse "yyyy-MM-dd" into "9 December 2014". Falls back to the raw string on any parse failure. 31 + fn format_release_date(s: &str) -> String { 32 + const MONTHS: [&str; 12] = [ 33 + "January", "February", "March", "April", "May", "June", 34 + "July", "August", "September", "October", "November", "December", 35 + ]; 36 + let parts: Vec<&str> = s.splitn(3, '-').collect(); 37 + if parts.len() == 3 { 38 + if let (Ok(y), Ok(m), Ok(d)) = ( 39 + parts[0].parse::<u32>(), 40 + parts[1].parse::<usize>(), 41 + parts[2].parse::<u32>(), 42 + ) { 43 + if m >= 1 && m <= 12 { 44 + return format!("{} {} {}", d, MONTHS[m - 1], y); 45 + } 46 + } 47 + } 48 + s.to_string() 49 + } 26 50 27 51 /// Square art tile that fills its container (used in grids). `icon_size` is the size_N shorthand number. 28 52 fn art_tile( ··· 107 131 miniplayer: Entity<MiniPlayer>, 108 132 search_input: Entity<SearchInput>, 109 133 files_view: Entity<FilesView>, 134 + modal_name_input: Entity<TextInput>, 135 + modal_desc_input: Entity<TextInput>, 136 + edit_name_input: Entity<TextInput>, 137 + edit_desc_input: Entity<TextInput>, 110 138 _search_sub: Option<Subscription>, 139 + _playlists_sub: Subscription, 140 + _edit_modal_sub: Subscription, 141 + _delete_modal_sub: Subscription, 142 + _album_meta_sub: Subscription, 111 143 } 112 144 113 145 impl LibraryPage { 114 - pub fn new(cx: &mut App) -> Self { 146 + pub fn new(cx: &mut gpui::Context<Self>) -> Self { 115 147 cx.set_global(LibrarySection::Songs); 116 148 cx.set_global(SelectedAlbum(String::new())); 117 149 cx.set_global(SelectedArtist(String::new())); ··· 121 153 cx.set_global(HoveredAlbumIdx::default()); 122 154 cx.set_global(LikedSongs::default()); 123 155 cx.set_global(LikedOrder::default()); 156 + cx.set_global(PlaylistsState::default()); 157 + cx.set_global(SelectedPlaylist::default()); 158 + cx.set_global(PlaylistsSidebarCollapsed(false)); 159 + cx.set_global(CreatePlaylistModal::default()); 160 + cx.set_global(AddToPlaylistMenuState::default()); 161 + cx.set_global(EditPlaylistModal::default()); 162 + cx.set_global(DeletePlaylistModal::default()); 163 + cx.set_global(SelectedAlbumMeta::default()); 164 + 165 + // Re-render whenever PlaylistsState changes (initial load, post-create refresh, etc.) 166 + let _playlists_sub = cx.observe_global::<PlaylistsState>(|_, cx| cx.notify()); 167 + let _edit_modal_sub = cx.observe_global::<EditPlaylistModal>(|this, cx| { 168 + let modal = cx.global::<EditPlaylistModal>().clone(); 169 + if modal.open { 170 + this.edit_name_input.update(cx, |input, cx| { 171 + input.value = modal.name.clone(); 172 + cx.notify(); 173 + }); 174 + this.edit_desc_input.update(cx, |input, cx| { 175 + input.value = modal.description.clone(); 176 + cx.notify(); 177 + }); 178 + } 179 + cx.notify(); 180 + }); 181 + let _delete_modal_sub = cx.observe_global::<DeletePlaylistModal>(|_, cx| cx.notify()); 182 + 183 + // Fetch album metadata (year + copyright) whenever the selected album changes. 184 + let _album_meta_sub = cx.observe_global::<SelectedAlbum>(|_this, cx| { 185 + let album_name = cx.global::<SelectedAlbum>().0.clone(); 186 + let album_id = cx 187 + .global::<Controller>() 188 + .state 189 + .read(cx) 190 + .tracks 191 + .iter() 192 + .find(|t| t.album == album_name) 193 + .map(|t| t.album_id.clone()) 194 + .unwrap_or_default(); 195 + if album_id.is_empty() { 196 + return; 197 + } 198 + let tokio = cx.global::<crate::state::TokioHandle>().0.clone(); 199 + cx.spawn(async move |_, cx| { 200 + let result = cx 201 + .background_executor() 202 + .spawn(async move { 203 + tokio.block_on(async { 204 + crate::client::get_album(&album_id).await 205 + }) 206 + }) 207 + .await; 208 + let _ = cx.update(|app: &mut gpui::App| { 209 + if let Ok((year_string, copyright_message)) = result { 210 + let meta = app.global_mut::<SelectedAlbumMeta>(); 211 + meta.year_string = year_string; 212 + meta.copyright_message = copyright_message; 213 + } 214 + }); 215 + }) 216 + .detach(); 217 + }); 218 + 219 + // Kick off the initial playlist load so the sidebar is populated from the start. 220 + let tokio = cx.global::<crate::state::TokioHandle>().0.clone(); 221 + cx.spawn(async move |_this, cx| { 222 + let (saved, smart) = cx 223 + .background_executor() 224 + .spawn(async move { 225 + tokio.block_on(async { 226 + let saved = crate::client::fetch_saved_playlists() 227 + .await 228 + .unwrap_or_default(); 229 + let smart = crate::client::fetch_smart_playlists() 230 + .await 231 + .unwrap_or_default(); 232 + (saved, smart) 233 + }) 234 + }) 235 + .await; 236 + let _ = cx.update(|app: &mut gpui::App| { 237 + let state = app.global_mut::<PlaylistsState>(); 238 + state.saved = saved; 239 + state.smart = smart; 240 + }); 241 + }) 242 + .detach(); 243 + 124 244 LibraryPage { 125 245 scroll_handle: UniformListScrollHandle::new(), 126 246 detail_scroll_handle: UniformListScrollHandle::new(), 127 247 miniplayer: cx.new(|_| MiniPlayer), 128 248 search_input: cx.new(|cx| SearchInput::new(cx)), 129 249 files_view: cx.new(|cx| FilesView::new(cx)), 250 + modal_name_input: cx.new(|cx| TextInput::new("Title", cx)), 251 + modal_desc_input: cx.new(|cx| TextInput::new("Description (optional)", cx)), 252 + edit_name_input: cx.new(|cx| TextInput::new("Title", cx)), 253 + edit_desc_input: cx.new(|cx| TextInput::new("Description (optional)", cx)), 130 254 _search_sub: None, 255 + _playlists_sub, 256 + _edit_modal_sub, 257 + _delete_modal_sub, 258 + _album_meta_sub, 131 259 } 132 260 } 133 261 } ··· 140 268 let hovered_album_idx = cx.global::<HoveredAlbumIdx>().0; 141 269 let selected_album = cx.global::<SelectedAlbum>().0.clone(); 142 270 let selected_artist = cx.global::<SelectedArtist>().0.clone(); 271 + let playlists_collapsed = cx.global::<PlaylistsSidebarCollapsed>().0; 272 + let saved_playlists = cx.global::<PlaylistsState>().saved.clone(); 273 + let smart_playlists = cx.global::<PlaylistsState>().smart.clone(); 274 + let playlist_tracks = cx.global::<PlaylistsState>().playlist_tracks.clone(); 275 + let selected_playlist = cx.global::<SelectedPlaylist>().clone(); 276 + let create_modal = cx.global::<CreatePlaylistModal>().clone(); 277 + let edit_modal = cx.global::<EditPlaylistModal>().clone(); 278 + let delete_modal = cx.global::<DeletePlaylistModal>().clone(); 279 + let add_to_playlist_menu = cx.global::<AddToPlaylistMenuState>().0.clone(); 280 + let album_meta = cx.global::<SelectedAlbumMeta>().clone(); 281 + 282 + // Trigger playlist tracks load when in detail views 283 + if (section == LibrarySection::PlaylistDetail 284 + || section == LibrarySection::SmartPlaylistDetail) 285 + && playlist_tracks.is_empty() 286 + && !selected_playlist.id.is_empty() 287 + { 288 + let pid = selected_playlist.id.clone(); 289 + let is_smart = selected_playlist.is_smart; 290 + let all_tracks = cx.global::<Controller>().state.read(cx).tracks.clone(); 291 + let tokio = cx.global::<crate::state::TokioHandle>().0.clone(); 292 + cx.spawn(async move |_this: gpui::WeakEntity<LibraryPage>, cx| { 293 + let track_ids = cx 294 + .background_executor() 295 + .spawn(async move { 296 + tokio.block_on(async move { 297 + if is_smart { 298 + crate::client::fetch_smart_playlist_track_ids(pid) 299 + .await 300 + .unwrap_or_default() 301 + } else { 302 + crate::client::fetch_saved_playlist_track_ids(pid) 303 + .await 304 + .unwrap_or_default() 305 + } 306 + }) 307 + }) 308 + .await; 309 + let id_set: std::collections::HashSet<String> = 310 + track_ids.iter().cloned().collect(); 311 + let mut resolved: Vec<crate::state::Track> = all_tracks 312 + .into_iter() 313 + .filter(|t| id_set.contains(&t.id)) 314 + .collect(); 315 + let order_map: std::collections::HashMap<String, usize> = 316 + track_ids.into_iter().enumerate().map(|(i, id)| (id, i)).collect(); 317 + resolved.sort_by_key(|t| { 318 + order_map.get(&t.id).copied().unwrap_or(usize::MAX) 319 + }); 320 + let _ = cx.update(|app: &mut gpui::App| { 321 + app.global_mut::<PlaylistsState>().playlist_tracks = resolved; 322 + }); 323 + }) 324 + .detach(); 325 + } 143 326 144 327 let viewport = window.viewport_size(); 145 328 let liked_songs = cx.global::<LikedSongs>().0.clone(); ··· 394 577 if self._search_sub.is_none() { 395 578 let si = self.search_input.clone(); 396 579 self._search_sub = Some(cx.observe(&si, |_this, si_entity, cx| { 397 - let query = si_entity.read(cx).query.clone(); 580 + let query = si_entity.read(cx).query.trim().to_string(); 398 581 cx.global::<Controller>().search(query); 399 582 cx.notify(); 400 583 })); 401 584 } 402 - let query = self.search_input.read(cx).query.clone(); 585 + let query = self.search_input.read(cx).query.trim().to_string(); 586 + let search_input = self.search_input.clone(); 403 587 404 588 let context_menu = cx.global::<LibraryContextMenuState>().0.clone(); 405 589 let album_context_menu = cx.global::<AlbumContextMenuState>().0.clone(); ··· 407 591 let n_album_tracks = album_tracks.len(); 408 592 let n_artist_tracks = artist_tracks.len(); 409 593 let scroll_handle = self.scroll_handle.clone(); 594 + let modal_name_input = self.modal_name_input.clone(); 595 + let modal_desc_input = self.modal_desc_input.clone(); 596 + let modal_name_value = self.modal_name_input.read(cx).value.trim().to_string(); 597 + let modal_desc_value = self.modal_desc_input.read(cx).value.trim().to_string(); 598 + let edit_name_input = self.edit_name_input.clone(); 599 + let edit_desc_input = self.edit_desc_input.clone(); 600 + let edit_name_value = self.edit_name_input.read(cx).value.trim().to_string(); 601 + let edit_desc_value = self.edit_desc_input.read(cx).value.trim().to_string(); 410 602 let _detail_scroll_handle = self.detail_scroll_handle.clone(); 411 603 412 604 // Sidebar nav item — Albums/Artists stay active while in their detail view ··· 466 658 track_artist: String, 467 659 track_album: String, 468 660 track_id: String, 469 - track_art: Option<String>| { 661 + track_art: Option<String>, 662 + remove_playlist_id: Option<String>| { 470 663 let show_artist = artist.is_some(); 471 664 let show_album = album.is_some(); 472 665 let artist_text = artist.unwrap_or_default(); ··· 486 679 let heart_id: gpui::SharedString = format!("{}_heart_{}", row_id.0, row_id.1).into(); 487 680 let play_id: gpui::SharedString = format!("{}_play_{}", row_id.0, row_id.1).into(); 488 681 let path_for_play = path.clone(); 682 + let track_id_for_remove = track_id.clone(); 489 683 div() 490 684 .id(row_id) 491 685 .group(group_name) ··· 573 767 this.child( 574 768 div() 575 769 .w_40() 576 - .flex_shrink_0() 770 + .min_w_0() 771 + .overflow_hidden() 577 772 .text_sm() 578 773 .truncate() 579 774 .text_color(theme.library_header_text) ··· 584 779 this.child( 585 780 div() 586 781 .w_40() 587 - .flex_shrink_0() 782 + .min_w_0() 783 + .overflow_hidden() 588 784 .text_sm() 589 785 .truncate() 590 786 .text_color(theme.library_header_text) ··· 652 848 artist: opts_artist.clone(), 653 849 album: opts_album.clone(), 654 850 album_art: opts_art.clone(), 851 + track_id: track_id.clone(), 655 852 }); 656 853 }) 657 854 .child(Icon::new(Icons::Options).size_4()), 658 855 ) 856 + .when_some(remove_playlist_id, |this, pid| { 857 + let remove_id: gpui::SharedString = 858 + format!("{}_remove_{}", row_id.0, row_id.1).into(); 859 + let tid_for_remove = track_id_for_remove.clone(); 860 + this.child( 861 + div() 862 + .id(remove_id) 863 + .w(px(28.0)) 864 + .flex_shrink_0() 865 + .flex() 866 + .items_center() 867 + .justify_center() 868 + .cursor_pointer() 869 + .text_color(theme.library_header_text) 870 + .hover(|s| s.text_color(gpui::rgb(0xef4444))) 871 + .on_click(move |_, _, cx: &mut App| { 872 + cx.stop_propagation(); 873 + let tokio = 874 + cx.global::<crate::state::TokioHandle>().0.clone(); 875 + let pid2 = pid.clone(); 876 + let tid2 = tid_for_remove.clone(); 877 + cx.spawn(async move |cx| { 878 + let result = cx 879 + .background_executor() 880 + .spawn(async move { 881 + tokio.block_on(async move { 882 + let _ = crate::client::remove_track_from_saved_playlist(pid2, tid2).await; 883 + let saved = crate::client::fetch_saved_playlists() 884 + .await 885 + .ok(); 886 + saved 887 + }) 888 + }) 889 + .await; 890 + let _ = cx.update(|app: &mut gpui::App| { 891 + if let Some(saved) = result { 892 + app.global_mut::<PlaylistsState>().saved = saved; 893 + } 894 + app.global_mut::<PlaylistsState>().playlist_tracks = 895 + vec![]; 896 + }); 897 + }) 898 + .detach(); 899 + }) 900 + .child(Icon::new(Icons::Trash).size_4()), 901 + ) 902 + }) 659 903 }; 660 904 661 905 let show_search = !query.is_empty(); ··· 664 908 match search_results { 665 909 None => div().flex_1().into_any_element(), 666 910 Some(ref r) 667 - if r.tracks.is_empty() && r.albums.is_empty() && r.artists.is_empty() => 911 + if r.tracks.is_empty() 912 + && r.albums.is_empty() 913 + && r.artists.is_empty() 914 + && r.playlists.is_empty() => 668 915 { 669 916 div() 670 917 .flex_1() ··· 724 971 r.artists.iter().take(8).enumerate().map( 725 972 |(idx, artist)| { 726 973 let name_clone = artist.name.clone(); 974 + let si = search_input.clone(); 727 975 let img_url = artist 728 976 .image 729 977 .as_deref() ··· 751 999 ); 752 1000 *cx.global_mut::<LibrarySection>( 753 1001 ) = LibrarySection::ArtistDetail; 1002 + si.update(cx, |this, cx| { 1003 + this.query.clear(); 1004 + cx.notify(); 1005 + }); 754 1006 }) 755 1007 .child({ 756 1008 let mut c = div() ··· 819 1071 r.albums.iter().take(8).enumerate().map( 820 1072 |(idx, album)| { 821 1073 let title_clone = album.title.clone(); 1074 + let si = search_input.clone(); 822 1075 let art_url = album 823 1076 .album_art 824 1077 .as_deref() ··· 843 1096 ); 844 1097 *cx.global_mut::<LibrarySection>( 845 1098 ) = LibrarySection::AlbumDetail; 1099 + si.update(cx, |this, cx| { 1100 + this.query.clear(); 1101 + cx.notify(); 1102 + }); 846 1103 }) 847 1104 .child({ 848 1105 let mut c = div() ··· 917 1174 track.album.clone(), 918 1175 track.id.clone(), 919 1176 track.album_art.clone(), 1177 + None, 920 1178 ) 921 1179 }); 922 1180 this.child( ··· 933 1191 ) 934 1192 .children(rows), 935 1193 ) 1194 + }) 1195 + // ── Playlists ───────────────────────────────────── 1196 + .when(!r.playlists.is_empty(), |this| { 1197 + this.child( 1198 + div() 1199 + .flex() 1200 + .flex_col() 1201 + .gap_y_4() 1202 + .child( 1203 + div() 1204 + .text_base() 1205 + .font_weight(FontWeight(600.0)) 1206 + .text_color(theme.library_text) 1207 + .child("Playlists"), 1208 + ) 1209 + .child(div().flex().flex_col().gap_y_1().children( 1210 + r.playlists.iter().take(10).enumerate().map( 1211 + |(idx, pl)| { 1212 + let pl_id = pl.id.clone(); 1213 + let pl_name = pl.name.clone(); 1214 + let pl_is_smart = pl.is_smart; 1215 + let si = search_input.clone(); 1216 + div() 1217 + .id(("spl", idx)) 1218 + .flex() 1219 + .items_center() 1220 + .gap_x_3() 1221 + .px_2() 1222 + .py_2() 1223 + .rounded_md() 1224 + .cursor_pointer() 1225 + .hover(|s| s.bg(theme.library_table_border)) 1226 + .on_click(move |_, _, cx: &mut App| { 1227 + *cx.global_mut::<SelectedPlaylist>() = 1228 + SelectedPlaylist { 1229 + id: pl_id.clone(), 1230 + name: pl_name.clone(), 1231 + is_smart: pl_is_smart, 1232 + }; 1233 + *cx.global_mut::<LibrarySection>() = 1234 + if pl_is_smart { 1235 + LibrarySection::SmartPlaylistDetail 1236 + } else { 1237 + LibrarySection::PlaylistDetail 1238 + }; 1239 + si.update(cx, |this, cx| { 1240 + this.query.clear(); 1241 + cx.notify(); 1242 + }); 1243 + }) 1244 + .child( 1245 + div() 1246 + .flex() 1247 + .items_center() 1248 + .justify_center() 1249 + .w_8() 1250 + .h_8() 1251 + .rounded_md() 1252 + .bg(theme.library_art_bg) 1253 + .text_color(theme.player_icons_text) 1254 + .child( 1255 + Icon::new(Icons::Playlist) 1256 + .size_4(), 1257 + ), 1258 + ) 1259 + .child( 1260 + div() 1261 + .flex() 1262 + .flex_col() 1263 + .gap_y_0p5() 1264 + .child( 1265 + div() 1266 + .text_sm() 1267 + .font_weight(FontWeight(500.0)) 1268 + .text_color(theme.library_text) 1269 + .truncate() 1270 + .child(pl.name.clone()), 1271 + ) 1272 + .child( 1273 + div() 1274 + .text_xs() 1275 + .text_color(theme.library_header_text) 1276 + .child(if pl_is_smart { 1277 + "Smart Playlist" 1278 + } else { 1279 + "Playlist" 1280 + }), 1281 + ), 1282 + ) 1283 + }, 1284 + ), 1285 + )), 1286 + ) 936 1287 }), 937 1288 ) 938 1289 .into_any_element() ··· 978 1329 .child( 979 1330 div() 980 1331 .w_40() 981 - .flex_shrink_0() 1332 + .min_w_0() 1333 + .overflow_hidden() 982 1334 .text_xs() 983 1335 .font_weight(FontWeight::MEDIUM) 984 1336 .text_color(theme.library_header_text) ··· 987 1339 .child( 988 1340 div() 989 1341 .w_40() 990 - .flex_shrink_0() 1342 + .min_w_0() 1343 + .overflow_hidden() 991 1344 .text_xs() 992 1345 .font_weight(FontWeight::MEDIUM) 993 1346 .text_color(theme.library_header_text) ··· 1029 1382 track.album.clone(), 1030 1383 track.id.clone(), 1031 1384 track.album_art.clone(), 1385 + None, 1032 1386 ) 1033 1387 }) 1034 1388 .collect() ··· 1347 1701 div() 1348 1702 .id("album_detail_scroll") 1349 1703 .flex_1() 1704 + .min_w_0() 1350 1705 .min_h_0() 1351 1706 .overflow_y_scroll() 1352 1707 .child( 1353 1708 div() 1354 1709 .w_full() 1710 + .min_w_0() 1355 1711 .flex() 1356 1712 .flex_col() 1357 1713 // Header ··· 1553 1909 row_album, 1554 1910 track_id, 1555 1911 art, 1912 + None, 1556 1913 ) 1557 1914 .into_any_element(), 1558 1915 ); 1559 1916 } 1560 1917 div().children(rows) 1918 + }) 1919 + // Footer: release year and copyright 1920 + .child({ 1921 + let year = album_meta.year_string.clone(); 1922 + let copyright = album_meta.copyright_message.clone(); 1923 + let show_footer = !year.is_empty() || copyright.is_some(); 1924 + div() 1925 + .when(show_footer, |d| { 1926 + d.flex() 1927 + .flex_col() 1928 + .gap_y_1() 1929 + .px_6() 1930 + .pt_6() 1931 + .pb_8() 1932 + .when(!year.is_empty(), |d| { 1933 + d.child( 1934 + div() 1935 + .text_sm() 1936 + .text_color(theme.library_header_text) 1937 + .child(format_release_date(&year)), 1938 + ) 1939 + }) 1940 + .when_some(copyright, |d, msg| { 1941 + d.child( 1942 + div() 1943 + .text_sm() 1944 + .text_color(theme.library_header_text) 1945 + .child(msg), 1946 + ) 1947 + }) 1948 + }) 1561 1949 }), 1562 1950 ) 1563 1951 .into_any_element() ··· 1576 1964 div() 1577 1965 .id("artist_detail_scroll") 1578 1966 .flex_1() 1967 + .min_w_0() 1579 1968 .min_h_0() 1580 1969 .overflow_y_scroll() 1581 1970 .child( 1582 1971 div() 1583 1972 .w_full() 1973 + .min_w_0() 1584 1974 .flex() 1585 1975 .flex_col() 1586 1976 // Header ··· 1812 2202 .child( 1813 2203 div() 1814 2204 .w_40() 1815 - .flex_shrink_0() 2205 + .min_w_0() 2206 + .overflow_hidden() 1816 2207 .text_xs() 1817 2208 .font_weight(FontWeight::MEDIUM) 1818 2209 .text_color(theme.library_header_text) ··· 1851 2242 row_album, 1852 2243 track_id, 1853 2244 art, 2245 + None, 1854 2246 ) 1855 2247 }, 1856 2248 )), ··· 2022 2414 .child( 2023 2415 div() 2024 2416 .w_40() 2025 - .flex_shrink_0() 2417 + .overflow_hidden() 2026 2418 .text_xs() 2027 2419 .font_weight(FontWeight::MEDIUM) 2028 2420 .text_color(theme.library_header_text) ··· 2031 2423 .child( 2032 2424 div() 2033 2425 .w_40() 2034 - .flex_shrink_0() 2426 + .min_w_0() 2427 + .overflow_hidden() 2035 2428 .text_xs() 2036 2429 .font_weight(FontWeight::MEDIUM) 2037 2430 .text_color(theme.library_header_text) ··· 2081 2474 row_album, 2082 2475 track_id, 2083 2476 art, 2477 + None, 2084 2478 ) 2085 2479 }, 2086 2480 )), ··· 2090 2484 2091 2485 // ── Files ───────────────────────────────────────────────────────────── 2092 2486 LibrarySection::Files => self.files_view.clone().into_any_element(), 2487 + 2488 + // ── Playlists ────────────────────────────────────────────────────────── 2489 + LibrarySection::Playlists => { 2490 + div() 2491 + .id("playlists_scroll") 2492 + .flex_1() 2493 + .min_h_0() 2494 + .overflow_y_scroll() 2495 + .child( 2496 + div() 2497 + .w_full() 2498 + .flex() 2499 + .flex_col() 2500 + // ── Header ────────────────────────────────── 2501 + .child( 2502 + div() 2503 + .px_6() 2504 + .pt_5() 2505 + .pb_4() 2506 + .flex() 2507 + .items_center() 2508 + .justify_between() 2509 + .child( 2510 + div() 2511 + .flex() 2512 + .items_center() 2513 + .gap_x_3() 2514 + .child( 2515 + Icon::new(Icons::Playlist) 2516 + .size_8() 2517 + .text_color(theme.library_text), 2518 + ) 2519 + .child( 2520 + div() 2521 + .text_2xl() 2522 + .font_weight(FontWeight(700.0)) 2523 + .text_color(theme.library_text) 2524 + .child("Playlists"), 2525 + ), 2526 + ) 2527 + .child( 2528 + div() 2529 + .id("new_playlist_header_btn") 2530 + .flex() 2531 + .items_center() 2532 + .gap_x_1() 2533 + .px_3() 2534 + .py_1p5() 2535 + .rounded_md() 2536 + .cursor_pointer() 2537 + .bg(theme.player_play_pause_bg) 2538 + .text_color(theme.player_play_pause_text) 2539 + .hover(|this| { 2540 + this.bg(theme.player_play_pause_hover) 2541 + }) 2542 + .on_click(|_, _, cx: &mut App| { 2543 + cx.global_mut::<CreatePlaylistModal>().open = 2544 + true; 2545 + }) 2546 + .child(Icon::new(Icons::CirclePlus).size_4()) 2547 + .child( 2548 + div() 2549 + .text_sm() 2550 + .font_weight(FontWeight(600.0)) 2551 + .child("New Playlist"), 2552 + ), 2553 + ), 2554 + ) 2555 + // ── Saved playlists grid ───────────────────── 2556 + .when(!saved_playlists.is_empty(), |this| { 2557 + this.child( 2558 + div() 2559 + .px_6() 2560 + .pb_2() 2561 + .text_xs() 2562 + .font_weight(FontWeight(600.0)) 2563 + .text_color(theme.library_header_text) 2564 + .child("MY PLAYLISTS"), 2565 + ) 2566 + .child( 2567 + div() 2568 + .px_6() 2569 + .pb_6() 2570 + .grid() 2571 + .grid_cols(album_cols) 2572 + .gap_4() 2573 + .children( 2574 + saved_playlists.clone().into_iter().enumerate().map( 2575 + |(i, pl)| { 2576 + let pl_id_click = pl.id.clone(); 2577 + let pl_name = pl.name.clone(); 2578 + let pl_name_click = pl.name.clone(); 2579 + let group_name: gpui::SharedString = 2580 + format!("pl_card_{}", i).into(); 2581 + div() 2582 + .id(("saved_pl", i)) 2583 + .group(group_name.clone()) 2584 + .flex() 2585 + .flex_col() 2586 + .gap_y_2() 2587 + .cursor_pointer() 2588 + .p_2() 2589 + .rounded_lg() 2590 + .hover(|this| { 2591 + this.bg(theme.library_track_bg_hover) 2592 + }) 2593 + .on_click(move |_, _, cx: &mut App| { 2594 + *cx.global_mut::<SelectedPlaylist>() = 2595 + SelectedPlaylist { 2596 + id: pl_id_click.clone(), 2597 + name: pl_name_click.clone(), 2598 + is_smart: false, 2599 + }; 2600 + cx.global_mut::<PlaylistsState>() 2601 + .playlist_tracks = vec![]; 2602 + *cx.global_mut::<LibrarySection>() = 2603 + LibrarySection::PlaylistDetail; 2604 + }) 2605 + .child( 2606 + div() 2607 + .relative() 2608 + .child(art_tile( 2609 + pl.image.clone(), 2610 + theme, 2611 + Icons::Playlist, 2612 + 8, 2613 + )) 2614 + .child( 2615 + div() 2616 + .absolute() 2617 + .bottom(px(4.0)) 2618 + .right(px(4.0)) 2619 + .flex() 2620 + .gap_x_1() 2621 + .opacity(0.0) 2622 + .group_hover(group_name.clone(), |s| s.opacity(1.0)) 2623 + .child( 2624 + div() 2625 + .id(("pl_edit_btn", i)) 2626 + .p_1() 2627 + .rounded_md() 2628 + .bg(theme.titlebar_bg) 2629 + .cursor_pointer() 2630 + .text_color(theme.library_header_text) 2631 + .hover(|s| s.text_color(theme.library_text)) 2632 + .on_click({ 2633 + let pl_id = pl.id.clone(); 2634 + let pl_name2 = pl.name.clone(); 2635 + let pl_desc = pl.description.clone().unwrap_or_default(); 2636 + move |_, _, cx: &mut App| { 2637 + cx.stop_propagation(); 2638 + let modal = cx.global_mut::<EditPlaylistModal>(); 2639 + modal.open = true; 2640 + modal.id = pl_id.clone(); 2641 + modal.name = pl_name2.clone(); 2642 + modal.description = pl_desc.clone(); 2643 + } 2644 + }) 2645 + .child(Icon::new(Icons::Pencil).size_4()), 2646 + ) 2647 + .child( 2648 + div() 2649 + .id(("pl_del_btn", i)) 2650 + .p_1() 2651 + .rounded_md() 2652 + .bg(theme.titlebar_bg) 2653 + .cursor_pointer() 2654 + .text_color(theme.library_header_text) 2655 + .hover(|s| s.text_color(gpui::rgb(0xef4444))) 2656 + .on_click({ 2657 + let pl_id = pl.id.clone(); 2658 + let pl_name2 = pl.name.clone(); 2659 + move |_, _, cx: &mut App| { 2660 + cx.stop_propagation(); 2661 + let modal = cx.global_mut::<DeletePlaylistModal>(); 2662 + modal.open = true; 2663 + modal.id = pl_id.clone(); 2664 + modal.name = pl_name2.clone(); 2665 + } 2666 + }) 2667 + .child(Icon::new(Icons::Trash).size_4()), 2668 + ), 2669 + ), 2670 + ) 2671 + .child( 2672 + div() 2673 + .flex() 2674 + .flex_col() 2675 + .gap_y_0p5() 2676 + .child( 2677 + div() 2678 + .text_sm() 2679 + .font_weight( 2680 + FontWeight(600.0), 2681 + ) 2682 + .text_color( 2683 + theme.library_text, 2684 + ) 2685 + .truncate() 2686 + .child(pl_name.clone()), 2687 + ) 2688 + .child( 2689 + div() 2690 + .text_xs() 2691 + .text_color( 2692 + theme 2693 + .library_header_text, 2694 + ) 2695 + .child(format!( 2696 + "{} tracks", 2697 + pl.track_count 2698 + )), 2699 + ), 2700 + ) 2701 + }, 2702 + ), 2703 + ), 2704 + ) 2705 + }) 2706 + // ── Smart playlists ────────────────────────── 2707 + .when(!smart_playlists.is_empty(), |this| { 2708 + this.child( 2709 + div() 2710 + .px_6() 2711 + .pb_2() 2712 + .text_xs() 2713 + .font_weight(FontWeight(600.0)) 2714 + .text_color(theme.library_header_text) 2715 + .child("SMART PLAYLISTS"), 2716 + ) 2717 + .child( 2718 + div() 2719 + .px_6() 2720 + .pb_6() 2721 + .grid() 2722 + .grid_cols(album_cols) 2723 + .gap_4() 2724 + .children( 2725 + smart_playlists 2726 + .clone() 2727 + .into_iter() 2728 + .enumerate() 2729 + .map(|(i, pl)| { 2730 + let pl_id_click = pl.id.clone(); 2731 + let pl_name = pl.name.clone(); 2732 + let pl_name_click = pl.name.clone(); 2733 + div() 2734 + .id(("smart_pl", i)) 2735 + .flex() 2736 + .flex_col() 2737 + .gap_y_2() 2738 + .cursor_pointer() 2739 + .p_2() 2740 + .rounded_lg() 2741 + .hover(|this| { 2742 + this.bg(theme.library_track_bg_hover) 2743 + }) 2744 + .on_click(move |_, _, cx: &mut App| { 2745 + *cx.global_mut::<SelectedPlaylist>() = 2746 + SelectedPlaylist { 2747 + id: pl_id_click.clone(), 2748 + name: pl_name_click.clone(), 2749 + is_smart: true, 2750 + }; 2751 + cx.global_mut::<PlaylistsState>() 2752 + .playlist_tracks = vec![]; 2753 + *cx.global_mut::<LibrarySection>() = 2754 + LibrarySection::SmartPlaylistDetail; 2755 + }) 2756 + .child(art_tile( 2757 + None, 2758 + theme, 2759 + Icons::MusicList, 2760 + 8, 2761 + )) 2762 + .child( 2763 + div() 2764 + .flex() 2765 + .flex_col() 2766 + .gap_y_0p5() 2767 + .child( 2768 + div() 2769 + .text_sm() 2770 + .font_weight( 2771 + FontWeight(600.0), 2772 + ) 2773 + .text_color( 2774 + theme.library_text, 2775 + ) 2776 + .truncate() 2777 + .child(pl_name.clone()), 2778 + ) 2779 + .child( 2780 + div() 2781 + .text_xs() 2782 + .text_color( 2783 + theme 2784 + .library_header_text, 2785 + ) 2786 + .child("Smart Playlist"), 2787 + ), 2788 + ) 2789 + }), 2790 + ), 2791 + ) 2792 + }) 2793 + .when( 2794 + saved_playlists.is_empty() && smart_playlists.is_empty(), 2795 + |this| { 2796 + this.child( 2797 + div() 2798 + .flex_1() 2799 + .flex() 2800 + .flex_col() 2801 + .items_center() 2802 + .justify_center() 2803 + .gap_y_3() 2804 + .py_16() 2805 + .text_color(theme.library_header_text) 2806 + .child( 2807 + Icon::new(Icons::Playlist) 2808 + .size_10() 2809 + .text_color(theme.library_header_text), 2810 + ) 2811 + .child( 2812 + div() 2813 + .text_sm() 2814 + .child("No playlists yet"), 2815 + ) 2816 + .child( 2817 + div() 2818 + .id("create_first_playlist_btn") 2819 + .px_4() 2820 + .py_2() 2821 + .rounded_md() 2822 + .cursor_pointer() 2823 + .bg(theme.player_play_pause_bg) 2824 + .text_color(theme.player_play_pause_text) 2825 + .text_sm() 2826 + .hover(|this| { 2827 + this.bg(theme.player_play_pause_hover) 2828 + }) 2829 + .on_click(|_, _, cx: &mut App| { 2830 + cx.global_mut::<CreatePlaylistModal>() 2831 + .open = true; 2832 + }) 2833 + .child("Create your first playlist"), 2834 + ), 2835 + ) 2836 + }, 2837 + ), 2838 + ) 2839 + .into_any_element() 2840 + } 2841 + 2842 + // ── PlaylistDetail ───────────────────────────────────────────────────── 2843 + LibrarySection::PlaylistDetail | LibrarySection::SmartPlaylistDetail => { 2844 + let pl_name = selected_playlist.name.clone(); 2845 + let pl_id_play = selected_playlist.id.clone(); 2846 + let pl_id_shuffled = selected_playlist.id.clone(); 2847 + let pl_is_smart = selected_playlist.is_smart; 2848 + let selected_playlist_id_for_remove = selected_playlist.id.clone(); 2849 + let n_pl_tracks = playlist_tracks.len(); 2850 + div() 2851 + .id("playlist_detail_scroll") 2852 + .flex_1() 2853 + .min_w_0() 2854 + .min_h_0() 2855 + .overflow_y_scroll() 2856 + .child( 2857 + div() 2858 + .w_full() 2859 + .min_w_0() 2860 + .flex() 2861 + .flex_col() 2862 + // ── Back + Header ───────────────────────────── 2863 + .child( 2864 + div() 2865 + .px_6() 2866 + .pt_5() 2867 + .pb_4() 2868 + .flex() 2869 + .flex_col() 2870 + .gap_y_4() 2871 + .child( 2872 + div() 2873 + .id("pl_back_btn") 2874 + .flex() 2875 + .items_center() 2876 + .gap_x_1() 2877 + .cursor_pointer() 2878 + .text_xs() 2879 + .text_color(theme.library_header_text) 2880 + .hover(|this| this.text_color(theme.library_text)) 2881 + .on_click(|_, _, cx: &mut App| { 2882 + *cx.global_mut::<LibrarySection>() = 2883 + LibrarySection::Playlists; 2884 + }) 2885 + .child( 2886 + Icon::new(Icons::ChevronLeft) 2887 + .size_3() 2888 + .text_color(theme.library_header_text), 2889 + ) 2890 + .child("Playlists"), 2891 + ) 2892 + .child( 2893 + div() 2894 + .flex() 2895 + .items_center() 2896 + .gap_x_4() 2897 + .child(art_fixed( 2898 + None, 2899 + theme, 2900 + if pl_is_smart { 2901 + Icons::MusicList 2902 + } else { 2903 + Icons::Playlist 2904 + }, 2905 + px(120.0), 2906 + )) 2907 + .child( 2908 + div() 2909 + .flex() 2910 + .flex_col() 2911 + .gap_y_2() 2912 + .child( 2913 + div() 2914 + .text_2xl() 2915 + .font_weight(FontWeight(700.0)) 2916 + .text_color(theme.library_text) 2917 + .child(pl_name.clone()), 2918 + ) 2919 + .child( 2920 + div() 2921 + .text_sm() 2922 + .text_color( 2923 + theme.library_header_text, 2924 + ) 2925 + .child(format!( 2926 + "{} track{}", 2927 + n_pl_tracks, 2928 + if n_pl_tracks == 1 { "" } else { "s" } 2929 + )), 2930 + ) 2931 + .child( 2932 + div() 2933 + .flex() 2934 + .items_center() 2935 + .gap_x_3() 2936 + .child( 2937 + div() 2938 + .id("pl_play_btn") 2939 + .flex() 2940 + .items_center() 2941 + .gap_x_2() 2942 + .px_4() 2943 + .py_2() 2944 + .rounded_md() 2945 + .bg( 2946 + theme 2947 + .player_play_pause_bg, 2948 + ) 2949 + .text_color( 2950 + theme 2951 + .player_play_pause_text, 2952 + ) 2953 + .when(n_pl_tracks == 0, |this| { 2954 + this.opacity(0.5).cursor_default() 2955 + }) 2956 + .when(n_pl_tracks > 0, |this| { 2957 + this.cursor_pointer() 2958 + .hover(|s| { 2959 + s.bg(theme.player_play_pause_hover) 2960 + }) 2961 + .on_click( 2962 + move |_, _, cx: &mut App| { 2963 + let rt = cx 2964 + .global::<Controller>() 2965 + .rt(); 2966 + let pid = pl_id_play.clone(); 2967 + if pl_is_smart { 2968 + rt.spawn( 2969 + crate::client::play_smart_playlist(pid), 2970 + ); 2971 + } else { 2972 + rt.spawn( 2973 + crate::client::play_saved_playlist(pid), 2974 + ); 2975 + } 2976 + }, 2977 + ) 2978 + }) 2979 + .child( 2980 + Icon::new(Icons::Play) 2981 + .size_4(), 2982 + ) 2983 + .child( 2984 + div() 2985 + .text_sm() 2986 + .font_weight( 2987 + FontWeight(600.0), 2988 + ) 2989 + .child("Play"), 2990 + ), 2991 + ) 2992 + .child( 2993 + div() 2994 + .id("pl_shuffle_btn") 2995 + .flex() 2996 + .items_center() 2997 + .gap_x_2() 2998 + .px_4() 2999 + .py_2() 3000 + .rounded_md() 3001 + .bg(theme.player_icons_bg_active) 3002 + .text_color(theme.library_text) 3003 + .when(n_pl_tracks == 0, |this| { 3004 + this.opacity(0.5).cursor_default() 3005 + }) 3006 + .when(n_pl_tracks > 0, |this| { 3007 + this.cursor_pointer() 3008 + .hover(|s| s.bg(theme.player_icons_bg_hover)) 3009 + .on_click({ 3010 + let pid = pl_id_shuffled.clone(); 3011 + move |_, _, cx: &mut App| { 3012 + let rt = cx.global::<Controller>().rt(); 3013 + rt.spawn(crate::client::play_saved_playlist_shuffled(pid.clone())); 3014 + } 3015 + }) 3016 + }) 3017 + .child(Icon::new(Icons::Shuffle).size_4()) 3018 + .child( 3019 + div() 3020 + .text_sm() 3021 + .font_weight(FontWeight(500.0)) 3022 + .child("Shuffle"), 3023 + ), 3024 + ), 3025 + ), 3026 + ), 3027 + ), 3028 + ) 3029 + // ── Column headers ────────────────────────────── 3030 + .child( 3031 + div() 3032 + .w_full() 3033 + .flex_shrink_0() 3034 + .flex() 3035 + .items_center() 3036 + .gap_x_4() 3037 + .px_6() 3038 + .py_4() 3039 + .border_b_1() 3040 + .border_color(theme.library_table_border) 3041 + .child( 3042 + div() 3043 + .w(px(28.0)) 3044 + .flex_shrink_0() 3045 + .text_xs() 3046 + .font_weight(FontWeight::MEDIUM) 3047 + .text_color(theme.library_header_text) 3048 + .child("#"), 3049 + ) 3050 + .child( 3051 + div() 3052 + .flex_1() 3053 + .min_w_0() 3054 + .text_xs() 3055 + .font_weight(FontWeight::MEDIUM) 3056 + .text_color(theme.library_header_text) 3057 + .child("TITLE"), 3058 + ) 3059 + .child( 3060 + div() 3061 + .w_40() 3062 + .overflow_hidden() 3063 + .text_xs() 3064 + .font_weight(FontWeight::MEDIUM) 3065 + .text_color(theme.library_header_text) 3066 + .child("ARTIST"), 3067 + ) 3068 + .child( 3069 + div() 3070 + .w_40() 3071 + .min_w_0() 3072 + .overflow_hidden() 3073 + .text_xs() 3074 + .font_weight(FontWeight::MEDIUM) 3075 + .text_color(theme.library_header_text) 3076 + .child("ALBUM"), 3077 + ) 3078 + .child( 3079 + div() 3080 + .w(px(56.0)) 3081 + .flex_shrink_0() 3082 + .text_xs() 3083 + .font_weight(FontWeight::MEDIUM) 3084 + .text_color(theme.library_header_text) 3085 + .child("TIME"), 3086 + ) 3087 + .child(div().w(px(28.0)).flex_shrink_0()) 3088 + .child(div().w(px(28.0)).flex_shrink_0()) 3089 + .when(!pl_is_smart, |this| { 3090 + this.child(div().w(px(28.0)).flex_shrink_0()) 3091 + }), 3092 + ) 3093 + // ── Track rows ────────────────────────────── 3094 + .children( 3095 + playlist_tracks 3096 + .into_iter() 3097 + .enumerate() 3098 + .map(|(i, t)| { 3099 + let is_current = current_idx 3100 + == cx 3101 + .global::<Controller>() 3102 + .state 3103 + .read(cx) 3104 + .tracks 3105 + .iter() 3106 + .position(|tr| tr.path == t.path); 3107 + let is_liked = liked_songs.contains(&t.id); 3108 + let row_artist = t.artist.clone(); 3109 + let row_album = t.album.clone(); 3110 + track_row( 3111 + ("pl_detail_row", i), 3112 + t.path, 3113 + (i + 1).to_string(), 3114 + t.title, 3115 + Some(t.artist), 3116 + Some(t.album), 3117 + t.duration, 3118 + is_current, 3119 + is_liked, 3120 + row_artist, 3121 + row_album, 3122 + t.id, 3123 + t.album_art, 3124 + if pl_is_smart { 3125 + None 3126 + } else { 3127 + Some(selected_playlist_id_for_remove.clone()) 3128 + }, 3129 + ) 3130 + }), 3131 + ), 3132 + ) 3133 + .into_any_element() 3134 + } 2093 3135 }; 2094 3136 content_inner 2095 3137 }; // end if/else search ··· 2110 3152 // Sidebar 2111 3153 .child( 2112 3154 div() 3155 + .id("sidebar_scroll") 2113 3156 .w(px(200.0)) 2114 3157 .h_full() 2115 3158 .flex_shrink_0() 2116 3159 .flex() 2117 3160 .flex_col() 3161 + .overflow_y_scroll() 2118 3162 .border_r_1() 2119 3163 .border_color(theme.library_table_border) 2120 3164 .pt_4() ··· 2149 3193 4, 2150 3194 "Files", 2151 3195 LibrarySection::Files, 2152 - )), 3196 + )) 3197 + // ── Playlists sidebar section ────────────────── 3198 + .child( 3199 + div() 3200 + .w_full() 3201 + .flex() 3202 + .flex_col() 3203 + .child( 3204 + // Section header row: label navigates, chevron toggles collapse 3205 + div() 3206 + .id("playlists_sidebar_header") 3207 + .w_full() 3208 + .flex() 3209 + .items_center() 3210 + .justify_between() 3211 + .px_4() 3212 + .py_2p5() 3213 + .cursor_pointer() 3214 + .hover(|this| this.text_color(theme.library_text)) 3215 + .on_click(|_, _, cx: &mut App| { 3216 + // Navigate to the Playlists grid and expand the list. 3217 + *cx.global_mut::<LibrarySection>() = 3218 + LibrarySection::Playlists; 3219 + cx.global_mut::<PlaylistsSidebarCollapsed>().0 = 3220 + false; 3221 + }) 3222 + .child( 3223 + div() 3224 + .flex() 3225 + .items_center() 3226 + .gap_x_2() 3227 + .text_sm() 3228 + .text_color(theme.library_header_text) 3229 + .child( 3230 + // Chevron is its own button — stops propagation 3231 + // so it only toggles collapse without navigating. 3232 + div() 3233 + .id("playlists_collapse_btn") 3234 + .cursor_pointer() 3235 + .on_click(|_, _, cx: &mut App| { 3236 + cx.stop_propagation(); 3237 + let c = cx.global_mut::<PlaylistsSidebarCollapsed>(); 3238 + c.0 = !c.0; 3239 + }) 3240 + .child( 3241 + Icon::new(Icons::ChevronLeft) 3242 + .size_3() 3243 + .rotate(if playlists_collapsed { 3244 + gpui::Radians( 3245 + -std::f32::consts::PI, 3246 + ) 3247 + } else { 3248 + gpui::Radians( 3249 + -std::f32::consts::FRAC_PI_2, 3250 + ) 3251 + }) 3252 + .text_color(theme.library_header_text), 3253 + ), 3254 + ) 3255 + .child( 3256 + Icon::new(Icons::Playlist) 3257 + .size_4() 3258 + .text_color(theme.library_header_text), 3259 + ) 3260 + .child("Playlists"), 3261 + ) 3262 + .child( 3263 + div() 3264 + .id("sidebar_new_playlist_btn") 3265 + .cursor_pointer() 3266 + .text_color(theme.library_header_text) 3267 + .hover(|this| { 3268 + this.text_color(theme.library_text) 3269 + }) 3270 + .on_click(|_, _, cx: &mut App| { 3271 + cx.stop_propagation(); 3272 + cx.global_mut::<CreatePlaylistModal>() 3273 + .open = true; 3274 + }) 3275 + .child(Icon::new(Icons::CirclePlus).size_4()), 3276 + ), 3277 + ) 3278 + // Playlist items (when not collapsed) 3279 + .when(!playlists_collapsed, |this| { 3280 + this.children( 3281 + saved_playlists 3282 + .iter() 3283 + .enumerate() 3284 + .map(|(i, pl)| { 3285 + let pl_id = pl.id.clone(); 3286 + let pl_name = pl.name.clone(); 3287 + let pl_name_label = pl.name.clone(); 3288 + let is_active = section 3289 + == LibrarySection::PlaylistDetail 3290 + && selected_playlist.id == pl.id; 3291 + div() 3292 + .id(("sidebar_pl", i)) 3293 + .w_full() 3294 + .px_4() 3295 + .pl_8() 3296 + .py_2() 3297 + .cursor_pointer() 3298 + .text_xs() 3299 + .font_weight(if is_active { 3300 + FontWeight(600.0) 3301 + } else { 3302 + FontWeight(400.0) 3303 + }) 3304 + .text_color(if is_active { 3305 + gpui::rgb(0xFFFFFF) 3306 + } else { 3307 + theme.library_header_text 3308 + }) 3309 + .when(is_active, |this| { 3310 + this.border_l_2() 3311 + .border_color(theme.switcher_active) 3312 + }) 3313 + .hover(|this| { 3314 + this.text_color(theme.library_text) 3315 + }) 3316 + .on_click(move |_, _, cx: &mut App| { 3317 + *cx.global_mut::<SelectedPlaylist>() = 3318 + SelectedPlaylist { 3319 + id: pl_id.clone(), 3320 + name: pl_name.clone(), 3321 + is_smart: false, 3322 + }; 3323 + cx.global_mut::<PlaylistsState>() 3324 + .playlist_tracks = vec![]; 3325 + *cx.global_mut::<LibrarySection>() = 3326 + LibrarySection::PlaylistDetail; 3327 + }) 3328 + .child( 3329 + div() 3330 + .flex() 3331 + .items_center() 3332 + .gap_x_2() 3333 + .child( 3334 + Icon::new(Icons::Playlist) 3335 + .size_3(), 3336 + ) 3337 + .child( 3338 + div() 3339 + .flex_1() 3340 + .min_w_0() 3341 + .truncate() 3342 + .child(pl_name_label), 3343 + ), 3344 + ) 3345 + }) 3346 + .collect::<Vec<_>>(), 3347 + ) 3348 + .children( 3349 + smart_playlists 3350 + .iter() 3351 + .enumerate() 3352 + .map(|(i, pl)| { 3353 + let pl_id = pl.id.clone(); 3354 + let pl_name = pl.name.clone(); 3355 + let pl_name_label = pl.name.clone(); 3356 + let is_active = section 3357 + == LibrarySection::SmartPlaylistDetail 3358 + && selected_playlist.id == pl.id; 3359 + div() 3360 + .id(("sidebar_smart_pl", i)) 3361 + .w_full() 3362 + .px_4() 3363 + .pl_8() 3364 + .py_2() 3365 + .cursor_pointer() 3366 + .text_xs() 3367 + .font_weight(if is_active { 3368 + FontWeight(600.0) 3369 + } else { 3370 + FontWeight(400.0) 3371 + }) 3372 + .text_color(if is_active { 3373 + gpui::rgb(0xFFFFFF) 3374 + } else { 3375 + theme.library_header_text 3376 + }) 3377 + .when(is_active, |this| { 3378 + this.border_l_2() 3379 + .border_color(theme.switcher_active) 3380 + }) 3381 + .hover(|this| { 3382 + this.text_color(theme.library_text) 3383 + }) 3384 + .on_click(move |_, _, cx: &mut App| { 3385 + *cx.global_mut::<SelectedPlaylist>() = 3386 + SelectedPlaylist { 3387 + id: pl_id.clone(), 3388 + name: pl_name.clone(), 3389 + is_smart: true, 3390 + }; 3391 + cx.global_mut::<PlaylistsState>() 3392 + .playlist_tracks = vec![]; 3393 + *cx.global_mut::<LibrarySection>() = 3394 + LibrarySection::SmartPlaylistDetail; 3395 + }) 3396 + .child( 3397 + div() 3398 + .flex() 3399 + .items_center() 3400 + .gap_x_2() 3401 + .child( 3402 + Icon::new(Icons::MusicList) 3403 + .size_3(), 3404 + ) 3405 + .child( 3406 + div() 3407 + .flex_1() 3408 + .min_w_0() 3409 + .truncate() 3410 + .child(pl_name_label), 3411 + ), 3412 + ) 3413 + }) 3414 + .collect::<Vec<_>>(), 3415 + ) 3416 + }), 3417 + ), 2153 3418 ) 2154 3419 .child(content), 2155 3420 ) ··· 2159 3424 let path_for_last = menu.path.clone(); 2160 3425 let menu_artist = menu.artist.clone(); 2161 3426 let menu_album = menu.album.clone(); 3427 + let menu_track_id = menu.track_id.clone(); 3428 + let menu_track_path = menu.path.clone(); 2162 3429 let header_art_url = menu 2163 3430 .album_art 2164 3431 .as_deref() 2165 3432 .filter(|s| !s.is_empty()) 2166 3433 .map(|id| format!("{COVERS_BASE}{id}")); 2167 - // header ~64px + 4 items × ~33px + borders 3434 + // header ~64px + 6 items × ~33px + separator + borders 2168 3435 let menu_w = px(240.0); 2169 - let menu_h = px(198.0); 3436 + let menu_h = px(264.0); 2170 3437 let margin = px(8.0); 2171 3438 let max_x = viewport.width - menu_w - margin; 2172 3439 let menu_x = if menu.pos.x > max_x { ··· 2277 3544 .cursor_pointer() 2278 3545 .text_color(theme.library_text) 2279 3546 .hover(|this| this.bg(theme.library_track_bg_hover)) 3547 + .on_hover(|hovered, _window, cx: &mut App| { 3548 + if *hovered { 3549 + cx.global_mut::<AddToPlaylistMenuState>().0 = None; 3550 + } 3551 + }) 2280 3552 .on_click(move |_, _, cx: &mut App| { 2281 3553 cx.global::<Controller>() 2282 3554 .insert_track_next(path_for_next.clone()); ··· 2293 3565 .cursor_pointer() 2294 3566 .text_color(theme.library_text) 2295 3567 .hover(|this| this.bg(theme.library_track_bg_hover)) 3568 + .on_hover(|hovered, _window, cx: &mut App| { 3569 + if *hovered { 3570 + cx.global_mut::<AddToPlaylistMenuState>().0 = None; 3571 + } 3572 + }) 2296 3573 .on_click(move |_, _, cx: &mut App| { 2297 3574 cx.global::<Controller>() 2298 3575 .insert_track_last(path_for_last.clone()); ··· 2309 3586 .cursor_pointer() 2310 3587 .text_color(theme.library_text) 2311 3588 .hover(|this| this.bg(theme.library_track_bg_hover)) 3589 + .on_hover(|hovered, _window, cx: &mut App| { 3590 + if *hovered { 3591 + cx.global_mut::<AddToPlaylistMenuState>().0 = None; 3592 + } 3593 + }) 2312 3594 .on_click(move |_, _, cx: &mut App| { 2313 3595 *cx.global_mut::<SelectedArtist>() = 2314 3596 SelectedArtist(menu_artist.clone()); ··· 2327 3609 .cursor_pointer() 2328 3610 .text_color(theme.library_text) 2329 3611 .hover(|this| this.bg(theme.library_track_bg_hover)) 3612 + .on_hover(|hovered, _window, cx: &mut App| { 3613 + if *hovered { 3614 + cx.global_mut::<AddToPlaylistMenuState>().0 = None; 3615 + } 3616 + }) 2330 3617 .on_click(move |_, _, cx: &mut App| { 2331 3618 *cx.global_mut::<SelectedAlbum>() = 2332 3619 SelectedAlbum(menu_album.clone()); ··· 2337 3624 cx.global_mut::<LibraryContextMenuState>().0 = None; 2338 3625 }) 2339 3626 .child("Go to Album"), 3627 + ) 3628 + .child(div().h(px(1.0)).bg(theme.library_table_border).mx_2()) 3629 + .child( 3630 + div() 3631 + .id("ctx_add_to_playlist") 3632 + .px_4() 3633 + .py_2() 3634 + .text_sm() 3635 + .cursor_pointer() 3636 + .text_color(theme.library_text) 3637 + .hover(|this| this.bg(theme.library_track_bg_hover)) 3638 + .on_hover(move |hovered, _window, cx: &mut App| { 3639 + if *hovered { 3640 + // item starts after: header(65) + 4 items(132) + separator(1) 3641 + let item_y = menu_y + px(198.0); 3642 + cx.global_mut::<AddToPlaylistMenuState>().0 = 3643 + Some(crate::ui::components::AddToPlaylistMenu { 3644 + anchor_x: menu_x + menu_w, 3645 + flip_x: menu_x, 3646 + anchor_y: item_y, 3647 + track_path: menu_track_path.clone(), 3648 + track_id: menu_track_id.clone(), 3649 + }); 3650 + } 3651 + }) 3652 + .child( 3653 + div() 3654 + .flex() 3655 + .items_center() 3656 + .justify_between() 3657 + .child("Add to Playlist") 3658 + .child(Icon::new(Icons::ChevronLeft).size_3().rotate( 3659 + gpui::Radians(std::f32::consts::PI), 3660 + )), 3661 + ), 2340 3662 ), 2341 3663 ) 2342 3664 }) 3665 + // ── Add-to-playlist submenu ──────────────────────────────────────────── 3666 + .when_some(add_to_playlist_menu, |this, atpm| { 3667 + let saved_for_menu = saved_playlists.clone(); 3668 + let sub_menu_w = px(220.0); 3669 + let sub_menu_h = px((saved_for_menu.len() as f32 * 33.0 + 50.0).max(80.0)); 3670 + let margin = px(8.0); 3671 + // Flyout to the right of the parent menu; flip left when right edge overflows. 3672 + let sub_x = if atpm.anchor_x + sub_menu_w + margin > viewport.width { 3673 + (atpm.flip_x - sub_menu_w).max(margin) 3674 + } else { 3675 + atpm.anchor_x 3676 + }; 3677 + // Align with the hovered row, clamped to stay within the viewport. 3678 + let sub_y = atpm 3679 + .anchor_y 3680 + .min(viewport.height - sub_menu_h - margin) 3681 + .max(margin); 3682 + this.child( 3683 + div() 3684 + .id("atp_backdrop") 3685 + .absolute() 3686 + .top_0() 3687 + .left_0() 3688 + .size_full() 3689 + .occlude() 3690 + .on_click(|_, _, cx: &mut App| { 3691 + cx.stop_propagation(); 3692 + cx.global_mut::<AddToPlaylistMenuState>().0 = None; 3693 + cx.global_mut::<LibraryContextMenuState>().0 = None; 3694 + }), 3695 + ) 3696 + .child( 3697 + div() 3698 + .absolute() 3699 + .left(sub_x) 3700 + .top(sub_y) 3701 + .w(sub_menu_w) 3702 + .bg(theme.titlebar_bg) 3703 + .border_1() 3704 + .border_color(theme.library_table_border) 3705 + .rounded_md() 3706 + .overflow_hidden() 3707 + .flex() 3708 + .flex_col() 3709 + .child( 3710 + div() 3711 + .px_3() 3712 + .py_2() 3713 + .border_b_1() 3714 + .border_color(theme.library_table_border) 3715 + .text_xs() 3716 + .font_weight(FontWeight(600.0)) 3717 + .text_color(theme.library_header_text) 3718 + .child("ADD TO PLAYLIST"), 3719 + ) 3720 + .child( 3721 + div() 3722 + .id("atp_new") 3723 + .px_4() 3724 + .py_2() 3725 + .text_sm() 3726 + .cursor_pointer() 3727 + .text_color(theme.library_text) 3728 + .hover(|this| this.bg(theme.library_track_bg_hover)) 3729 + .on_click({ 3730 + let pending_id = atpm.track_id.clone(); 3731 + move |_, _, cx: &mut App| { 3732 + cx.global_mut::<AddToPlaylistMenuState>().0 = None; 3733 + cx.global_mut::<LibraryContextMenuState>().0 = None; 3734 + let modal = cx.global_mut::<CreatePlaylistModal>(); 3735 + modal.open = true; 3736 + modal.pending_track_id = Some(pending_id.clone()); 3737 + }}) 3738 + .child( 3739 + div() 3740 + .flex() 3741 + .items_center() 3742 + .gap_x_2() 3743 + .child(Icon::new(Icons::CirclePlus).size_4()) 3744 + .child("New Playlist..."), 3745 + ), 3746 + ) 3747 + .children(saved_for_menu.into_iter().enumerate().map(|(i, pl)| { 3748 + let track_id_for_add = atpm.track_id.clone(); 3749 + let pl_id_for_add = pl.id.clone(); 3750 + div() 3751 + .id(("atp_item", i)) 3752 + .px_4() 3753 + .py_2() 3754 + .text_sm() 3755 + .cursor_pointer() 3756 + .text_color(theme.library_text) 3757 + .hover(|this| this.bg(theme.library_track_bg_hover)) 3758 + .on_click(move |_, _, cx: &mut App| { 3759 + let tokio = 3760 + cx.global::<crate::state::TokioHandle>().0.clone(); 3761 + let tid = track_id_for_add.clone(); 3762 + let pid = pl_id_for_add.clone(); 3763 + cx.global_mut::<AddToPlaylistMenuState>().0 = None; 3764 + cx.global_mut::<LibraryContextMenuState>().0 = None; 3765 + cx.spawn(async move |cx| { 3766 + let saved = cx 3767 + .background_executor() 3768 + .spawn(async move { 3769 + tokio.block_on(async move { 3770 + let _ = crate::client::add_track_to_playlist( 3771 + pid, tid, 3772 + ) 3773 + .await; 3774 + crate::client::fetch_saved_playlists() 3775 + .await 3776 + .ok() 3777 + }) 3778 + }) 3779 + .await; 3780 + if let Some(saved) = saved { 3781 + let _ = cx.update(|app: &mut gpui::App| { 3782 + app.global_mut::<PlaylistsState>().saved = saved; 3783 + }); 3784 + } 3785 + }) 3786 + .detach(); 3787 + }) 3788 + .child(pl.name.clone()) 3789 + })), 3790 + ) 3791 + }) 2343 3792 .when_some(album_context_menu, |this, menu| { 2344 3793 let album_id_play = menu.album_id.clone(); 2345 3794 let album_id_shuffled = menu.album_id.clone(); ··· 2749 4198 }, 2750 4199 )) 2751 4200 }), 4201 + ) 4202 + }) 4203 + // ── Create Playlist Modal ─────────────────────────────────────────────��� 4204 + .when(create_modal.open, |this| { 4205 + this.child( 4206 + // Backdrop 4207 + div() 4208 + .id("modal_backdrop") 4209 + .absolute() 4210 + .top_0() 4211 + .left_0() 4212 + .size_full() 4213 + .bg(gpui::rgba(0x00000099)) 4214 + .occlude() 4215 + .on_click(|_, _, cx: &mut App| { 4216 + cx.global_mut::<CreatePlaylistModal>().open = false; 4217 + }), 4218 + ) 4219 + .child( 4220 + // Modal card — centred 4221 + div() 4222 + .id("create_pl_modal_card") 4223 + .absolute() 4224 + .top(viewport.height * 0.25) 4225 + .left((viewport.width - px(380.0)) / 2.0) 4226 + .w(px(380.0)) 4227 + .bg(theme.titlebar_bg) 4228 + .border_1() 4229 + .border_color(theme.library_table_border) 4230 + .rounded_lg() 4231 + .p_6() 4232 + .flex() 4233 + .flex_col() 4234 + .gap_y_4() 4235 + .on_click(|_, _, cx: &mut App| cx.stop_propagation()) 4236 + // Title 4237 + .child( 4238 + div() 4239 + .text_lg() 4240 + .font_weight(FontWeight(700.0)) 4241 + .text_color(theme.library_text) 4242 + .child("New Playlist"), 4243 + ) 4244 + // Name field 4245 + .child( 4246 + div() 4247 + .flex() 4248 + .flex_col() 4249 + .gap_y_1() 4250 + .child( 4251 + div() 4252 + .text_sm() 4253 + .text_color(theme.library_header_text) 4254 + .child("Title"), 4255 + ) 4256 + .child(modal_name_input.clone()), 4257 + ) 4258 + // Description field 4259 + .child( 4260 + div() 4261 + .flex() 4262 + .flex_col() 4263 + .gap_y_1() 4264 + .child( 4265 + div() 4266 + .text_sm() 4267 + .text_color(theme.library_header_text) 4268 + .child("Description (optional)"), 4269 + ) 4270 + .child(modal_desc_input.clone()), 4271 + ) 4272 + // Buttons 4273 + .child( 4274 + div() 4275 + .flex() 4276 + .items_center() 4277 + .justify_end() 4278 + .gap_x_3() 4279 + .child( 4280 + div() 4281 + .id("modal_cancel_btn") 4282 + .px_4() 4283 + .py_2() 4284 + .rounded_md() 4285 + .cursor_pointer() 4286 + .text_sm() 4287 + .text_color(theme.library_header_text) 4288 + .hover(|this| this.text_color(theme.library_text)) 4289 + .on_click(|_, _, cx: &mut App| { 4290 + let modal = cx.global_mut::<CreatePlaylistModal>(); 4291 + modal.open = false; 4292 + modal.pending_track_id = None; 4293 + }) 4294 + .child("Cancel"), 4295 + ) 4296 + .child( 4297 + div() 4298 + .id("modal_create_btn") 4299 + .px_4() 4300 + .py_2() 4301 + .rounded_md() 4302 + .cursor_pointer() 4303 + .text_sm() 4304 + .font_weight(FontWeight(600.0)) 4305 + .bg(theme.player_play_pause_bg) 4306 + .text_color(theme.player_play_pause_text) 4307 + .hover(|this| this.bg(theme.player_play_pause_hover)) 4308 + .on_click({ 4309 + let modal_name_input = modal_name_input.clone(); 4310 + let modal_desc_input = modal_desc_input.clone(); 4311 + move |_, _, cx: &mut App| { 4312 + let name = modal_name_value.clone(); 4313 + if name.trim().is_empty() { 4314 + return; 4315 + } 4316 + let desc = if modal_desc_value.is_empty() { 4317 + None 4318 + } else { 4319 + Some(modal_desc_value.clone()) 4320 + }; 4321 + let modal = cx.global_mut::<CreatePlaylistModal>(); 4322 + modal.open = false; 4323 + let pending_id = modal.pending_track_id.take(); 4324 + // Clear inputs 4325 + modal_name_input.update(cx, |i, cx| { 4326 + i.value.clear(); 4327 + cx.notify(); 4328 + }); 4329 + modal_desc_input.update(cx, |i, cx| { 4330 + i.value.clear(); 4331 + cx.notify(); 4332 + }); 4333 + let track_ids = pending_id 4334 + .map(|id| vec![id]) 4335 + .unwrap_or_default(); 4336 + let tokio = cx.global::<crate::state::TokioHandle>().0.clone(); 4337 + cx.spawn(async move |cx| { 4338 + let saved = cx 4339 + .background_executor() 4340 + .spawn(async move { 4341 + tokio.block_on(async move { 4342 + let _ = crate::client::create_saved_playlist(name, desc, track_ids).await; 4343 + crate::client::fetch_saved_playlists().await.ok() 4344 + }) 4345 + }) 4346 + .await; 4347 + if let Some(saved) = saved { 4348 + let _ = cx.update(|app: &mut gpui::App| { 4349 + app.global_mut::<PlaylistsState>().saved = saved; 4350 + }); 4351 + } 4352 + }) 4353 + .detach(); 4354 + }}) 4355 + .child("Create"), 4356 + ), 4357 + ), 4358 + ) 4359 + }) 4360 + // ── Edit Playlist Modal ─────────────────────────────────────────────── 4361 + .when(edit_modal.open, |this| { 4362 + let edit_id = edit_modal.id.clone(); 4363 + this.child( 4364 + div() 4365 + .id("edit_modal_backdrop") 4366 + .absolute() 4367 + .top_0() 4368 + .left_0() 4369 + .size_full() 4370 + .bg(gpui::rgba(0x00000099)) 4371 + .occlude() 4372 + .on_click(|_, _, cx: &mut App| { 4373 + cx.global_mut::<EditPlaylistModal>().open = false; 4374 + }), 4375 + ) 4376 + .child( 4377 + div() 4378 + .id("edit_pl_modal_card") 4379 + .absolute() 4380 + .top(viewport.height * 0.25) 4381 + .left((viewport.width - px(380.0)) / 2.0) 4382 + .w(px(380.0)) 4383 + .bg(theme.titlebar_bg) 4384 + .border_1() 4385 + .border_color(theme.library_table_border) 4386 + .rounded_lg() 4387 + .p_6() 4388 + .flex() 4389 + .flex_col() 4390 + .gap_y_4() 4391 + .on_click(|_, _, cx: &mut App| cx.stop_propagation()) 4392 + .child( 4393 + div() 4394 + .text_lg() 4395 + .font_weight(FontWeight(700.0)) 4396 + .text_color(theme.library_text) 4397 + .child("Edit Playlist"), 4398 + ) 4399 + .child( 4400 + div() 4401 + .flex() 4402 + .flex_col() 4403 + .gap_y_1() 4404 + .child( 4405 + div() 4406 + .text_sm() 4407 + .text_color(theme.library_header_text) 4408 + .child("Title"), 4409 + ) 4410 + .child(edit_name_input.clone()), 4411 + ) 4412 + .child( 4413 + div() 4414 + .flex() 4415 + .flex_col() 4416 + .gap_y_1() 4417 + .child( 4418 + div() 4419 + .text_sm() 4420 + .text_color(theme.library_header_text) 4421 + .child("Description (optional)"), 4422 + ) 4423 + .child(edit_desc_input.clone()), 4424 + ) 4425 + .child( 4426 + div() 4427 + .flex() 4428 + .items_center() 4429 + .justify_end() 4430 + .gap_x_3() 4431 + .child( 4432 + div() 4433 + .id("edit_modal_cancel_btn") 4434 + .px_4() 4435 + .py_2() 4436 + .rounded_md() 4437 + .cursor_pointer() 4438 + .text_sm() 4439 + .text_color(theme.library_header_text) 4440 + .hover(|this| this.text_color(theme.library_text)) 4441 + .on_click(|_, _, cx: &mut App| { 4442 + cx.global_mut::<EditPlaylistModal>().open = false; 4443 + }) 4444 + .child("Cancel"), 4445 + ) 4446 + .child( 4447 + div() 4448 + .id("edit_modal_save_btn") 4449 + .px_4() 4450 + .py_2() 4451 + .rounded_md() 4452 + .cursor_pointer() 4453 + .text_sm() 4454 + .font_weight(FontWeight(600.0)) 4455 + .bg(theme.player_play_pause_bg) 4456 + .text_color(theme.player_play_pause_text) 4457 + .hover(|this| this.bg(theme.player_play_pause_hover)) 4458 + .on_click({ 4459 + let edit_name_input2 = edit_name_input.clone(); 4460 + let edit_desc_input2 = edit_desc_input.clone(); 4461 + move |_, _, cx: &mut App| { 4462 + let name = edit_name_value.clone(); 4463 + if name.trim().is_empty() { 4464 + return; 4465 + } 4466 + let desc = if edit_desc_value.is_empty() { 4467 + None 4468 + } else { 4469 + Some(edit_desc_value.clone()) 4470 + }; 4471 + let id = edit_id.clone(); 4472 + cx.global_mut::<EditPlaylistModal>().open = false; 4473 + edit_name_input2.update(cx, |i, cx| { 4474 + i.value.clear(); 4475 + cx.notify(); 4476 + }); 4477 + edit_desc_input2.update(cx, |i, cx| { 4478 + i.value.clear(); 4479 + cx.notify(); 4480 + }); 4481 + let tokio = 4482 + cx.global::<crate::state::TokioHandle>() 4483 + .0 4484 + .clone(); 4485 + cx.spawn(async move |cx| { 4486 + let saved = cx 4487 + .background_executor() 4488 + .spawn(async move { 4489 + tokio.block_on(async move { 4490 + let _ = crate::client::update_saved_playlist(id, name, desc).await; 4491 + crate::client::fetch_saved_playlists().await.ok() 4492 + }) 4493 + }) 4494 + .await; 4495 + if let Some(saved) = saved { 4496 + let _ = cx.update(|app: &mut gpui::App| { 4497 + app.global_mut::<PlaylistsState>() 4498 + .saved = saved; 4499 + }); 4500 + } 4501 + }) 4502 + .detach(); 4503 + } 4504 + }) 4505 + .child("Save"), 4506 + ), 4507 + ), 4508 + ) 4509 + }) 4510 + // ── Delete Playlist Modal ───────────────────────────────────────────── 4511 + .when(delete_modal.open, |this| { 4512 + let del_id = delete_modal.id.clone(); 4513 + let del_name = delete_modal.name.clone(); 4514 + let del_name_msg = delete_modal.name.clone(); 4515 + this.child( 4516 + div() 4517 + .id("delete_modal_backdrop") 4518 + .absolute() 4519 + .top_0() 4520 + .left_0() 4521 + .size_full() 4522 + .bg(gpui::rgba(0x00000099)) 4523 + .occlude() 4524 + .on_click(|_, _, cx: &mut App| { 4525 + cx.global_mut::<DeletePlaylistModal>().open = false; 4526 + }), 4527 + ) 4528 + .child( 4529 + div() 4530 + .id("delete_pl_modal_card") 4531 + .absolute() 4532 + .top(viewport.height * 0.3) 4533 + .left((viewport.width - px(360.0)) / 2.0) 4534 + .w(px(360.0)) 4535 + .bg(theme.titlebar_bg) 4536 + .border_1() 4537 + .border_color(theme.library_table_border) 4538 + .rounded_lg() 4539 + .p_6() 4540 + .flex() 4541 + .flex_col() 4542 + .gap_y_4() 4543 + .on_click(|_, _, cx: &mut App| cx.stop_propagation()) 4544 + .child( 4545 + div() 4546 + .text_lg() 4547 + .font_weight(FontWeight(700.0)) 4548 + .text_color(theme.library_text) 4549 + .child("Delete Playlist"), 4550 + ) 4551 + .child( 4552 + div() 4553 + .text_sm() 4554 + .text_color(theme.library_header_text) 4555 + .child(format!( 4556 + "{} will be permanently deleted.", 4557 + del_name_msg 4558 + )), 4559 + ) 4560 + .child( 4561 + div() 4562 + .flex() 4563 + .items_center() 4564 + .justify_end() 4565 + .gap_x_3() 4566 + .child( 4567 + div() 4568 + .id("delete_modal_cancel_btn") 4569 + .px_4() 4570 + .py_2() 4571 + .rounded_md() 4572 + .cursor_pointer() 4573 + .text_sm() 4574 + .text_color(theme.library_header_text) 4575 + .hover(|this| this.text_color(theme.library_text)) 4576 + .on_click(|_, _, cx: &mut App| { 4577 + cx.global_mut::<DeletePlaylistModal>().open = false; 4578 + }) 4579 + .child("Cancel"), 4580 + ) 4581 + .child( 4582 + div() 4583 + .id("delete_modal_delete_btn") 4584 + .px_4() 4585 + .py_2() 4586 + .rounded_md() 4587 + .cursor_pointer() 4588 + .text_sm() 4589 + .font_weight(FontWeight(600.0)) 4590 + .bg(gpui::rgb(0xef4444)) 4591 + .text_color(gpui::rgb(0xFFFFFF)) 4592 + .hover(|this| this.bg(gpui::rgb(0xdc2626))) 4593 + .on_click(move |_, _, cx: &mut App| { 4594 + let id = del_id.clone(); 4595 + let name = del_name.clone(); 4596 + cx.global_mut::<DeletePlaylistModal>().open = false; 4597 + // If currently viewing this playlist, navigate back 4598 + let cur_id = 4599 + cx.global::<SelectedPlaylist>().id.clone(); 4600 + if cur_id == id { 4601 + *cx.global_mut::<LibrarySection>() = 4602 + LibrarySection::Playlists; 4603 + } 4604 + let tokio = 4605 + cx.global::<crate::state::TokioHandle>() 4606 + .0 4607 + .clone(); 4608 + let _ = name; 4609 + cx.spawn(async move |cx| { 4610 + let saved = cx 4611 + .background_executor() 4612 + .spawn(async move { 4613 + tokio.block_on(async move { 4614 + let _ = crate::client::delete_saved_playlist(id).await; 4615 + crate::client::fetch_saved_playlists().await.ok() 4616 + }) 4617 + }) 4618 + .await; 4619 + if let Some(saved) = saved { 4620 + let _ = cx.update(|app: &mut gpui::App| { 4621 + app.global_mut::<PlaylistsState>() 4622 + .saved = saved; 4623 + }); 4624 + } 4625 + }) 4626 + .detach(); 4627 + }) 4628 + .child("Delete"), 4629 + ), 4630 + ), 2752 4631 ) 2753 4632 }) 2754 4633 }
+92
gpui/src/ui/components/text_input.rs
··· 1 + use crate::ui::theme::Theme; 2 + use gpui::{ 3 + div, App, Context, FocusHandle, InteractiveElement, IntoElement, KeyDownEvent, ParentElement, 4 + Render, StatefulInteractiveElement, Styled, Subscription, Window, 5 + }; 6 + 7 + pub struct TextInput { 8 + pub value: String, 9 + pub placeholder: String, 10 + pub focus_handle: FocusHandle, 11 + _focus_out_sub: Option<Subscription>, 12 + } 13 + 14 + impl TextInput { 15 + pub fn new(placeholder: impl Into<String>, cx: &mut App) -> Self { 16 + TextInput { 17 + value: String::new(), 18 + placeholder: placeholder.into(), 19 + focus_handle: cx.focus_handle(), 20 + _focus_out_sub: None, 21 + } 22 + } 23 + } 24 + 25 + impl Render for TextInput { 26 + fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { 27 + let theme = *cx.global::<Theme>(); 28 + let is_focused = self.focus_handle.is_focused(window); 29 + 30 + if self._focus_out_sub.is_none() { 31 + let handle = self.focus_handle.clone(); 32 + self._focus_out_sub = Some(cx.on_focus_out(&handle, window, |_, _, _, cx| { 33 + cx.notify(); 34 + })); 35 + } 36 + 37 + let display = if self.value.is_empty() && !is_focused { 38 + self.placeholder.clone() 39 + } else { 40 + self.value.clone() 41 + }; 42 + let text_color = if self.value.is_empty() && !is_focused { 43 + theme.library_header_text 44 + } else { 45 + theme.library_text 46 + }; 47 + 48 + div() 49 + .id("text_input_box") 50 + .key_context("TextInput") 51 + .track_focus(&self.focus_handle) 52 + .on_click(cx.listener(|this, _, window, _cx| { 53 + window.focus(&this.focus_handle); 54 + })) 55 + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { 56 + let key = event.keystroke.key.as_str(); 57 + if key == "backspace" { 58 + this.value.pop(); 59 + cx.notify(); 60 + } else if key == "escape" { 61 + this.value.clear(); 62 + cx.notify(); 63 + } else if !event.keystroke.modifiers.platform && !event.keystroke.modifiers.control 64 + { 65 + if let Some(c) = &event.keystroke.key_char { 66 + this.value.push_str(c); 67 + cx.notify(); 68 + } 69 + } 70 + })) 71 + .px_3() 72 + .py_2() 73 + .rounded_lg() 74 + .bg(theme.switcher_bg) 75 + .border_1() 76 + .border_color(if is_focused { 77 + theme.switcher_active 78 + } else { 79 + theme.border 80 + }) 81 + .flex() 82 + .items_center() 83 + .cursor_pointer() 84 + .text_sm() 85 + .text_color(text_color) 86 + .child(if is_focused { 87 + format!("{display}|") 88 + } else { 89 + display 90 + }) 91 + } 92 + }
+1 -1
gpui/src/ui/global_keybinds.rs
··· 9 9 10 10 pub fn register_keybinds(cx: &mut App) { 11 11 cx.bind_keys([ 12 - KeyBinding::new("space", PlayPause, Some("! SearchInput")), 12 + KeyBinding::new("space", PlayPause, Some("Rockbox && !SearchInput && !TextInput")), 13 13 KeyBinding::new("cmd-right", Next, None), 14 14 KeyBinding::new("cmd-left", Prev, None), 15 15 KeyBinding::new("shift-s", Shuffle, None),
+14 -2
gpui/src/ui/rockbox.rs
··· 7 7 use crate::ui::theme::Theme; 8 8 use gpui::prelude::FluentBuilder; 9 9 use gpui::{ 10 - div, px, Animation, AnimationExt as _, AppContext, Context, ElementId, Entity, 11 - InteractiveElement, IntoElement, ParentElement, Render, Styled, Window, 10 + div, px, Animation, AnimationExt as _, AppContext, Context, ElementId, Entity, FocusHandle, 11 + InteractiveElement, IntoElement, ParentElement, Render, Styled, 12 + Window, 12 13 }; 13 14 14 15 pub struct Rockbox { 16 + pub focus_handle: FocusHandle, 15 17 pub titlebar: Entity<Titlebar>, 16 18 pub player_page: Entity<PlayerPage>, 17 19 pub library_page: Entity<LibraryPage>, ··· 29 31 let library_page = cx.new(|cx| LibraryPage::new(cx)); 30 32 let queue_page = cx.new(|cx| QueuePage::new(cx)); 31 33 Rockbox { 34 + focus_handle: cx.focus_handle(), 32 35 titlebar, 33 36 player_page, 34 37 library_page, ··· 39 42 40 43 impl Render for Rockbox { 41 44 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { 45 + // When no other element has keyboard focus, grab it so the "Rockbox" 46 + // key context is always in the dispatch path and global bindings 47 + // (e.g. space → PlayPause) can fire. 48 + if window.focused(cx).is_none() { 49 + window.focus(&self.focus_handle); 50 + } 51 + 42 52 let theme = *cx.global::<Theme>(); 43 53 let page = *cx.global::<Page>(); 44 54 let page_state = window.use_keyed_state("page_transition", cx, |_, _| page); ··· 67 77 }; 68 78 div() 69 79 .id("root") 80 + .key_context("Rockbox") 81 + .track_focus(&self.focus_handle) 70 82 .size_full() 71 83 .font_family("Space Grotesk") 72 84 .relative()
+6 -1
zig/build.zig
··· 88 88 }); 89 89 90 90 if (target.result.os.tag == .macos) { 91 - exe.root_module.addLibraryPath(.{ .cwd_relative = "/opt/homebrew/lib" }); 91 + // Homebrew path differs by architecture: /opt/homebrew on aarch64, /usr/local on x86_64 92 + if (target.result.cpu.arch == .aarch64) { 93 + exe.root_module.addLibraryPath(.{ .cwd_relative = "/opt/homebrew/lib" }); 94 + } else { 95 + exe.root_module.addLibraryPath(.{ .cwd_relative = "/usr/local/lib" }); 96 + } 92 97 exe.root_module.linkFramework("CoreFoundation", .{}); 93 98 } 94 99