From 35139b84a95ce8831fd9358a2b4db1c133a019ce Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Wed, 20 Nov 2024 20:01:31 +0100 Subject: [PATCH] progress on integrated player --- Cargo.lock | 209 ++++++++++++++++++++++++++++++++++-- Cargo.toml | 22 +++- flake.lock | 28 ++--- src/main.rs | 88 ++++++++------- src/player.rs | 193 +++++++++++++++++++++++++++++++++ src/widgets/mod.rs | 5 +- src/widgets/playback/bar.rs | 95 ++++++++++++++++ src/widgets/playback/mod.rs | 1 + 8 files changed, 570 insertions(+), 71 deletions(-) create mode 100644 src/player.rs create mode 100644 src/widgets/playback/bar.rs create mode 100644 src/widgets/playback/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f620158..04d391d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7a3dc3ad32931b2d6e97c99a702208dfd1e2c446580e5f99d1d8355df26db6" +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.6.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -98,7 +120,7 @@ dependencies = [ "jni-sys", "libc", "log", - "ndk", + "ndk 0.9.0", "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", @@ -542,7 +564,7 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad3a619a9de81e1d7de1f1186dcba4506ed661a0e483d84410fdef0ee87b2f96" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -608,7 +630,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.10.5", "lazy_static", "lazycell", "log", @@ -622,6 +644,24 @@ dependencies = [ "which", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.10.5", + "proc-macro2", + "quote 1.0.37", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.87", +] + [[package]] name = "bit-set" version = "0.6.0" @@ -1079,6 +1119,26 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" +dependencies = [ + "bindgen 0.70.1", +] + [[package]] name = "cosmic-text" version = "0.12.1" @@ -1102,6 +1162,29 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "cpufeatures" version = "0.2.15" @@ -1309,6 +1392,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "data-encoding" version = "2.6.0" @@ -1412,7 +1501,9 @@ dependencies = [ "http-cache-reqwest", "image", "itertools 0.10.5", + "librespot-connect", "librespot-core", + "librespot-metadata", "librespot-oauth", "librespot-playback", "librespot-protocol", @@ -2985,7 +3076,7 @@ dependencies = [ [[package]] name = "librespot-audio" version = "0.5.0" -source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#1b9192b52a983f5aca2c145621393ac0d12ace1a" +source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#7be55c96ec36f944bb7dc0799b6697c874d59498" dependencies = [ "aes", "bytes", @@ -3002,10 +3093,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "librespot-connect" +version = "0.5.0" +source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#7be55c96ec36f944bb7dc0799b6697c874d59498" +dependencies = [ + "form_urlencoded", + "futures-util", + "librespot-core", + "librespot-playback", + "librespot-protocol", + "log", + "protobuf", + "rand", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", +] + [[package]] name = "librespot-core" version = "0.5.0" -source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#1b9192b52a983f5aca2c145621393ac0d12ace1a" +source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#7be55c96ec36f944bb7dc0799b6697c874d59498" dependencies = [ "aes", "base64 0.22.1", @@ -3062,7 +3173,7 @@ dependencies = [ [[package]] name = "librespot-metadata" version = "0.5.0" -source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#1b9192b52a983f5aca2c145621393ac0d12ace1a" +source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#7be55c96ec36f944bb7dc0799b6697c874d59498" dependencies = [ "async-trait", "bytes", @@ -3079,7 +3190,7 @@ dependencies = [ [[package]] name = "librespot-oauth" version = "0.5.0" -source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#1b9192b52a983f5aca2c145621393ac0d12ace1a" +source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#7be55c96ec36f944bb7dc0799b6697c874d59498" dependencies = [ "log", "oauth2", @@ -3090,8 +3201,9 @@ dependencies = [ [[package]] name = "librespot-playback" version = "0.5.0" -source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#1b9192b52a983f5aca2c145621393ac0d12ace1a" +source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#7be55c96ec36f944bb7dc0799b6697c874d59498" dependencies = [ + "cpal", "futures-util", "librespot-audio", "librespot-core", @@ -3100,6 +3212,7 @@ dependencies = [ "parking_lot", "rand", "rand_distr", + "rodio", "shell-words", "symphonia", "thiserror", @@ -3110,7 +3223,7 @@ dependencies = [ [[package]] name = "librespot-protocol" version = "0.5.0" -source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#1b9192b52a983f5aca2c145621393ac0d12ace1a" +source = "git+https://github.com/photovoltex/librespot.git?branch=integrate-dealer#7be55c96ec36f944bb7dc0799b6697c874d59498" dependencies = [ "protobuf", "protobuf-codegen", @@ -3191,6 +3304,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -3415,6 +3537,20 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror", +] + [[package]] name = "ndk" version = "0.9.0" @@ -3883,6 +4019,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -4808,6 +4967,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rodio" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +dependencies = [ + "cpal", + "thiserror", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -6802,6 +6971,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.57.0" @@ -6831,6 +7010,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -7175,7 +7364,7 @@ dependencies = [ "js-sys", "libc", "memmap2 0.9.5", - "ndk", + "ndk 0.9.0", "objc2", "objc2-app-kit", "objc2-foundation", diff --git a/Cargo.toml b/Cargo.toml index 0d59e3f..7ebb546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,13 +6,18 @@ edition = "2021" [dependencies] # cushy = { version = "0.4.0", features=["tokio", "tokio-multi-thread", "plotters", "roboto-flex"], default-features = false } -cushy = { git = "https://github.com/khonsulabs/cushy.git", branch = "main", features=["tokio", "tokio-multi-thread", "plotters", "roboto-flex"] } +cushy = { git = "https://github.com/khonsulabs/cushy.git", branch = "main", features = [ + "tokio", + "tokio-multi-thread", + "plotters", + "roboto-flex", +] } tokio = { version = "1.40.0", features = ["rt", "rt-multi-thread"] } plotters = { version = "0.3.7", default-features = false } image = { version = "0.25.0", features = ["png"] } mpris = "2.0.1" reqwest = "0.12.8" -reqwest-middleware="0.3.3" +reqwest-middleware = "0.3.3" http-cache-reqwest = "0.14.0" color_quant = "1.0" hsl = "0.1.1" @@ -22,9 +27,18 @@ clap = { version = "4.5.20", features = ["derive"] } chrono = "0.4" librespot-core = { git = "https://github.com/photovoltex/librespot.git", branch = "integrate-dealer" } librespot-oauth = { git = "https://github.com/photovoltex/librespot.git", branch = "integrate-dealer" } -librespot-playback = { git = "https://github.com/photovoltex/librespot.git", branch = "integrate-dealer" } +librespot-playback = { git = "https://github.com/photovoltex/librespot.git", branch = "integrate-dealer", features = [ + "rodio-backend", +] } librespot-protocol = { git = "https://github.com/photovoltex/librespot.git", branch = "integrate-dealer" } -futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } +librespot-connect = { git = "https://github.com/photovoltex/librespot.git", branch = "integrate-dealer" } +librespot-metadata = { git = "https://github.com/photovoltex/librespot.git", branch = "integrate-dealer" } +futures-util = { version = "0.3", features = [ + "alloc", + "bilock", + "sink", + "unstable", +] } rspotify = { version = "0.13.3" } oauth2 = "4.4" diff --git a/flake.lock b/flake.lock index 2793f62..c3f5e01 100644 --- a/flake.lock +++ b/flake.lock @@ -105,11 +105,11 @@ "pre-commit-hooks": "pre-commit-hooks_3" }, "locked": { - "lastModified": 1723311214, - "narHash": "sha256-xdGZQBEa1AC2us/sY3igS/CucWY6jErXsAvCFRhB2LI=", + "lastModified": 1732039290, + "narHash": "sha256-LQKY7bShf2H9kJouxa9ZspfdrulnZF9o4kLTqGqCDYM=", "owner": "nix-community", "repo": "crate2nix", - "rev": "236f6addfd452a48be805819e3216af79e988fd5", + "rev": "9ff208ce7f5a482272b1bcefbe363c772d7ff914", "type": "github" }, "original": { @@ -393,11 +393,11 @@ ] }, "locked": { - "lastModified": 1727826117, - "narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=", + "lastModified": 1730504689, + "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1", + "rev": "506278e768c2a08bec68eb62932193e341f55c90", "type": "github" }, "original": { @@ -704,8 +704,8 @@ "nixpkgs_7": { "locked": { "lastModified": 0, - "narHash": "sha256-yumd4fBc/hi8a9QgA9IT8vlQuLZ2oqhkJXHPKxH/tRw=", - "path": "/nix/store/rs4fjbnw4qx7ns2hzzrz2iz52va7vs5z-source", + "narHash": "sha256-mwrFF0vElHJP8X3pFCByJR365Q2463ATp2qGIrDUdlE=", + "path": "/nix/store/dxdcvjnvz3b91gvsrhpb7gp156nnj8bf-source", "type": "path" }, "original": { @@ -715,11 +715,11 @@ }, "nixpkgs_8": { "locked": { - "lastModified": 1718428119, - "narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=", + "lastModified": 1728538411, + "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5", + "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", "type": "github" }, "original": { @@ -843,11 +843,11 @@ "nixpkgs": "nixpkgs_8" }, "locked": { - "lastModified": 1728354625, - "narHash": "sha256-r+Sa1NRRT7LXKzCaVaq75l1GdZcegODtF06uaxVVVbI=", + "lastModified": 1732069891, + "narHash": "sha256-moKx8AVJrViCSdA0e0nSsG8b1dAsObI4sRAtbqbvBY8=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "d216ade5a0091ce60076bf1f8bc816433a1fc5da", + "rev": "8509a51241c407d583b1963d5079585a992506e8", "type": "github" }, "original": { diff --git a/src/main.rs b/src/main.rs index 6c8ac63..01a7d3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::thread; +use std::{sync::Arc, thread}; use api::{SpotifyContext, SpotifyContextRef}; use auth::get_token; @@ -8,19 +8,28 @@ use cushy::{ value::Dynamic, widget::MakeWidget, window::MakeWindow, Application, Open, PendingApp, Run, TokioRuntime, }; +use librespot_connect::{ + spirc::{Spirc, SpircLoadCommand}, + state::ConnectStateConfig, +}; use librespot_core::{authentication::Credentials, Session, SessionConfig}; use librespot_playback::{ audio_backend, config::{AudioFormat, PlayerConfig}, - mixer::NoOpVolume, - player::Player, + mixer::{softmixer::SoftMixer, Mixer, MixerConfig, NoOpVolume}, + player::{Player, PlayerEvent}, +}; +use player::{new_dynamic_player, DynamicPlayer, DynamicPlayerInner}; +use widgets::{ + library::playlist::playlists_widget, pages::liked::LikedSongsPage, playback::bar::bar, + ActivePage, }; -use widgets::{library::playlist::playlists_widget, pages::liked::LikedSongsPage, ActivePage}; mod api; mod auth; mod cli; mod nodebug; +mod player; mod rt; mod theme; mod vibrancy; @@ -28,7 +37,7 @@ mod widgets; fn main() -> cushy::Result { let args = Args::parse(); - let mut app = PendingApp::new(TokioRuntime::default()); + let app = PendingApp::new(TokioRuntime::default()); let token = get_token().unwrap(); @@ -36,6 +45,7 @@ fn main() -> cushy::Result { let player_config = PlayerConfig::default(); let audio_format = AudioFormat::default(); let credentials = Credentials::with_access_token(&token.access_token); + let connect_config = ConnectStateConfig::default(); let backend = audio_backend::find(None).unwrap(); let session; @@ -44,6 +54,8 @@ fn main() -> cushy::Result { let guard = app.cushy().enter_runtime(); session = Session::new(session_config, None); + dbg!(session.user_data()); + let player = Player::new( player_config, session.clone(), @@ -51,48 +63,42 @@ fn main() -> cushy::Result { move || backend(None, audio_format), ); - tokio::spawn({ - let session = session.clone(); - async move { - if let Err(e) = session.connect(credentials, false).await { - println!("Error connecting: {}", e); - } - } - }); - - thread::spawn(move || { - let mut channel = player.get_player_event_channel(); - loop { - let event = channel.blocking_recv(); - if let Some(event) = event { - dbg!(event); - } else { - break; - } - } - }); - - dbg!(session.user_data()); - - let context = SpotifyContextRef::new(SpotifyContext::new(session, token)); + let context = SpotifyContextRef::new(SpotifyContext::new(session.clone(), token)); let mut app = app.as_app(); tokio::spawn(async move { - let user = context.current_user().await.unwrap(); - dbg!(&user); - let userid = user.id; + let (_spirc, spirc_task) = Spirc::new( + connect_config, + session.clone(), + credentials, + player.clone(), + Arc::new(SoftMixer::open(MixerConfig::default())), + ) + .await + .unwrap(); + let dynplayer = new_dynamic_player(player); + // this cannot happen in `{}` inside join for some reason + let dynplayer2 = dynplayer.clone(); + tokio::join!(spirc_task, dynplayer2.run(), async move { + let user = context.current_user().await.unwrap(); + dbg!(&user); + // let userid = user.id; - let playlists = context.current_user_playlists(None, None).await.unwrap(); + let playlists = context.current_user_playlists(None, None).await.unwrap(); - let selected_page = Dynamic::new(ActivePage::default()); + let selected_page = Dynamic::new(ActivePage::default()); - playlists_widget(playlists.items, selected_page) - .and(LikedSongsPage::new(context.clone()).into_widget()) - .into_columns() - .expand() - .make_window() - .open(&mut app) - .unwrap(); + playlists_widget(playlists.items, selected_page) + .and(LikedSongsPage::new(context.clone()).into_widget()) + .into_columns() + .expand() + .and(bar(dynplayer)) + .into_rows() + .expand() + .make_window() + .open(&mut app) + .unwrap(); + }); }); drop(guard); diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..81b80f4 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,193 @@ +use std::{ + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; + +use cushy::value::{Destination, Dynamic}; +use librespot_metadata::audio::AudioItem; +use librespot_playback::player::{Player, PlayerEvent}; + +pub type DynamicPlayer = Arc; + +pub struct DynamicPlayerInner { + player: Arc, + pub state: Dynamic, + pub track: Dynamic>>, + pub track_progress: Dynamic>, + started_at: Arc>>, + pub repeat: Dynamic, + pub shuffle: Dynamic, + pub volume: Dynamic, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub enum RepeatMode { + #[default] + None, + Track, + Context, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub enum PlayerState { + Loading { + loading_at: Duration, + }, + Playing, + Paused { + paused_at: Duration, + }, + Stopped, + #[default] + Disconnected, +} + +pub fn new_dynamic_player(player: Arc) -> DynamicPlayer { + Arc::new(DynamicPlayerInner::new(player)) +} + +impl DynamicPlayerInner { + pub fn new(player: Arc) -> Self { + Self { + player, + repeat: Default::default(), + shuffle: Default::default(), + started_at: Default::default(), + state: Default::default(), + track: Default::default(), + volume: Default::default(), + track_progress: Default::default(), + } + } + pub async fn run(&self) { + let mut channel = self.player.get_player_event_channel(); + let mut id = 0u64; + loop { + tokio::select! { + event = channel.recv() => { + if let Some(event) = event { + match event { + PlayerEvent::Stopped { + play_request_id, .. + } => { + println!("Stopped {play_request_id}"); + self.state.set(PlayerState::Stopped); + } + PlayerEvent::Loading { + play_request_id, + position_ms, + track_id, + } => { + println!("Loading {play_request_id} {position_ms} {track_id}"); + self.state.set(PlayerState::Loading { + loading_at: Duration::from_millis(position_ms as u64), + }); + } + PlayerEvent::Playing { + play_request_id, + position_ms, + track_id, + } => { + println!("Playing {play_request_id} {position_ms} {track_id}"); + self.state.set(PlayerState::Playing); + *self.started_at.lock().unwrap() = + Some(Instant::now() - Duration::from_millis(position_ms as u64)); + } + PlayerEvent::Paused { + play_request_id, + position_ms, + track_id, + } => { + println!("Paused {play_request_id} {position_ms} {track_id}"); + self.state.set(PlayerState::Paused { + paused_at: Duration::from_millis(position_ms as u64), + }); + } + PlayerEvent::Unavailable { + play_request_id, + track_id, + } => { + println!("Unavailable {play_request_id} {track_id}"); + } + PlayerEvent::VolumeChanged { volume } => { + println!("volume {volume}"); + self.volume.set(volume as f32 / u16::MAX as f32) + } + PlayerEvent::PositionCorrection { + play_request_id, + position_ms, + track_id, + } => { + println!("PositionCorrection {play_request_id} {position_ms} {track_id}"); + *self.started_at.lock().unwrap() = + Some(Instant::now() - Duration::from_millis(position_ms as u64)); + } + PlayerEvent::Seeked { + play_request_id, + position_ms, + track_id, + } => { + println!("Seeked {play_request_id} {position_ms} {track_id}"); + *self.started_at.lock().unwrap() = + Some(Instant::now() - Duration::from_millis(position_ms as u64)); + } + PlayerEvent::TrackChanged { audio_item } => { + println!("TrackChanged {}", audio_item.uri); + dbg!(&audio_item); + *self.track.lock() = Some(audio_item); + } + PlayerEvent::SessionConnected { + connection_id, + user_name, + } => { + println!("SessionConnected {connection_id} {user_name}"); + self.state.set(PlayerState::Stopped); + } + PlayerEvent::SessionDisconnected { + connection_id, + user_name, + } => { + println!("SessionDisconnected {connection_id} {user_name}"); + self.state.set(PlayerState::Disconnected); + } + PlayerEvent::SessionClientChanged { + client_brand_name, + client_id, + client_model_name, + client_name, + } => { + println!("SessionClientChanged {client_brand_name} {client_id} {client_model_name} {client_name}"); + } + PlayerEvent::ShuffleChanged { shuffle } => { + println!("ShuffleChanged {shuffle}"); + self.shuffle.set(shuffle); + } + PlayerEvent::RepeatChanged { context, track } => { + let repeat_mode = match (context, track) { + (true, false) => RepeatMode::Context, + (false, true) => RepeatMode::Track, + _ => RepeatMode::None, + }; + println!("RepeatChanged {repeat_mode:?}"); + self.repeat.set(repeat_mode); + } + PlayerEvent::AutoPlayChanged { .. } => { + println!("AutoPlayChanged") + } + PlayerEvent::FilterExplicitContentChanged { .. } => { + println!("FilterExplicitContentChanged") + } + PlayerEvent::PlayRequestIdChanged { play_request_id } => { + println!("PlayRequestIdChanged {play_request_id}"); + id = play_request_id; + } + _ => {} + }; + } else { + break; + } + } + } + } + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 4c6a073..10679eb 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -5,13 +5,14 @@ pub mod image; pub mod library; pub mod owned; pub mod pages; +pub mod playback; #[derive(PartialEq, Debug, Default)] pub enum ActivePage { #[default] LikedSongs, Playlist(SimplifiedPlaylist), - Album(SimplifiedAlbum) + Album(SimplifiedAlbum), } -type SelectedPage = Dynamic; \ No newline at end of file +type SelectedPage = Dynamic; diff --git a/src/widgets/playback/bar.rs b/src/widgets/playback/bar.rs new file mode 100644 index 0000000..299f899 --- /dev/null +++ b/src/widgets/playback/bar.rs @@ -0,0 +1,95 @@ +use cushy::{ + figures::{units::Lp, Size}, + styles::{Dimension, DimensionRange}, + value::Source, + widget::MakeWidget, + widgets::{Button, Image, Label}, +}; +use itertools::Itertools; +use librespot_metadata::audio::UniqueFields; + +use crate::{ + player::{DynamicPlayer, PlayerState}, + widgets::image::ImageExt, +}; + +pub fn bar(player: DynamicPlayer) -> impl MakeWidget { + meta(player).size(Size { + width: DimensionRange::default(), + height: Dimension::Lp(Lp::inches_f(1.5)).into(), + }) +} + +fn meta(player: DynamicPlayer) -> impl MakeWidget { + Image::new_empty() + .with_url(player.track.map_each(|track| { + track + .as_ref() + .map(|track| track.covers.first().map(|cover| cover.url.clone())) + .flatten() + })) + .size(Size::squared(Dimension::Lp(Lp::inches_f(1.)))) + .and( + player + .track + .map_each(|track| { + track + .as_ref() + .map(|track| { + track + .name + .clone() + .and(match &track.unique_fields { + UniqueFields::Track { + artists, + album, + album_artists, + popularity, + number, + disc_number, + } => { + artists.iter().map(|artist| artist.name.clone()).join(", ") + } + UniqueFields::Episode { + description, + publish_time, + show_name, + } => show_name.clone(), + }) + .into_rows() + .make_widget() + }) + .unwrap_or(Label::::new("No track found").make_widget()) + }) + .expand(), + ) + .into_columns() +} + +fn controls(player: DynamicPlayer) -> impl MakeWidget { + "shuffle" + .into_button() + .and("previous".into_button()) + .and(player.state.map_each(|state| { + match state { + PlayerState::Playing => "pause", + PlayerState::Paused { .. } => "play", + _ => "play", + } + .into_button() + .make_widget() + })) + .and("skip".into_button()) + .and("repeat".into_button()) + .into_columns() + .and(progress(player)) + .into_rows() +} + +fn progress(player: DynamicPlayer) -> impl MakeWidget { + "progress bar here" +} + +fn vol(player: DynamicPlayer) -> impl MakeWidget { + "vol control here" +} diff --git a/src/widgets/playback/mod.rs b/src/widgets/playback/mod.rs new file mode 100644 index 0000000..46f285c --- /dev/null +++ b/src/widgets/playback/mod.rs @@ -0,0 +1 @@ +pub mod bar;