diff --git a/.github/workflows/release-for-arm.yml b/.github/workflows/release-for-arm.yml index 89e04e40..0f5eec63 100644 --- a/.github/workflows/release-for-arm.yml +++ b/.github/workflows/release-for-arm.yml @@ -5,7 +5,7 @@ on: jobs: release: name: release ${{ matrix.target }} - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: diff --git a/Cargo.lock b/Cargo.lock index 023651f3..956ce2b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -432,6 +432,7 @@ dependencies = [ "music-player-settings", "music-player-storage", "music-player-tracklist", + "music-player-types", "serde", "serde_json", "tauri", @@ -1138,11 +1139,12 @@ dependencies = [ [[package]] name = "bstr" -version = "0.2.17" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b" dependencies = [ "memchr", + "serde", ] [[package]] @@ -1240,9 +1242,9 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.13.0" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa0e3586af56b3bfa51fca452bd56e8dbbbd5d8d81cbf0b7e4e35b695b537eb8" +checksum = "497049e9477329f8f6a559972ee42e117487d01d1e8c2cc9f836ea6fa23a9e1a" dependencies = [ "serde", "toml", @@ -2714,9 +2716,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "globset" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ "aho-corasick", "bstr", @@ -3079,6 +3081,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-timeout" version = "0.4.1" @@ -3153,11 +3170,10 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +checksum = "a05705bc64e0b66a806c3740bd6578ea66051b157ec42dc219c785cbf185aef3" dependencies = [ - "crossbeam-utils", "globset", "lazy_static", "log", @@ -3738,9 +3754,9 @@ dependencies = [ [[package]] name = "mdns-sd" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b070a70988c737646f0ff395f4317934c4a90ec14910477e3d1d5a5e22f60d" +checksum = "709cba29c9d7334db28706bc2767db2531934fd2e55781cba82c930fb7f22b47" dependencies = [ "flume 0.10.14", "if-addrs", @@ -3895,6 +3911,7 @@ dependencies = [ name = "music-player" version = "0.2.0-alpha.8" dependencies = [ + "anyhow", "clap", "crossterm", "dirs", @@ -3903,6 +3920,7 @@ dependencies = [ "lofty", "md5", "music-player-addons", + "music-player-audio", "music-player-client", "music-player-discovery", "music-player-entity", @@ -3930,11 +3948,40 @@ dependencies = [ [[package]] name = "music-player-addons" version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "music-player-client", + "music-player-types", + "surf", +] + +[[package]] +name = "music-player-audio" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes 1.3.0", + "futures-util", + "hyper", + "hyper-rustls", + "md5", + "mime_guess", + "music-player-settings", + "parking_lot 0.12.1", + "rustls", + "symphonia", + "tempfile", + "thiserror", + "tokio", + "url", +] [[package]] name = "music-player-client" version = "0.1.1" dependencies = [ + "anyhow", "envtestkit", "futures-util", "music-player-playback", @@ -3942,6 +3989,7 @@ dependencies = [ "music-player-settings", "music-player-storage", "music-player-tracklist", + "music-player-types", "tokio", "tokio-tungstenite", "tonic", @@ -3979,6 +4027,7 @@ dependencies = [ name = "music-player-graphql" version = "0.1.4" dependencies = [ + "anyhow", "async-graphql", "async-graphql-tide", "chrono", @@ -3987,6 +4036,9 @@ dependencies = [ "futures-channel", "futures-util", "md5", + "mdns-sd", + "music-player-addons", + "music-player-discovery", "music-player-entity", "music-player-migration", "music-player-playback", @@ -4024,6 +4076,7 @@ dependencies = [ "lazy_static", "librespot-protocol", "log", + "music-player-audio", "music-player-entity", "music-player-tracklist", "owo-colors", @@ -4035,6 +4088,7 @@ dependencies = [ "symphonia", "thiserror", "tokio", + "url", "zerocopy", ] @@ -4073,6 +4127,7 @@ dependencies = [ "music-player-settings", "music-player-storage", "music-player-tracklist", + "music-player-types", "owo-colors", "prost", "sea-orm", @@ -4102,8 +4157,10 @@ dependencies = [ name = "music-player-storage" version = "0.1.2" dependencies = [ + "anyhow", "itertools", "md5", + "music-player-entity", "music-player-settings", "music-player-types", "sea-orm", @@ -4127,6 +4184,8 @@ version = "0.1.1" dependencies = [ "lofty", "md5", + "mdns-sd", + "music-player-discovery", "tantivy", ] @@ -4142,6 +4201,8 @@ dependencies = [ "dirs", "futures-util", "mime_guess", + "music-player-addons", + "music-player-discovery", "music-player-entity", "music-player-graphql", "music-player-playback", @@ -4149,6 +4210,7 @@ dependencies = [ "music-player-settings", "music-player-storage", "music-player-tracklist", + "music-player-types", "owo-colors", "rust-embed", "sea-orm", @@ -4248,9 +4310,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "nom" -version = "7.1.1" +version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" dependencies = [ "memchr", "minimal-lexical", @@ -4421,9 +4483,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "oneshot" @@ -4644,9 +4706,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" +checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4" dependencies = [ "thiserror", "ucd-trie", @@ -4654,9 +4716,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344" +checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603" dependencies = [ "pest", "pest_generator", @@ -4664,9 +4726,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c" +checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7" dependencies = [ "pest", "pest_meta", @@ -4677,9 +4739,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20" +checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065" dependencies = [ "once_cell", "pest", @@ -4949,9 +5011,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8992a85d8e93a28bdf76137db888d3874e3b230dee5ed8bebac4c9f7617773" +checksum = "e97e3215779627f01ee256d2fad52f3d95e8e1c11e9fc6fd08f7cd455d5d5c78" dependencies = [ "proc-macro2", "syn", @@ -5518,6 +5580,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.1" @@ -5756,6 +5830,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.22.0" @@ -5820,18 +5917,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.151" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.151" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -7250,9 +7347,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.23.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +checksum = "7125661431c26622a80ca5051a2f936c9a678318e0351007b0cc313143024e5c" dependencies = [ "autocfg", "bytes 1.3.0", diff --git a/Cargo.toml b/Cargo.toml index 53ce5c88..77ead6be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,81 +12,11 @@ description = "An extensible music player daemon written in Rust" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[workspace.metadata.raze] -# The path at which to write output files. -# -# `cargo raze` will generate Bazel-compatible BUILD files into this path. -# This can either be a relative path (e.g. "foo/bar"), relative to this -# Cargo.toml file; or relative to the Bazel workspace root (e.g. "//foo/bar"). -workspace_path = "//cargo" -default_gen_buildrs = true -experimental_api = true -render_package_aliases = false - -# This causes aliases for dependencies to be rendered in the BUILD -# file located next to this `Cargo.toml` file. -package_aliases_dir = "." - -# The set of targets to generate BUILD rules for. -targets = [ - "x86_64-apple-darwin", -] - -# The two acceptable options are "Remote" and "Vendored" which -# is used to indicate whether the user is using a non-vendored or -# vendored set of dependencies. -genmode = "Vendored" - -[package.metadata.raze.crates.ring.'*'] -compile_data_attr = "glob([\"*/**\"])" - -[package.metadata.raze.crates.webpki.'*'] -compile_data_attr = "glob([\"*/**\"])" - -[package.metadata.raze.crates.clap_derive.'*'] -compile_data_attr = "glob([\"*/**\"])" - -[package.metadata.raze.crates.protobuf-codegen-pure.'*'] -compile_data_attr = "glob([\"*/**\"])" - -[package.metadata.raze.crates.json5.'*'] -compile_data_attr = "glob([\"*/**\"])" - -[package.metadata.raze.crates.value-bag.'*'] -compile_data_attr = "glob([\"*/**\"])" - -[package.metadata.raze.crates.hmac.'*'] -compile_data_attr = "glob([\"*/**\"])" - -[package.metadata.raze.crates.lofty.'*'] -compile_data_attr = "glob([\"*/**\"])" - -[package.metadata.raze.crates.digest.'*'] -additional_flags = [ - '--cfg=feature=\"mac\"', -] - -[package.metadata.raze.crates.sea-orm.'*'] -additional_flags = [ - '--cfg=feature=\"runtime-tokio-rustls\"', - '--cfg=feature=\"sqlx-sqlite\"', -] - -[package.metadata.raze.crates.sea-orm-migration.'*'] -additional_flags = [ - '--cfg=feature=\"runtime-tokio-rustls\"', - '--cfg=feature=\"sqlx-sqlite\"', -] - -[package.metadata.raze.crates.tokio.'*'] -additional_flags = [ - '--cfg=feature=\"full\"', -] - [workspace] members = [ "addons", + "audio", "client", "discovery", "entity", @@ -163,6 +93,10 @@ version = "0.1.4" path = "types" version = "0.1.1" +[dependencies.music-player-audio] +path = "audio" +version = "0.1.0" + [dependencies.sea-orm-migration] version = "^0.9.0" features = [ @@ -191,3 +125,4 @@ futures-channel = "0.3.24" serde_json = "1.0.85" dirs = "4.0.0" spinners = "4.1.0" +anyhow = "1.0.67" diff --git a/WORKSPACE b/WORKSPACE index fd45092a..a33fd484 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -165,6 +165,7 @@ crates_repository( manifests = [ "//:Cargo.toml", "//addons:Cargo.toml", + "//audio:Cargo.toml", "//client:Cargo.toml", "//discovery:Cargo.toml", "//entity:Cargo.toml", diff --git a/addons/BUILD b/addons/BUILD index 2b07e8e3..e3c5b13f 100644 --- a/addons/BUILD +++ b/addons/BUILD @@ -9,6 +9,7 @@ rust_library( "src/datpiff.rs", "src/deezer.rs", "src/genius.rs", + "src/kodi.rs", "src/lastfm.rs", "src/lib.rs", "src/local.rs", @@ -16,5 +17,11 @@ rust_library( "src/myvazo.rs", "src/tononkira.rs", ], - deps = all_crate_deps(), + deps = [ + "//client:music_player_client", + "//types:music_player_types", + ] + all_crate_deps(), + proc_macro_deps = [ + "@crate_index//:async-trait", + ], ) \ No newline at end of file diff --git a/addons/Cargo.toml b/addons/Cargo.toml index e90b154a..f59a3adc 100644 --- a/addons/Cargo.toml +++ b/addons/Cargo.toml @@ -8,4 +8,15 @@ authors = ["Tsiry Sandratraina "] description = "The addons for the music player" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies.music-player-types] +path = "../types" +version = "0.1.1" + +[dependencies.music-player-client] +path = "../client" +version = "0.1.1" + [dependencies] +surf = "2.3.2" +async-trait = "0.1.59" +anyhow = "1.0.67" diff --git a/addons/src/datpiff.rs b/addons/src/datpiff.rs index 2eecc491..708457d2 100644 --- a/addons/src/datpiff.rs +++ b/addons/src/datpiff.rs @@ -1,3 +1,5 @@ +use anyhow::Error; + use super::{Addon, StreamingAddon}; pub struct DatPiff { @@ -47,7 +49,7 @@ impl Addon for DatPiff { } impl StreamingAddon for DatPiff { - fn stream(&self, url: &str) -> Result<(), Box> { + fn stream(&self, url: &str) -> Result<(), Error> { todo!("Implement DatPiff::stream"); } } diff --git a/addons/src/deezer.rs b/addons/src/deezer.rs index c963d4b5..c72bb0d8 100644 --- a/addons/src/deezer.rs +++ b/addons/src/deezer.rs @@ -1,3 +1,5 @@ +use anyhow::Error; + use super::{Addon, StreamingAddon}; pub struct Deezer { @@ -47,7 +49,7 @@ impl Addon for Deezer { } impl StreamingAddon for Deezer { - fn stream(&self, url: &str) -> Result<(), Box> { + fn stream(&self, url: &str) -> Result<(), Error> { todo!("Implement Deezer::stream"); } } diff --git a/addons/src/kodi.rs b/addons/src/kodi.rs new file mode 100644 index 00000000..8144438f --- /dev/null +++ b/addons/src/kodi.rs @@ -0,0 +1,133 @@ +use anyhow::Error; + +use async_trait::async_trait; +use music_player_types::types::{Album, Artist, Device, Playlist, Track}; + +use super::{Addon, Browseable, Player, StreamingAddon}; + +pub struct Client { + pub host: String, + pub port: u16, +} + +pub struct Kodi { + name: String, + version: String, + author: String, + description: String, + enabled: bool, + client: Option, +} + +impl Kodi { + pub fn new() -> Self { + Self { + name: "Kodi".to_string(), + version: "0.1.0".to_string(), + author: "Tsiry Sandratraina".to_string(), + description: "Kodi addon".to_string(), + enabled: true, + client: None, + } + } +} + +impl Addon for Kodi { + fn name(&self) -> &str { + &self.name + } + + fn version(&self) -> &str { + &self.version + } + + fn author(&self) -> &str { + &self.author + } + + fn description(&self) -> &str { + &self.description + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } +} + +impl StreamingAddon for Kodi { + fn stream(&self, url: &str) -> Result<(), Error> { + todo!("Implement Kodi::stream"); + } +} + +#[async_trait] +impl Browseable for Kodi { + async fn albums(&mut self, offset: i32, limit: i32) -> Result, Error> { + todo!() + } + + async fn artists(&mut self, offset: i32, limit: i32) -> Result, Error> { + todo!() + } + + async fn tracks(&mut self, offset: i32, limit: i32) -> Result, Error> { + todo!() + } + + async fn playlists(&mut self, offset: i32, limit: i32) -> Result, Error> { + todo!() + } + + async fn album(&mut self, id: &str) -> Result { + todo!() + } + + async fn artist(&mut self, id: &str) -> Result { + todo!() + } + + async fn track(&mut self, id: &str) -> Result { + todo!() + } + + async fn playlist(&mut self, id: &str) -> Result { + todo!() + } +} + +#[async_trait] +impl Player for Kodi { + async fn play(&mut self) -> Result<(), Error> { + todo!() + } + + async fn pause(&mut self) -> Result<(), Error> { + todo!() + } + + async fn stop(&mut self) -> Result<(), Error> { + todo!() + } + + async fn next(&mut self) -> Result<(), Error> { + todo!() + } + + async fn previous(&mut self) -> Result<(), Error> { + todo!() + } + + async fn seek(&mut self, position: u32) -> Result<(), Error> { + todo!() + } +} + +impl From for Kodi { + fn from(device: Device) -> Self { + Self { ..Kodi::new() } + } +} diff --git a/addons/src/lib.rs b/addons/src/lib.rs index 6acf529b..991d873a 100644 --- a/addons/src/lib.rs +++ b/addons/src/lib.rs @@ -1,10 +1,15 @@ pub mod datpiff; pub mod deezer; pub mod genius; +pub mod kodi; pub mod local; pub mod myvazo; pub mod tononkira; +use anyhow::Error; +use async_trait::async_trait; +use music_player_types::types::{Album, Artist, Playlist, Track}; + pub trait Addon { fn name(&self) -> &str; fn version(&self) -> &str; @@ -15,9 +20,61 @@ pub trait Addon { } pub trait StreamingAddon { - fn stream(&self, url: &str) -> Result<(), Box>; + fn stream(&self, url: &str) -> Result<(), Error>; } pub trait LyricsAddon { fn get_lyrics(&self, artist: &str, title: &str) -> Option; } + +#[async_trait] +pub trait Browseable { + async fn albums(&mut self, offset: i32, limit: i32) -> Result, Error>; + async fn artists(&mut self, offset: i32, limit: i32) -> Result, Error>; + async fn tracks(&mut self, offset: i32, limit: i32) -> Result, Error>; + async fn playlists(&mut self, offset: i32, limit: i32) -> Result, Error>; + async fn album(&mut self, id: &str) -> Result; + async fn artist(&mut self, id: &str) -> Result; + async fn track(&mut self, id: &str) -> Result; + async fn playlist(&mut self, id: &str) -> Result; +} + +#[async_trait] +pub trait Player { + async fn play(&mut self) -> Result<(), Error>; + async fn pause(&mut self) -> Result<(), Error>; + async fn stop(&mut self) -> Result<(), Error>; + async fn next(&mut self) -> Result<(), Error>; + async fn previous(&mut self) -> Result<(), Error>; + async fn seek(&mut self, position: u32) -> Result<(), Error>; +} + +pub struct CurrentDevice { + pub source: Option>, + pub receiver: Option>, +} + +impl CurrentDevice { + pub fn new() -> Self { + Self { + source: None, + receiver: None, + } + } + + pub fn set_source(&mut self, source: Box) { + self.source = Some(source); + } + + pub fn clear_source(&mut self) { + self.source = None; + } + + pub fn set_receiver(&mut self, receiver: Box) { + self.receiver = Some(receiver); + } + + pub fn clear_receiver(&mut self) { + self.receiver = None; + } +} diff --git a/addons/src/local.rs b/addons/src/local.rs index 5ac7bca9..76d4d2a8 100644 --- a/addons/src/local.rs +++ b/addons/src/local.rs @@ -1,4 +1,18 @@ -use super::{Addon, StreamingAddon}; +use super::{Addon, Browseable, Player, StreamingAddon}; +use anyhow::{Error, Ok}; +use async_trait::async_trait; +use music_player_client::{ + library::LibraryClient, playback::PlaybackClient, playlist::PlaylistClient, + tracklist::TracklistClient, +}; +use music_player_types::types::{Album, Artist, Device, Playlist, Track}; + +pub struct Client { + pub library: LibraryClient, + pub playback: PlaybackClient, + pub playlist: PlaylistClient, + pub tracklist: TracklistClient, +} pub struct Local { name: String, @@ -6,6 +20,9 @@ pub struct Local { author: String, description: String, enabled: bool, + client: Option, + host: String, + port: u16, } impl Local { @@ -16,6 +33,9 @@ impl Local { author: "Tsiry Sandratraina".to_string(), description: "Local addon".to_string(), enabled: true, + client: None, + host: "localhost".to_string(), + port: 5051, } } } @@ -47,7 +67,134 @@ impl Addon for Local { } impl StreamingAddon for Local { - fn stream(&self, url: &str) -> Result<(), Box> { + fn stream(&self, url: &str) -> Result<(), Error> { todo!("Implement Local::stream"); } } + +#[async_trait] +impl Browseable for Local { + async fn albums(&mut self, offset: i32, limit: i32) -> Result, Error> { + let response = self + .client + .as_mut() + .unwrap() + .library + .albums(offset, limit) + .await?; + Ok(response.into_iter().map(Into::into).collect()) + } + + async fn artists(&mut self, offset: i32, limit: i32) -> Result, Error> { + let response = self + .client + .as_mut() + .unwrap() + .library + .artists(offset, limit) + .await?; + Ok(response.into_iter().map(Into::into).collect()) + } + + async fn tracks(&mut self, offset: i32, limit: i32) -> Result, Error> { + let response = self + .client + .as_mut() + .unwrap() + .library + .songs(offset, limit) + .await?; + Ok(response.into_iter().map(Into::into).collect()) + } + + async fn playlists(&mut self, offset: i32, limit: i32) -> Result, Error> { + let response = self.client.as_mut().unwrap().playlist.list_all().await?; + Ok(response) + } + + async fn album(&mut self, id: &str) -> Result { + let response = self.client.as_mut().unwrap().library.album(id).await?; + match response { + Some(album) => Ok(album.into()), + None => Err(Error::msg("Album not found")), + } + } + + async fn artist(&mut self, id: &str) -> Result { + let response = self.client.as_mut().unwrap().library.artist(id).await?; + match response { + Some(artist) => Ok(artist.into()), + None => Err(Error::msg("Artist not found")), + } + } + + async fn track(&mut self, id: &str) -> Result { + let response = self.client.as_mut().unwrap().library.song(id).await?; + match response { + Some(track) => Ok(track.into()), + None => Err(Error::msg("Track not found")), + } + } + + async fn playlist(&mut self, id: &str) -> Result { + let response = self.client.as_mut().unwrap().playlist.find(id).await?; + Ok(response) + } +} + +#[async_trait] +impl Player for Local { + async fn play(&mut self) -> Result<(), Error> { + self.client.as_mut().unwrap().playback.play().await?; + todo!() + } + + async fn pause(&mut self) -> Result<(), Error> { + self.client.as_mut().unwrap().playback.pause().await?; + todo!() + } + + async fn stop(&mut self) -> Result<(), Error> { + self.client.as_mut().unwrap().playback.stop().await?; + todo!() + } + + async fn next(&mut self) -> Result<(), Error> { + self.client.as_mut().unwrap().playback.next().await?; + todo!() + } + + async fn previous(&mut self) -> Result<(), Error> { + self.client.as_mut().unwrap().playback.prev().await?; + todo!() + } + + async fn seek(&mut self, position: u32) -> Result<(), Error> { + todo!() + } +} + +impl From for Local { + fn from(device: Device) -> Self { + Self { + host: device.host, + port: device.port, + ..Local::new() + } + } +} + +impl Local { + pub async fn connect(&mut self) -> Result<(), Error> { + let client = Client { + library: LibraryClient::new(self.host.clone(), self.port).await?, + playback: PlaybackClient::new(self.host.clone(), self.port).await?, + playlist: PlaylistClient::new(self.host.clone(), self.port).await?, + tracklist: TracklistClient::new(self.host.clone(), self.port).await?, + }; + + self.client = Some(client); + + Ok(()) + } +} diff --git a/addons/src/myvazo.rs b/addons/src/myvazo.rs index 6a27cd2b..c9777740 100644 --- a/addons/src/myvazo.rs +++ b/addons/src/myvazo.rs @@ -1,3 +1,5 @@ +use anyhow::Error; + use super::{Addon, StreamingAddon}; pub struct MyVazo { @@ -47,7 +49,7 @@ impl Addon for MyVazo { } impl StreamingAddon for MyVazo { - fn stream(&self, url: &str) -> Result<(), Box> { + fn stream(&self, url: &str) -> Result<(), Error> { todo!("Implement MyVazo::stream"); } } diff --git a/audio/BUILD b/audio/BUILD new file mode 100644 index 00000000..5f841707 --- /dev/null +++ b/audio/BUILD @@ -0,0 +1,32 @@ +package(default_visibility = ["//visibility:public"]) + +load("@crate_index//:defs.bzl", "aliases", "all_crate_deps") +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "music_player_audio", + srcs = [ + "src/fetch/mod.rs", + "src/fetch/cache.rs", + "src/fetch/client.rs", + "src/fetch/receive.rs", + "src/lib.rs", + "src/range_set.rs", + ], + deps = [ + "//settings:music_player_settings", + "@crate_index//:anyhow", + "@crate_index//:bytes", + "@crate_index//:futures-util", + "@crate_index//:hyper", + "@crate_index//:hyper-rustls", + "@crate_index//:mime_guess", + "@crate_index//:parking_lot", + "@crate_index//:rustls", + "@crate_index//:symphonia", + "@crate_index//:tempfile", + "@crate_index//:thiserror", + "@crate_index//:tokio", + "@crate_index//:url", + ] + all_crate_deps(), +) \ No newline at end of file diff --git a/audio/Cargo.toml b/audio/Cargo.toml new file mode 100644 index 00000000..0a970db5 --- /dev/null +++ b/audio/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "music-player-audio" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies.music-player-settings] +path = "../settings" +version = "0.1.1" + +[dependencies] +anyhow = "1.0.68" +bytes = "1.3.0" +futures-util = "0.3.25" +hyper = { version = "0.14.23", features = ["client", "stream", "tcp", "http1", "http2"] } +parking_lot = "0.12.1" +tempfile = "3.3.0" +thiserror = "1.0.38" +tokio = { version = "1.23.0", features = ["full"] } +symphonia = { version = "0.5.1", features = ["aac", "alac", "mp3", "isomp4", "flac"] } +hyper-rustls = "0.23.2" +rustls = "0.20.7" +mime_guess = "2.0.4" +url = "2.3.1" +md5 = "0.7.0" diff --git a/audio/src/decoder/mod.rs b/audio/src/decoder/mod.rs new file mode 100644 index 00000000..6f5fa180 --- /dev/null +++ b/audio/src/decoder/mod.rs @@ -0,0 +1,76 @@ +use std::ops::Deref; +use thiserror::Error; + +pub mod symphonia_decoder; + +#[derive(Error, Debug)] +pub enum DecoderError { + #[error("Symphonia Decoder Error: {0}")] + SymphoniaDecoder(String), +} + +pub type DecoderResult = Result; + +#[derive(Error, Debug)] +pub enum AudioPacketError { + #[error("Decoder Raw Error: Can't return Raw on Samples")] + Raw, + #[error("Decoder Samples Error: Can't return Samples on Raw")] + Samples, +} + +pub type AudioPacketResult = Result; + +pub enum AudioPacket { + Samples(Vec), + Raw(Vec), +} + +impl AudioPacket { + pub fn samples(&self) -> AudioPacketResult<&[f64]> { + match self { + AudioPacket::Samples(s) => Ok(s), + AudioPacket::Raw(_) => Err(AudioPacketError::Raw), + } + } + + pub fn raw(&self) -> AudioPacketResult<&[u8]> { + match self { + AudioPacket::Raw(d) => Ok(d), + AudioPacket::Samples(_) => Err(AudioPacketError::Samples), + } + } + + pub fn is_empty(&self) -> bool { + match self { + AudioPacket::Samples(s) => s.is_empty(), + AudioPacket::Raw(d) => d.is_empty(), + } + } +} + +#[derive(Debug, Clone)] +pub struct AudioPacketPosition { + pub position_ms: u32, + pub skipped: bool, +} + +impl Deref for AudioPacketPosition { + type Target = u32; + fn deref(&self) -> &Self::Target { + &self.position_ms + } +} + +pub trait AudioDecoder { + fn seek(&mut self, position_ms: u32) -> Result; + fn next_packet( + &mut self, + ) -> DecoderResult>; +} + +impl From for DecoderError { + fn from(err: symphonia::core::errors::Error) -> Self { + Self::SymphoniaDecoder(err.to_string()) + } +} diff --git a/audio/src/decoder/symphonia_decoder.rs b/audio/src/decoder/symphonia_decoder.rs new file mode 100644 index 00000000..79bb8b47 --- /dev/null +++ b/audio/src/decoder/symphonia_decoder.rs @@ -0,0 +1,217 @@ +use std::io; + +use symphonia::core::{ + audio::SampleBuffer, + codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL}, + errors::Error, + formats::{FormatOptions, FormatReader, SeekMode, SeekTo, Track}, + io::{MediaSource, MediaSourceStream}, + meta::MetadataOptions, + probe::Hint, + units::Time, +}; + +use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult}; + +pub const SAMPLE_RATE: u32 = 44100; +pub const NUM_CHANNELS: u8 = 2; +pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; +pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0; +pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64; + +#[derive(Copy, Clone)] +struct PlayTrackOptions { + track_id: u32, + seek_ts: u64, +} + +fn first_supported_track(tracks: &[Track]) -> Option<&Track> { + tracks + .iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) +} + +pub struct SymphoniaDecoder { + format: Box, + decoder: Box, + sample_buffer: Option>, +} + +impl SymphoniaDecoder { + pub fn new(input: R, hint: Hint) -> DecoderResult + where + R: MediaSource + 'static, + { + // Create the media source stream using the boxed media source from above. + let mss = MediaSourceStream::new(Box::new(input), Default::default()); + + // Use the default options for format readers other than for gapless playback. + let format_opts = FormatOptions { + enable_gapless: false, + ..Default::default() + }; + + // Use the default options for metadata readers. + let metadata_opts: MetadataOptions = Default::default(); + + let track: Option = None; + + // Probe the media source stream for metadata and get the format reader. + match symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts) { + Ok(probed) => { + // Playback mode. + // print_format(song, &mut probed); + + // Set the decoder options. + let decode_opts = DecoderOptions { + verify: false, + ..Default::default() + }; + + // Play it! + // play(probed.format, track, seek_time, &decode_opts, no_progress); + + // If the user provided a track number, select that track if it exists, otherwise, select the + // first track with a known codec. + let track = track + .and_then(|t| probed.format.tracks().get(t)) + .or_else(|| first_supported_track(probed.format.tracks())); + + let track_id = match track { + Some(track) => track.id, + _ => { + return Err(DecoderError::SymphoniaDecoder( + "No supported tracks found".to_string(), + )) + } + }; + + let seek_ts = 0; + + let track_info = PlayTrackOptions { track_id, seek_ts }; + + // Get the selected track using the track ID. + let track = match probed + .format + .tracks() + .iter() + .find(|track| track.id == track_info.track_id) + { + Some(track) => track, + _ => { + return Err(DecoderError::SymphoniaDecoder( + "No supported tracks found".to_string(), + )) + } + }; + + // Create a decoder for the track. + let decoder = + symphonia::default::get_codecs().make(&track.codec_params, &decode_opts)?; + return Ok(SymphoniaDecoder { + format: probed.format, + decoder, + sample_buffer: None, + }); + } + Err(err) => { + // The input was not supported by any format reader. + panic!("file not supported. reason? {}", err); + } + } + } + + fn ts_to_ms(&self, ts: u64) -> u32 { + let time_base = self.decoder.codec_params().time_base; + let seeked_to_ms = match time_base { + Some(time_base) => { + let time = time_base.calc_time(ts); + (time.seconds as f64 + time.frac) * 1000. + } + // Fallback in the unexpected case that the format has no base time set. + None => ts as f64 * PAGES_PER_MS, + }; + seeked_to_ms as u32 + } +} + +impl AudioDecoder for SymphoniaDecoder { + fn seek(&mut self, position_ms: u32) -> Result { + let seconds = position_ms as u64 / 1000; + let frac = (position_ms as f64 % 1000.) / 1000.; + let time = Time::new(seconds, frac); + + // `track_id: None` implies the default track ID (of the container, not of Spotify). + let seeked_to_ts = self.format.seek( + SeekMode::Accurate, + SeekTo::Time { + time, + track_id: None, + }, + )?; + + // Seeking is a `FormatReader` operation, so the decoder cannot reliably + // know when a seek took place. Reset it to avoid audio glitches. + self.decoder.reset(); + + Ok(self.ts_to_ms(seeked_to_ts.actual_ts)) + } + + fn next_packet( + &mut self, + ) -> DecoderResult> { + let mut skipped = false; + + loop { + let packet = match self.format.next_packet() { + Ok(packet) => packet, + Err(Error::IoError(err)) => { + if err.kind() == io::ErrorKind::UnexpectedEof { + return Ok(None); + } else { + return Err(DecoderError::SymphoniaDecoder(err.to_string())); + } + } + Err(err) => { + return Err(err.into()); + } + }; + + let position_ms = self.ts_to_ms(packet.ts()); + let packet_position = AudioPacketPosition { + position_ms, + skipped, + }; + + match self.decoder.decode(&packet) { + Ok(decoded) => { + let spec = *decoded.spec(); + let sample_buffer = match self.sample_buffer.as_mut() { + Some(buffer) => buffer, + None => { + let duration = decoded.capacity() as u64; + self.sample_buffer.insert(SampleBuffer::new(duration, spec)) + } + }; + + sample_buffer.copy_interleaved_ref(decoded); + let samples = AudioPacket::Samples(sample_buffer.samples().to_vec()); + + return Ok(Some(( + packet_position, + samples, + spec.channels.count() as u16, + spec.rate, + ))); + } + Err(Error::DecodeError(_)) => { + // The packet failed to decode due to corrupted or invalid data, get a new + // packet and try again. + skipped = true; + continue; + } + Err(err) => return Err(err.into()), + } + } + } +} diff --git a/audio/src/fetch/cache.rs b/audio/src/fetch/cache.rs new file mode 100644 index 00000000..7a9ad96b --- /dev/null +++ b/audio/src/fetch/cache.rs @@ -0,0 +1,34 @@ +use anyhow::Error; +use music_player_settings::get_application_directory; +use std::{ + fs::File, + io::{self, Read}, + path::Path, +}; +pub struct Cache { + cache_dir: String, +} + +impl Cache { + pub fn new() -> Self { + let cache_dir = format!("{}/cache", get_application_directory()); + Self { cache_dir } + } + + pub fn save_file(&self, name: &str, contents: &mut F) -> Result<(), Error> { + if self.is_file_cached(name) { + return Ok(()); + } + let mut file = File::create(format!("{}/{}", self.cache_dir, name))?; + io::copy(contents, &mut file)?; + Ok(()) + } + + pub fn is_file_cached(&self, name: &str) -> bool { + Path::new(&format!("{}/{}", self.cache_dir, name)).exists() + } + + pub fn open_file(&self, name: &str) -> Result { + Ok(File::open(format!("{}/{}", self.cache_dir, name))?) + } +} diff --git a/audio/src/fetch/client.rs b/audio/src/fetch/client.rs new file mode 100644 index 00000000..db0a3f4f --- /dev/null +++ b/audio/src/fetch/client.rs @@ -0,0 +1,43 @@ +use anyhow::Error; +use futures_util::{future::IntoStream, FutureExt, TryStreamExt}; +use hyper::{ + client::{self, HttpConnector, ResponseFuture}, + header::RANGE, + Request, +}; +use hyper_rustls::{ConfigBuilderExt, HttpsConnector}; + +pub struct Client { + client: hyper::Client>, +} + +impl Client { + pub fn new() -> Self { + let tls = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_native_roots() + .with_no_client_auth(); + // Prepare the HTTPS connector + let https = hyper_rustls::HttpsConnectorBuilder::new() + .with_tls_config(tls) + .https_or_http() + .enable_http1() + .build(); + let client = client::Client::builder().build(https); + Self { client } + } + + pub fn stream_from_url( + &self, + url: &str, + offset: usize, + length: usize, + ) -> Result, Error> { + let req = Request::builder() + .method("GET") + .uri(url) + .header(RANGE, format!("bytes={}-{}", offset, offset + length - 1)) + .body(hyper::Body::empty())?; + Ok(self.client.request(req).into_stream()) + } +} diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs new file mode 100644 index 00000000..a842e875 --- /dev/null +++ b/audio/src/fetch/mod.rs @@ -0,0 +1,700 @@ +use std::{ + cmp::min, + fs, + io::{self, Read, Seek, SeekFrom}, + path::Path, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, + }, + time::Duration, +}; + +use anyhow::Error; +use futures_util::{future::IntoStream, StreamExt, TryFutureExt}; +use hyper::{ + client::ResponseFuture, + header::{self, CONTENT_RANGE}, + Body, Response, StatusCode, +}; +use symphonia::core::io::MediaSource; +use thiserror::Error; + +use parking_lot::{Condvar, Mutex}; +use tempfile::NamedTempFile; +use tokio::sync::{mpsc, oneshot, Semaphore}; +use url::Url; + +use self::{client::Client, receive::audio_file_fetch}; + +use crate::{ + fetch::cache::Cache, + range_set::{Range, RangeSet}, +}; + +pub mod client; + +pub mod receive; + +pub mod cache; + +pub type AudioFileResult = Result<(), anyhow::Error>; + +pub const MINIMUM_DOWNLOAD_SIZE: usize = 64 * 1024; + +pub const MINIMUM_THROUGHPUT: usize = 8 * 1024; + +pub const READ_AHEAD_BEFORE_PLAYBACK: Duration = Duration::from_secs(1); + +pub const READ_AHEAD_DURING_PLAYBACK: Duration = Duration::from_secs(5); + +pub const DOWNLOAD_TIMEOUT: Duration = + Duration::from_secs((MINIMUM_DOWNLOAD_SIZE / MINIMUM_THROUGHPUT) as u64); + +pub const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0; + +/// If the measured ping time to the Spotify server is larger than this value, it is capped +/// to avoid run-away block sizes and pre-fetching. +pub const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500); + +/// The ping time that is used for calculations before a ping time was actually measured. +pub const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500); + +#[derive(Error, Debug)] +pub enum AudioFileError { + #[error("other end of channel disconnected")] + Channel, + #[error("required header not found")] + Header, + #[error("streamer received no data")] + NoData, + #[error("no output available")] + Output, + #[error("invalid status code {0}")] + StatusCode(StatusCode), + #[error("wait timeout exceeded")] + WaitTimeout, +} + +pub enum AudioFile { + Cached(fs::File), + Streaming(AudioFileStreaming), + Local(fs::File), +} + +#[derive(Debug)] +pub struct StreamingRequest { + streamer: IntoStream, + initial_response: Option>, + offset: usize, + length: usize, +} + +#[derive(Clone)] +pub struct StreamLoaderController { + channel_tx: Option>, + stream_shared: Option>, + file_size: usize, +} + +impl StreamLoaderController { + pub fn len(&self) -> usize { + self.file_size + } + + pub fn is_empty(&self) -> bool { + self.file_size == 0 + } + + pub fn range_available(&self, range: Range) -> bool { + let available = if let Some(ref shared) = self.stream_shared { + let download_status = shared.download_status.lock(); + + range.length + <= download_status + .downloaded + .contained_length_from_value(range.start) + } else { + range.length <= self.len() - range.start + }; + + available + } + + pub fn range_to_end_available(&self) -> bool { + match self.stream_shared { + Some(ref shared) => { + let read_position = shared.read_position(); + self.range_available(Range::new(read_position, self.len() - read_position)) + } + None => true, + } + } + + pub fn ping_time(&self) -> Option { + self.stream_shared.as_ref().map(|shared| shared.ping_time()) + } + + fn send_stream_loader_command(&self, command: StreamLoaderCommand) { + if let Some(ref channel) = self.channel_tx { + // Ignore the error in case the channel has been closed already. + // This means that the file was completely downloaded. + let _ = channel.send(command); + } + } + + pub fn fetch(&self, range: Range) { + // signal the stream loader to fetch a range of the file + self.send_stream_loader_command(StreamLoaderCommand::Fetch(range)); + } + + pub fn fetch_blocking(&self, mut range: Range) -> AudioFileResult { + // signal the stream loader to tech a range of the file and block until it is loaded. + + // ensure the range is within the file's bounds. + if range.start >= self.len() { + range.length = 0; + } else if range.end() > self.len() { + range.length = self.len() - range.start; + } + + self.fetch(range); + + if let Some(ref shared) = self.stream_shared { + let mut download_status = shared.download_status.lock(); + + while range.length + > download_status + .downloaded + .contained_length_from_value(range.start) + { + if shared + .cond + .wait_for(&mut download_status, DOWNLOAD_TIMEOUT) + .timed_out() + { + return Err(AudioFileError::WaitTimeout.into()); + } + + if range.length + > (download_status + .downloaded + .union(&download_status.requested) + .contained_length_from_value(range.start)) + { + // For some reason, the requested range is neither downloaded nor requested. + // This could be due to a network error. Request it again. + self.fetch(range); + } + } + } + + Ok(()) + } + + pub fn fetch_next_and_wait( + &self, + request_length: usize, + wait_length: usize, + ) -> AudioFileResult { + match self.stream_shared { + Some(ref shared) => { + let start = shared.read_position(); + + let request_range = Range { + start, + length: request_length, + }; + self.fetch(request_range); + + let wait_range = Range { + start, + length: wait_length, + }; + self.fetch_blocking(wait_range) + } + None => Ok(()), + } + } + + pub fn set_random_access_mode(&self) { + // optimise download strategy for random access + if let Some(ref shared) = self.stream_shared { + shared.set_download_streaming(false) + } + } + + pub fn set_stream_mode(&self) { + // optimise download strategy for streaming + if let Some(ref shared) = self.stream_shared { + shared.set_download_streaming(true) + } + } + + pub fn close(&self) { + // terminate stream loading and don't load any more data for this file. + self.send_stream_loader_command(StreamLoaderCommand::Close); + } + + pub fn mime_type(&self) -> Option { + if let Some(ref shared) = self.stream_shared { + shared.get_mime_type() + } else { + None + } + } +} + +pub struct AudioFileStreaming { + read_file: fs::File, + position: u64, + stream_loader_command_tx: mpsc::UnboundedSender, + shared: Arc, +} + +struct AudioFileDownloadStatus { + requested: RangeSet, + downloaded: RangeSet, +} + +impl AudioFile { + pub async fn open(url: &str, bytes_per_second: usize) -> Result { + if Url::parse(url).is_err() { + return Ok(AudioFile::Local(fs::File::open(url)?)); + } + + let cache = Cache::new(); + let file_id = format!("{:x}", md5::compute(url.to_owned())); + if cache.is_file_cached(file_id.as_str()) { + println!("File is cached: {}", file_id); + return Ok(AudioFile::Cached(cache.open_file(file_id.as_str())?)); + } + + let (complete_tx, complete_rx) = oneshot::channel(); + + let streaming = AudioFileStreaming::open(url.to_owned(), complete_tx, bytes_per_second); + + let file_id = format!("{:x}", md5::compute(url.to_owned())); + + // spawn a task to download the file + tokio::spawn(complete_rx.map_ok(move |mut file| { + println!("Download complete: {}", file.path().display()); + let cache = Cache::new(); + match cache.save_file(&file_id, &mut file) { + Ok(_) => println!("Saved to cache: {}", file_id), + Err(e) => println!("Failed to save to cache: {}", e), + } + })); + + Ok(AudioFile::Streaming(streaming.await?)) + } + + pub fn get_stream_loader_controller(&self) -> Result { + let controller = match self { + AudioFile::Streaming(ref stream) => StreamLoaderController { + channel_tx: Some(stream.stream_loader_command_tx.clone()), + stream_shared: Some(stream.shared.clone()), + file_size: stream.shared.file_size, + }, + AudioFile::Cached(ref file) => StreamLoaderController { + channel_tx: None, + stream_shared: None, + file_size: file.metadata()?.len() as usize, + }, + AudioFile::Local(ref file) => StreamLoaderController { + channel_tx: None, + stream_shared: None, + file_size: file.metadata()?.len() as usize, + }, + }; + + Ok(controller) + } + + pub fn is_cached(&self) -> bool { + matches!(self, AudioFile::Cached { .. }) + } + + pub fn is_local(&self) -> bool { + matches!(self, AudioFile::Local { .. }) + } + + pub async fn get_mime_type(url: &str) -> Result { + if Url::parse(url).is_err() { + if !Path::new(url).exists() { + return Err(Error::msg("File does not exist")); + } + match mime_guess::from_path(url).first() { + Some(mime) => return Ok(mime.to_string()), + None => return Err(Error::msg("No mime type found")), + } + } + let mut streamer = Client::new().stream_from_url(url, 0, 512)?; + let response = streamer.next().await.ok_or(AudioFileError::NoData)??; + + let content_type = match response.headers().get(header::CONTENT_TYPE) { + Some(content_type) => content_type, + None => return Err(Error::msg("No Content-Type header")), + }; + + let mime = content_type.to_str()?; + + Ok(mime.to_owned()) + } +} + +impl AudioFileStreaming { + pub async fn open( + url: String, + complete_tx: oneshot::Sender, + bytes_per_second: usize, + ) -> Result { + // When the audio file is really small, this `download_size` may turn out to be + // larger than the audio file we're going to stream later on. This is OK; requesting + // `Content-Range` > `Content-Length` will return the complete file with status code + // 206 Partial Content. + + let mut streamer = Client::new().stream_from_url(url.as_str(), 0, MINIMUM_DOWNLOAD_SIZE)?; + + // Get the first chunk with the headers to get the file size. + // The remainder of that chunk with possibly also a response body is then + // further processed in `audio_file_fetch`. + let response = streamer.next().await.ok_or(AudioFileError::NoData)??; + + let code = response.status(); + if code != StatusCode::PARTIAL_CONTENT { + return Err(AudioFileError::StatusCode(code).into()); + } + + let header_value = response + .headers() + .get(CONTENT_RANGE) + .ok_or(AudioFileError::Header)?; + let str_value = header_value.to_str()?; + let hyphen_index = str_value.find('-').unwrap_or_default(); + let slash_index = str_value.find('/').unwrap_or_default(); + let upper_bound: usize = str_value[hyphen_index + 1..slash_index].parse()?; + let file_size = str_value[slash_index + 1..].parse()?; + + let content_type = match response.headers().get(header::CONTENT_TYPE) { + Some(content_type) => content_type, + None => return Err(Error::msg("No Content-Type header")), + }; + + let mime = content_type.to_str()?; + let mime = mime.to_owned(); + + let initial_request = StreamingRequest { + streamer, + initial_response: Some(response), + offset: 0, + length: upper_bound + 1, + }; + + let shared = Arc::new(AudioFileShared { + url, + file_size, + bytes_per_second, + cond: Condvar::new(), + download_status: Mutex::new(AudioFileDownloadStatus { + requested: RangeSet::new(), + downloaded: RangeSet::new(), + }), + download_streaming: AtomicBool::new(false), + download_slots: Semaphore::new(1), + ping_time_ms: AtomicUsize::new(0), + read_position: AtomicUsize::new(0), + throughput: AtomicUsize::new(0), + mime_type: mime, + }); + + let write_file = NamedTempFile::new_in("/tmp/audio")?; + write_file.as_file().set_len(file_size as u64)?; + + let read_file = write_file.reopen()?; + + let (stream_loader_command_tx, stream_loader_command_rx) = + mpsc::unbounded_channel::(); + + tokio::spawn(audio_file_fetch( + shared.clone(), + initial_request, + write_file, + stream_loader_command_rx, + complete_tx, + )); + + Ok(AudioFileStreaming { + read_file, + position: 0, + stream_loader_command_tx, + shared, + }) + } +} + +impl Read for AudioFileStreaming { + fn read(&mut self, output: &mut [u8]) -> io::Result { + let offset = self.position as usize; + + if offset >= self.shared.file_size { + return Ok(0); + } + + let length = min(output.len(), self.shared.file_size - offset); + if length == 0 { + return Ok(0); + } + + let length_to_request = if self.shared.is_download_streaming() { + let length_to_request = length + + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * self.shared.bytes_per_second as f32) + as usize; + + // Due to the read-ahead stuff, we potentially request more than the actual request demanded. + min(length_to_request, self.shared.file_size - offset) + } else { + length + }; + + let mut ranges_to_request = RangeSet::new(); + ranges_to_request.add_range(&Range::new(offset, length_to_request)); + + let mut download_status = self.shared.download_status.lock(); + + ranges_to_request.subtract_range_set(&download_status.downloaded); + ranges_to_request.subtract_range_set(&download_status.requested); + + for &range in ranges_to_request.iter() { + self.stream_loader_command_tx + .send(StreamLoaderCommand::Fetch(range)) + .map_err(|err| io::Error::new(io::ErrorKind::BrokenPipe, err))?; + } + + while !download_status.downloaded.contains(offset) { + if self + .shared + .cond + .wait_for(&mut download_status, DOWNLOAD_TIMEOUT) + .timed_out() + { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + Error::msg("Download timed out"), + )); + } + } + let available_length = download_status + .downloaded + .contained_length_from_value(offset); + + drop(download_status); + + self.position = self.read_file.seek(SeekFrom::Start(offset as u64))?; + let read_len = min(length, available_length); + let read_len = self.read_file.read(&mut output[..read_len])?; + + self.position += read_len as u64; + self.shared.set_read_position(self.position); + + Ok(read_len) + } +} + +impl Seek for AudioFileStreaming { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + // If we are already at this position, we don't need to switch download mode. + // These checks and locks are less expensive than interrupting streaming. + let current_position = self.position as i64; + let requested_pos = match pos { + SeekFrom::Start(pos) => pos as i64, + SeekFrom::End(pos) => self.shared.file_size as i64 - pos - 1, + SeekFrom::Current(pos) => current_position + pos, + }; + if requested_pos == current_position { + return Ok(current_position as u64); + } + + // Again if we have already downloaded this part. + let available = self + .shared + .download_status + .lock() + .downloaded + .contains(requested_pos as usize); + + let mut was_streaming = false; + if !available { + // Ensure random access mode if we need to download this part. + // Checking whether we are streaming now is a micro-optimization + // to save an atomic load. + was_streaming = self.shared.is_download_streaming(); + if was_streaming { + self.shared.set_download_streaming(false); + } + } + + self.position = self.read_file.seek(pos)?; + self.shared.set_read_position(self.position); + + if !available && was_streaming { + self.shared.set_download_streaming(true); + } + + Ok(self.position) + } +} + +impl Read for AudioFile { + fn read(&mut self, output: &mut [u8]) -> io::Result { + match *self { + AudioFile::Cached(ref mut file) => file.read(output), + AudioFile::Streaming(ref mut file) => file.read(output), + AudioFile::Local(ref mut file) => file.read(output), + } + } +} + +impl Seek for AudioFile { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + match *self { + AudioFile::Cached(ref mut file) => file.seek(pos), + AudioFile::Streaming(ref mut file) => file.seek(pos), + AudioFile::Local(ref mut file) => file.seek(pos), + } + } +} + +#[derive(Debug)] +pub enum StreamLoaderCommand { + Fetch(Range), // signal the stream loader to fetch a range of the file + Close, // terminate and don't load any more data +} + +struct AudioFileShared { + url: String, + file_size: usize, + bytes_per_second: usize, + cond: Condvar, + download_status: Mutex, + download_streaming: AtomicBool, + download_slots: Semaphore, + ping_time_ms: AtomicUsize, + read_position: AtomicUsize, + throughput: AtomicUsize, + mime_type: String, +} + +impl AudioFileShared { + fn is_download_streaming(&self) -> bool { + self.download_streaming.load(Ordering::Acquire) + } + + fn set_download_streaming(&self, streaming: bool) { + self.download_streaming.store(streaming, Ordering::Release) + } + + fn ping_time(&self) -> Duration { + let ping_time_ms = self.ping_time_ms.load(Ordering::Acquire); + if ping_time_ms > 0 { + Duration::from_millis(ping_time_ms as u64) + } else { + INITIAL_PING_TIME_ESTIMATE + } + } + + fn set_ping_time(&self, duration: Duration) { + self.ping_time_ms + .store(duration.as_millis() as usize, Ordering::Release) + } + + fn throughput(&self) -> usize { + self.throughput.load(Ordering::Acquire) + } + + fn set_throughput(&self, throughput: usize) { + self.throughput.store(throughput, Ordering::Release) + } + + fn read_position(&self) -> usize { + self.read_position.load(Ordering::Acquire) + } + + fn set_read_position(&self, position: u64) { + self.read_position + .store(position as usize, Ordering::Release) + } + + fn get_mime_type(&self) -> Option { + if Url::parse(&self.url).is_err() { + if Path::new(&self.url).exists() { + match mime_guess::from_path(&self.url).first() { + Some(mime) => { + return Some(mime.to_string()); + } + None => return None, + }; + } + } + Some(format!("{}", self.mime_type)) + } +} + +pub struct Subfile { + stream: T, + offset: u64, + length: u64, +} + +impl Subfile { + pub fn new(mut stream: T, offset: u64, length: u64) -> Result, io::Error> { + let target = SeekFrom::Start(offset); + stream.seek(target)?; + + Ok(Subfile { + stream, + offset, + length, + }) + } +} + +impl Read for Subfile { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.stream.read(buf) + } +} + +impl Seek for Subfile { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + let pos = match pos { + SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset), + SeekFrom::End(offset) => { + if (self.length as i64 - offset) < self.offset as i64 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "newpos would be < self.offset", + )); + } + pos + } + _ => pos, + }; + + let newpos = self.stream.seek(pos)?; + Ok(newpos - self.offset) + } +} + +impl MediaSource for Subfile +where + R: Read + Seek + Send + Sync, +{ + fn is_seekable(&self) -> bool { + true + } + + fn byte_len(&self) -> Option { + Some(self.length) + } +} diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs new file mode 100644 index 00000000..b14b4bb2 --- /dev/null +++ b/audio/src/fetch/receive.rs @@ -0,0 +1,467 @@ +use std::{ + cmp::{max, min}, + io::{Seek, SeekFrom, Write}, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::Error; +use bytes::Bytes; +use futures_util::StreamExt; +use hyper::StatusCode; +use tempfile::NamedTempFile; +use tokio::sync::{mpsc, oneshot}; + +use crate::{ + fetch::PREFETCH_THRESHOLD_FACTOR, + range_set::{Range, RangeSet}, +}; + +use super::{ + client::Client, AudioFileError, AudioFileResult, AudioFileShared, StreamLoaderCommand, + StreamingRequest, MAXIMUM_ASSUMED_PING_TIME, MINIMUM_DOWNLOAD_SIZE, MINIMUM_THROUGHPUT, +}; + +struct AudioFileFetch { + shared: Arc, + output: Option, + + file_data_tx: mpsc::UnboundedSender, + complete_tx: Option>, + network_response_times: Vec, +} + +// Might be replaced by enum from std once stable +#[derive(PartialEq, Eq)] +enum ControlFlow { + Break, + Continue, +} + +#[derive(Debug)] +struct PartialFileData { + offset: usize, + data: Bytes, +} + +#[derive(Debug)] +enum ReceivedData { + Throughput(usize), + ResponseTime(Duration), + Data(PartialFileData), +} + +const ONE_SECOND: Duration = Duration::from_secs(1); + +impl AudioFileFetch { + fn has_download_slots_available(&self) -> bool { + self.shared.download_slots.available_permits() > 0 + } + + fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult { + if length < MINIMUM_DOWNLOAD_SIZE { + length = MINIMUM_DOWNLOAD_SIZE; + } + + // If we are in streaming mode (so not seeking) then start downloading as large + // of chunks as possible for better throughput and improved CPU usage, while + // still being reasonably responsive (~1 second) in case we want to seek. + if self.shared.is_download_streaming() { + let throughput = self.shared.throughput(); + length = max(length, throughput); + } + + if offset + length > self.shared.file_size { + length = self.shared.file_size - offset; + } + let mut ranges_to_request = RangeSet::new(); + ranges_to_request.add_range(&Range::new(offset, length)); + + // The iteration that follows spawns streamers fast, without awaiting them, + // so holding the lock for the entire scope of this function should be faster + // then locking and unlocking multiple times. + let mut download_status = self.shared.download_status.lock(); + + ranges_to_request.subtract_range_set(&download_status.downloaded); + ranges_to_request.subtract_range_set(&download_status.requested); + + for range in ranges_to_request.iter() { + let streamer = + Client::new().stream_from_url(&self.shared.url, range.start, range.length)?; + download_status.requested.add_range(range); + + let streaming_request = StreamingRequest { + streamer, + initial_response: None, + offset: range.start, + length: range.length, + }; + + tokio::spawn(receive_data( + self.shared.clone(), + self.file_data_tx.clone(), + streaming_request, + )); + } + + Ok(()) + } + + fn pre_fetch_more_data(&mut self, bytes: usize) -> AudioFileResult { + // determine what is still missing + let mut missing_data = RangeSet::new(); + missing_data.add_range(&Range::new(0, self.shared.file_size)); + { + let download_status = self.shared.download_status.lock(); + missing_data.subtract_range_set(&download_status.downloaded); + missing_data.subtract_range_set(&download_status.requested); + } + + // download data from after the current read position first + let mut tail_end = RangeSet::new(); + let read_position = self.shared.read_position(); + tail_end.add_range(&Range::new( + read_position, + self.shared.file_size - read_position, + )); + let tail_end = tail_end.intersection(&missing_data); + + if !tail_end.is_empty() { + let range = tail_end.get_range(0); + let offset = range.start; + let length = min(range.length, bytes); + self.download_range(offset, length)?; + } else if !missing_data.is_empty() { + // ok, the tail is downloaded, download something fom the beginning. + let range = missing_data.get_range(0); + let offset = range.start; + let length = min(range.length, bytes); + self.download_range(offset, length)?; + } + + Ok(()) + } + + fn handle_file_data(&mut self, data: ReceivedData) -> Result { + match data { + ReceivedData::Throughput(mut throughput) => { + if throughput < MINIMUM_THROUGHPUT { + throughput = MINIMUM_THROUGHPUT; + } + + let old_throughput = self.shared.throughput(); + let avg_throughput = if old_throughput > 0 { + (old_throughput + throughput) / 2 + } else { + throughput + }; + + // print when the new estimate deviates by more than 10% from the last + if f32::abs((avg_throughput as f32 - old_throughput as f32) / old_throughput as f32) + > 0.1 + { + // trace!( + // "Throughput now estimated as: {} kbps", + // avg_throughput / 1000 + //); + } + + self.shared.set_throughput(avg_throughput); + } + ReceivedData::ResponseTime(mut response_time) => { + if response_time > MAXIMUM_ASSUMED_PING_TIME { + response_time = MAXIMUM_ASSUMED_PING_TIME; + } + + let old_ping_time_ms = self.shared.ping_time().as_millis(); + + // prune old response times. Keep at most two so we can push a third. + while self.network_response_times.len() >= 3 { + self.network_response_times.remove(0); + } + + // record the response time + self.network_response_times.push(response_time); + + // stats::median is experimental. So we calculate the median of up to three ourselves. + let ping_time = { + match self.network_response_times.len() { + 1 => self.network_response_times[0], + 2 => (self.network_response_times[0] + self.network_response_times[1]) / 2, + 3 => { + let mut times = self.network_response_times.clone(); + times.sort_unstable(); + times[1] + } + _ => unreachable!(), + } + }; + + // print when the new estimate deviates by more than 10% from the last + if f32::abs( + (ping_time.as_millis() as f32 - old_ping_time_ms as f32) + / old_ping_time_ms as f32, + ) > 0.1 + { + //trace!( + // "Time to first byte now estimated as: {} ms", + // ping_time.as_millis() + //); + } + + // store our new estimate for everyone to see + self.shared.set_ping_time(ping_time); + } + ReceivedData::Data(data) => { + match self.output.as_mut() { + Some(output) => { + output.seek(SeekFrom::Start(data.offset as u64))?; + output.write_all(data.data.as_ref())?; + } + None => return Err(AudioFileError::Output.into()), + } + + let received_range = Range::new(data.offset, data.data.len()); + + let full = { + let mut download_status = self.shared.download_status.lock(); + download_status.downloaded.add_range(&received_range); + self.shared.cond.notify_all(); + + download_status.downloaded.contained_length_from_value(0) + >= self.shared.file_size + }; + + if full { + self.finish()?; + return Ok(ControlFlow::Break); + } + } + } + + Ok(ControlFlow::Continue) + } + + fn handle_stream_loader_command( + &mut self, + cmd: StreamLoaderCommand, + ) -> Result { + match cmd { + StreamLoaderCommand::Fetch(request) => { + self.download_range(request.start, request.length)? + } + StreamLoaderCommand::Close => return Ok(ControlFlow::Break), + } + + Ok(ControlFlow::Continue) + } + + fn finish(&mut self) -> AudioFileResult { + let output = self.output.take(); + + let complete_tx = self.complete_tx.take(); + + if let Some(mut output) = output { + output.seek(SeekFrom::Start(0))?; + if let Some(complete_tx) = complete_tx { + complete_tx + .send(output) + .map_err(|_| AudioFileError::Channel)?; + } + } + + Ok(()) + } +} + +async fn receive_data( + shared: Arc, + file_data_tx: mpsc::UnboundedSender, + mut request: StreamingRequest, +) -> AudioFileResult { + let mut offset = request.offset; + let mut actual_length = 0; + + let permit = shared.download_slots.acquire().await?; + + let request_time = Instant::now(); + let mut measure_ping_time = true; + let mut measure_throughput = true; + + let result: Result<_, Error> = loop { + let response = match request.initial_response.take() { + Some(data) => { + // the request was already made outside of this function + measure_ping_time = false; + measure_throughput = false; + + data + } + None => match request.streamer.next().await { + Some(Ok(response)) => response, + Some(Err(e)) => break Err(e.into()), + None => { + if actual_length != request.length { + let msg = format!("did not expect body to contain {} bytes", actual_length); + break Err(Error::msg(msg)); + } + + break Ok(()); + } + }, + }; + + if measure_ping_time { + let duration = Instant::now().duration_since(request_time); + // may be zero if we are handling an initial response + if duration.as_millis() > 0 { + file_data_tx + .send(ReceivedData::ResponseTime(duration)) + .unwrap(); + measure_ping_time = false; + } + } + + let code = response.status(); + if code != StatusCode::PARTIAL_CONTENT { + if code == StatusCode::TOO_MANY_REQUESTS { + //if let Some(duration) = HttpClient::get_retry_after(response.headers()) { + // sleeping here means we hold onto this streamer "slot" + // (we don't decrease the number of open requests) + //tokio::time::sleep(duration).await; + // } + } + + break Err(AudioFileError::StatusCode(code).into()); + } + + let body = response.into_body(); + let data = match hyper::body::to_bytes(body).await { + Ok(bytes) => bytes, + Err(e) => break Err(e.into()), + }; + + let data_size = data.len(); + println!(" data_size: {}", data_size); + file_data_tx.send(ReceivedData::Data(PartialFileData { offset, data }))?; + + actual_length += data_size; + offset += data_size; + }; + + drop(request.streamer); + + if measure_throughput { + let duration = Instant::now().duration_since(request_time).as_millis(); + if actual_length > 0 && duration > 0 { + let throughput = ONE_SECOND.as_millis() as usize * actual_length / duration as usize; + file_data_tx.send(ReceivedData::Throughput(throughput))?; + } + } + + let bytes_remaining = request.length - actual_length; + if bytes_remaining > 0 { + { + let missing_range = Range::new(offset, bytes_remaining); + let mut download_status = shared.download_status.lock(); + download_status.requested.subtract_range(&missing_range); + shared.cond.notify_all(); + } + } + + drop(permit); + + if let Err(e) = result { + return Err(e); + } + + Ok(()) +} + +pub(super) async fn audio_file_fetch( + shared: Arc, + initial_request: StreamingRequest, + output: NamedTempFile, + mut stream_loader_command_rx: mpsc::UnboundedReceiver, + complete_tx: oneshot::Sender, +) -> AudioFileResult { + let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel(); + + { + let requested_range = Range::new( + initial_request.offset, + initial_request.offset + initial_request.length, + ); + + let mut download_status = shared.download_status.lock(); + download_status.requested.add_range(&requested_range); + } + + tokio::spawn(receive_data( + shared.clone(), + file_data_tx.clone(), + initial_request, + )); + + let mut fetch = AudioFileFetch { + shared, + output: Some(output), + + file_data_tx, + complete_tx: Some(complete_tx), + network_response_times: Vec::with_capacity(3), + }; + + loop { + tokio::select! { + cmd = stream_loader_command_rx.recv() => { + match cmd { + Some(cmd) => { + if fetch.handle_stream_loader_command(cmd)? == ControlFlow::Break { + break; + } + } + None => break, + } + } + data = file_data_rx.recv() => { + match data { + Some(data) => { + if fetch.handle_file_data(data)? == ControlFlow::Break { + break; + } + } + None => break, + } + }, + else => (), + } + + if fetch.shared.is_download_streaming() && fetch.has_download_slots_available() { + let bytes_pending: usize = { + let download_status = fetch.shared.download_status.lock(); + + download_status + .requested + .minus(&download_status.downloaded) + .len() + }; + + let ping_time_seconds = fetch.shared.ping_time().as_secs_f32(); + let throughput = fetch.shared.throughput(); + + let desired_pending_bytes = max( + (PREFETCH_THRESHOLD_FACTOR + * ping_time_seconds + * fetch.shared.bytes_per_second as f32) as usize, + (ping_time_seconds * throughput as f32) as usize, + ); + + if bytes_pending < desired_pending_bytes { + fetch.pre_fetch_more_data(desired_pending_bytes - bytes_pending)?; + } + } + } + + Ok(()) +} diff --git a/audio/src/lib.rs b/audio/src/lib.rs new file mode 100644 index 00000000..edd3fbf3 --- /dev/null +++ b/audio/src/lib.rs @@ -0,0 +1,3 @@ +pub mod fetch; + +pub mod range_set; diff --git a/audio/src/main.rs b/audio/src/main.rs new file mode 100644 index 00000000..f4a58b4f --- /dev/null +++ b/audio/src/main.rs @@ -0,0 +1,74 @@ +pub mod decoder; +pub mod fetch; +pub mod range_set; + +use decoder::{symphonia_decoder::SymphoniaDecoder, AudioDecoder}; +use fetch::{AudioFile, Subfile}; +use symphonia::core::probe::Hint; + +type Decoder = Box; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let url = "https://raw.githubusercontent.com/tsirysndr/music-player/master/fixtures/audio/06%20-%20J.%20Cole%20-%20Fire%20Squad(Explicit).m4a"; + // let url = "/tmp/audio/06 - J. Cole - Fire Squad(Explicit).m4a"; + let bytes_per_second = 40 * 1024; // 320kbps + let audio_file = match AudioFile::open(url, bytes_per_second).await { + Ok(audio_file) => audio_file, + Err(e) => { + println!("Error: {}", e); + return Ok(()); + } + }; + + let stream_loader_controller = audio_file.get_stream_loader_controller()?; + stream_loader_controller.set_stream_mode(); + let audio_file = match Subfile::new(audio_file, 0, stream_loader_controller.len() as u64) { + Ok(audio_file) => audio_file, + Err(e) => { + println!("Error: {}", e); + return Ok(()); + } + }; + + let symphonia_decoder = |audio_file, format| { + SymphoniaDecoder::new(audio_file, format).map(|decoder| Box::new(decoder) as Decoder) + }; + + let mut format = Hint::new(); + format.mime_type(&AudioFile::get_mime_type(url).await?); + + let decoder_type = symphonia_decoder(audio_file, format); + + let mut decoder = match decoder_type { + Ok(decoder) => decoder, + Err(e) => { + panic!("Failed to create decoder: {}", e); + } + }; + + loop { + match decoder.next_packet() { + Ok(result) => { + if let Some((ref packet_position, packet, channels, sample_rate)) = result { + match packet.samples() { + Ok(samples) => { + println!("Packet: {:?}", packet_position); + } + Err(e) => { + println!("Error: {}", e); + } + } + } else { + break; + } + } + Err(e) => { + println!("Error: {}", e); + break; + } + } + } + + Ok(()) +} diff --git a/audio/src/range_set.rs b/audio/src/range_set.rs new file mode 100644 index 00000000..ee9136f2 --- /dev/null +++ b/audio/src/range_set.rs @@ -0,0 +1,243 @@ +use std::{ + cmp::{max, min}, + fmt, + slice::Iter, +}; + +#[derive(Copy, Clone, Debug)] +pub struct Range { + pub start: usize, + pub length: usize, +} + +impl fmt::Display for Range { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{}, {}]", self.start, self.start + self.length - 1) + } +} + +impl Range { + pub fn new(start: usize, length: usize) -> Range { + Range { start, length } + } + + pub fn end(&self) -> usize { + self.start + self.length + } +} + +#[derive(Debug, Clone)] +pub struct RangeSet { + ranges: Vec, +} + +impl fmt::Display for RangeSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "(")?; + for range in self.ranges.iter() { + write!(f, "{}", range)?; + } + write!(f, ")") + } +} + +impl RangeSet { + pub fn new() -> RangeSet { + RangeSet { + ranges: Vec::::new(), + } + } + + pub fn is_empty(&self) -> bool { + self.ranges.is_empty() + } + + pub fn len(&self) -> usize { + self.ranges.iter().map(|r| r.length).sum() + } + + pub fn get_range(&self, index: usize) -> Range { + self.ranges[index] + } + + pub fn iter(&self) -> Iter<'_, Range> { + self.ranges.iter() + } + + pub fn contains(&self, value: usize) -> bool { + for range in self.ranges.iter() { + if value < range.start { + return false; + } else if range.start <= value && value < range.end() { + return true; + } + } + false + } + + pub fn contained_length_from_value(&self, value: usize) -> usize { + for range in self.ranges.iter() { + if value < range.start { + return 0; + } else if range.start <= value && value < range.end() { + return range.end() - value; + } + } + 0 + } + + #[allow(dead_code)] + pub fn contains_range_set(&self, other: &RangeSet) -> bool { + for range in other.ranges.iter() { + if self.contained_length_from_value(range.start) < range.length { + return false; + } + } + true + } + + pub fn add_range(&mut self, range: &Range) { + if range.length == 0 { + // the interval is empty -> nothing to do. + return; + } + + for index in 0..self.ranges.len() { + // the new range is clear of any ranges we already iterated over. + if range.end() < self.ranges[index].start { + // the new range starts after anything we already passed and ends before the next range starts (they don't touch) -> insert it. + self.ranges.insert(index, *range); + return; + } else if range.start <= self.ranges[index].end() + && self.ranges[index].start <= range.end() + { + // the new range overlaps (or touches) the first range. They are to be merged. + // In addition we might have to merge further ranges in as well. + + let mut new_range = *range; + + while index < self.ranges.len() && self.ranges[index].start <= new_range.end() { + let new_end = max(new_range.end(), self.ranges[index].end()); + new_range.start = min(new_range.start, self.ranges[index].start); + new_range.length = new_end - new_range.start; + self.ranges.remove(index); + } + + self.ranges.insert(index, new_range); + return; + } + } + + // the new range is after everything else -> just add it + self.ranges.push(*range); + } + + #[allow(dead_code)] + pub fn add_range_set(&mut self, other: &RangeSet) { + for range in other.ranges.iter() { + self.add_range(range); + } + } + + #[allow(dead_code)] + pub fn union(&self, other: &RangeSet) -> RangeSet { + let mut result = self.clone(); + result.add_range_set(other); + result + } + + pub fn subtract_range(&mut self, range: &Range) { + if range.length == 0 { + return; + } + + for index in 0..self.ranges.len() { + // the ranges we already passed don't overlap with the range to remove + + if range.end() <= self.ranges[index].start { + // the remaining ranges are past the one to subtract. -> we're done. + return; + } else if range.start <= self.ranges[index].start + && self.ranges[index].start < range.end() + { + // the range to subtract started before the current range and reaches into the current range + // -> we have to remove the beginning of the range or the entire range and do the same for following ranges. + + while index < self.ranges.len() && self.ranges[index].end() <= range.end() { + self.ranges.remove(index); + } + + if index < self.ranges.len() && self.ranges[index].start < range.end() { + self.ranges[index].length -= range.end() - self.ranges[index].start; + self.ranges[index].start = range.end(); + } + + return; + } else if range.end() < self.ranges[index].end() { + // the range to subtract punches a hole into the current range -> we need to create two smaller ranges. + + let first_range = Range { + start: self.ranges[index].start, + length: range.start - self.ranges[index].start, + }; + + self.ranges[index].length -= range.end() - self.ranges[index].start; + self.ranges[index].start = range.end(); + + self.ranges.insert(index, first_range); + + return; + } else if range.start < self.ranges[index].end() { + // the range truncates the existing range -> truncate the range. Let the for loop take care of overlaps with other ranges. + self.ranges[index].length = range.start - self.ranges[index].start; + } + } + } + + pub fn subtract_range_set(&mut self, other: &RangeSet) { + for range in other.ranges.iter() { + self.subtract_range(range); + } + } + + pub fn minus(&self, other: &RangeSet) -> RangeSet { + let mut result = self.clone(); + result.subtract_range_set(other); + result + } + + pub fn intersection(&self, other: &RangeSet) -> RangeSet { + let mut result = RangeSet::new(); + + let mut self_index: usize = 0; + let mut other_index: usize = 0; + + while self_index < self.ranges.len() && other_index < other.ranges.len() { + if self.ranges[self_index].end() <= other.ranges[other_index].start { + // skip the interval + self_index += 1; + } else if other.ranges[other_index].end() <= self.ranges[self_index].start { + // skip the interval + other_index += 1; + } else { + // the two intervals overlap. Add the union and advance the index of the one that ends first. + let new_start = max( + self.ranges[self_index].start, + other.ranges[other_index].start, + ); + let new_end = min( + self.ranges[self_index].end(), + other.ranges[other_index].end(), + ); + result.add_range(&Range::new(new_start, new_end - new_start)); + if self.ranges[self_index].end() <= other.ranges[other_index].end() { + self_index += 1; + } else { + other_index += 1; + } + } + } + + result + } +} diff --git a/cargo-bazel-lock.json b/cargo-bazel-lock.json index cbbc9541..3c261b3d 100644 --- a/cargo-bazel-lock.json +++ b/cargo-bazel-lock.json @@ -1,5 +1,5 @@ { - "checksum": "3672372300856738fbde283675432be6ef29704c02a1ccc2c323cac506d4b35e", + "checksum": "965377660ff5f30baa5541d2a1cf060f99fac0397422b44f73df95ef0867a551", "crates": { "Inflector 0.11.4": { "name": "Inflector", @@ -125,7 +125,7 @@ "target": "log" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -141,7 +141,7 @@ "target": "smallvec" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -224,7 +224,7 @@ "target": "pin_project_lite" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -286,7 +286,7 @@ "target": "log" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -682,7 +682,7 @@ "target": "regex" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -732,7 +732,7 @@ "target": "futures_core" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -809,7 +809,7 @@ "target": "socket2" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -1048,7 +1048,7 @@ "target": "mime" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -1060,7 +1060,7 @@ "target": "regex" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -1175,7 +1175,7 @@ "target": "pin_project_lite" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -1642,7 +1642,7 @@ ], "cfg(not(all(target_arch = \"arm\", target_os = \"none\")))": [ { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" } ] @@ -2143,7 +2143,7 @@ "target": "futures" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -2155,7 +2155,7 @@ "target": "tauri" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -2621,11 +2621,11 @@ "target": "futures_lite" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio", "alias": "tokio_crate" } @@ -2725,7 +2725,7 @@ "target": "num_traits" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -2737,7 +2737,7 @@ "target": "regex" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -2967,11 +2967,11 @@ "target": "async_graphql_value" }, { - "id": "pest 2.5.1", + "id": "pest 2.5.2", "target": "pest" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -3097,7 +3097,7 @@ "target": "indexmap" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -3521,7 +3521,7 @@ "target": "rand" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -3702,7 +3702,7 @@ "target": "memchr" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -4572,7 +4572,7 @@ "target": "pin_project_lite" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -4931,7 +4931,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -5774,13 +5774,13 @@ }, "license": "BSD-3-Clause/MIT" }, - "bstr 0.2.17": { + "bstr 1.1.0": { "name": "bstr", - "version": "0.2.17", + "version": "1.1.0", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/bstr/0.2.17/download", - "sha256": "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" + "url": "https://crates.io/api/v1/crates/bstr/1.1.0/download", + "sha256": "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b" } }, "targets": [ @@ -5803,6 +5803,7 @@ "**" ], "crate_features": [ + "alloc", "std" ], "deps": { @@ -5810,12 +5811,16 @@ { "id": "memchr 2.5.0", "target": "memchr" + }, + { + "id": "serde 1.0.152", + "target": "serde" } ], "selects": {} }, - "edition": "2018", - "version": "0.2.17" + "edition": "2021", + "version": "1.1.0" }, "license": "MIT OR Apache-2.0" }, @@ -6162,7 +6167,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -6360,13 +6365,13 @@ }, "license": "MIT" }, - "cargo_toml 0.13.0": { + "cargo_toml 0.13.3": { "name": "cargo_toml", - "version": "0.13.0", + "version": "0.13.3", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/cargo_toml/0.13.0/download", - "sha256": "aa0e3586af56b3bfa51fca452bd56e8dbbbd5d8d81cbf0b7e4e35b695b537eb8" + "url": "https://crates.io/api/v1/crates/cargo_toml/0.13.3/download", + "sha256": "497049e9477329f8f6a559972ee42e117487d01d1e8c2cc9f836ea6fa23a9e1a" } }, "targets": [ @@ -6391,7 +6396,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -6402,7 +6407,7 @@ "selects": {} }, "edition": "2021", - "version": "0.13.0" + "version": "0.13.3" }, "license": "Apache-2.0 OR MIT" }, @@ -6594,7 +6599,7 @@ "deps": { "common": [ { - "id": "nom 7.1.1", + "id": "nom 7.1.2", "target": "nom" } ], @@ -6863,7 +6868,7 @@ "target": "num_traits" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -7102,7 +7107,7 @@ "target": "indexmap" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -7636,7 +7641,7 @@ "target": "lazy_static" }, { - "id": "nom 7.1.1", + "id": "nom 7.1.2", "target": "nom" }, { @@ -7652,7 +7657,7 @@ "target": "ini" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -8471,7 +8476,7 @@ ], "cfg(target_os = \"windows\")": [ { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -9104,7 +9109,7 @@ "target": "parking_lot" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -10045,7 +10050,7 @@ "target": "codespan_reporting" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -11604,7 +11609,7 @@ "target": "build_script_build" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -11780,7 +11785,7 @@ "target": "log" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -12049,7 +12054,7 @@ "target": "log" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -12078,7 +12083,7 @@ "proc_macro_deps": { "common": [ { - "id": "serde_derive 1.0.151", + "id": "serde_derive 1.0.152", "target": "serde_derive" } ], @@ -14280,7 +14285,7 @@ "target": "libc" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -14464,7 +14469,7 @@ "target": "libc" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -14663,13 +14668,13 @@ }, "license": "MIT/Apache-2.0" }, - "globset 0.4.9": { + "globset 0.4.10": { "name": "globset", - "version": "0.4.9", + "version": "0.4.10", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/globset/0.4.9/download", - "sha256": "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" + "url": "https://crates.io/api/v1/crates/globset/0.4.10/download", + "sha256": "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" } }, "targets": [ @@ -14702,7 +14707,7 @@ "target": "aho_corasick" }, { - "id": "bstr 0.2.17", + "id": "bstr 1.1.0", "target": "bstr" }, { @@ -14721,7 +14726,7 @@ "selects": {} }, "edition": "2018", - "version": "0.4.9" + "version": "0.4.10" }, "license": "Unlicense OR MIT" }, @@ -14958,7 +14963,7 @@ "target": "libc" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -15243,7 +15248,7 @@ "target": "slab" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -16340,7 +16345,7 @@ "target": "rand" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -16593,7 +16598,7 @@ "target": "socket2" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -16616,6 +16621,82 @@ }, "license": "MIT" }, + "hyper-rustls 0.23.2": { + "name": "hyper-rustls", + "version": "0.23.2", + "repository": { + "Http": { + "url": "https://crates.io/api/v1/crates/hyper-rustls/0.23.2/download", + "sha256": "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" + } + }, + "targets": [ + { + "Library": { + "crate_name": "hyper_rustls", + "crate_root": "src/lib.rs", + "srcs": { + "include": [ + "**/*.rs" + ], + "exclude": [] + } + } + } + ], + "library_target_name": "hyper_rustls", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": [ + "default", + "http1", + "log", + "logging", + "native-tokio", + "rustls-native-certs", + "tls12", + "tokio-runtime" + ], + "deps": { + "common": [ + { + "id": "http 0.2.8", + "target": "http" + }, + { + "id": "hyper 0.14.23", + "target": "hyper" + }, + { + "id": "log 0.4.17", + "target": "log" + }, + { + "id": "rustls 0.20.7", + "target": "rustls" + }, + { + "id": "rustls-native-certs 0.6.2", + "target": "rustls_native_certs" + }, + { + "id": "tokio 1.24.0", + "target": "tokio" + }, + { + "id": "tokio-rustls 0.23.4", + "target": "tokio_rustls" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.23.2" + }, + "license": "Apache-2.0/ISC/MIT" + }, "hyper-timeout 0.4.1": { "name": "hyper-timeout", "version": "0.4.1", @@ -16655,7 +16736,7 @@ "target": "pin_project_lite" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -16991,13 +17072,13 @@ }, "license": "MIT OR BSD-3-Clause" }, - "ignore 0.4.18": { + "ignore 0.4.19": { "name": "ignore", - "version": "0.4.18", + "version": "0.4.19", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/ignore/0.4.18/download", - "sha256": "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" + "url": "https://crates.io/api/v1/crates/ignore/0.4.19/download", + "sha256": "a05705bc64e0b66a806c3740bd6578ea66051b157ec42dc219c785cbf185aef3" } }, "targets": [ @@ -17022,11 +17103,7 @@ "deps": { "common": [ { - "id": "crossbeam-utils 0.8.14", - "target": "crossbeam_utils" - }, - { - "id": "globset 0.4.9", + "id": "globset 0.4.10", "target": "globset" }, { @@ -17068,9 +17145,9 @@ } }, "edition": "2018", - "version": "0.4.18" + "version": "0.4.19" }, - "license": "Unlicense/MIT" + "license": "Unlicense OR MIT" }, "image 0.24.5": { "name": "image", @@ -17185,7 +17262,7 @@ "target": "build_script_build" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -17510,7 +17587,7 @@ "target": "log" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -18143,7 +18220,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -18193,11 +18270,11 @@ "deps": { "common": [ { - "id": "pest 2.5.1", + "id": "pest 2.5.2", "target": "pest" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -18207,7 +18284,7 @@ "proc_macro_deps": { "common": [ { - "id": "pest_derive 2.5.1", + "id": "pest_derive 2.5.2", "target": "pest_derive" } ], @@ -18743,7 +18820,7 @@ "target": "thiserror" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -19461,7 +19538,7 @@ "target": "ogg_pager" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" } ], @@ -19594,7 +19671,7 @@ "target": "build_script_build" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -19669,7 +19746,7 @@ "target": "scoped_tls" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -20323,13 +20400,13 @@ }, "license": "MIT" }, - "mdns-sd 0.5.9": { + "mdns-sd 0.5.10": { "name": "mdns-sd", - "version": "0.5.9", + "version": "0.5.10", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/mdns-sd/0.5.9/download", - "sha256": "69b070a70988c737646f0ff395f4317934c4a90ec14910477e3d1d5a5e22f60d" + "url": "https://crates.io/api/v1/crates/mdns-sd/0.5.10/download", + "sha256": "709cba29c9d7334db28706bc2767db2531934fd2e55781cba82c930fb7f22b47" } }, "targets": [ @@ -20383,7 +20460,7 @@ "selects": {} }, "edition": "2018", - "version": "0.5.9" + "version": "0.5.10" }, "license": "Apache-2.0 OR MIT" }, @@ -21212,7 +21289,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -21290,6 +21367,10 @@ ], "deps": { "common": [ + { + "id": "anyhow 1.0.68", + "target": "anyhow" + }, { "id": "clap 3.2.23", "target": "clap" @@ -21343,7 +21424,7 @@ "target": "tabled" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -21385,11 +21466,134 @@ "compile_data_glob": [ "**" ], + "deps": { + "common": [ + { + "id": "anyhow 1.0.68", + "target": "anyhow" + }, + { + "id": "surf 2.3.2", + "target": "surf" + } + ], + "selects": {} + }, "edition": "2021", + "proc_macro_deps": { + "common": [ + { + "id": "async-trait 0.1.60", + "target": "async_trait" + } + ], + "selects": {} + }, "version": "0.1.0" }, "license": "MIT" }, + "music-player-audio 0.1.0": { + "name": "music-player-audio", + "version": "0.1.0", + "repository": null, + "targets": [ + { + "Library": { + "crate_name": "music_player_audio", + "crate_root": "src/lib.rs", + "srcs": { + "include": [ + "**/*.rs" + ], + "exclude": [] + } + } + }, + { + "Binary": { + "crate_name": "music-player-audio", + "crate_root": "src/main.rs", + "srcs": { + "include": [ + "**/*.rs" + ], + "exclude": [] + } + } + } + ], + "library_target_name": "music_player_audio", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "anyhow 1.0.68", + "target": "anyhow" + }, + { + "id": "bytes 1.3.0", + "target": "bytes" + }, + { + "id": "futures-util 0.3.25", + "target": "futures_util" + }, + { + "id": "hyper 0.14.23", + "target": "hyper" + }, + { + "id": "hyper-rustls 0.23.2", + "target": "hyper_rustls" + }, + { + "id": "md5 0.7.0", + "target": "md5" + }, + { + "id": "mime_guess 2.0.4", + "target": "mime_guess" + }, + { + "id": "parking_lot 0.12.1", + "target": "parking_lot" + }, + { + "id": "rustls 0.20.7", + "target": "rustls" + }, + { + "id": "symphonia 0.5.1", + "target": "symphonia" + }, + { + "id": "tempfile 3.3.0", + "target": "tempfile" + }, + { + "id": "thiserror 1.0.38", + "target": "thiserror" + }, + { + "id": "tokio 1.24.0", + "target": "tokio" + }, + { + "id": "url 2.3.1", + "target": "url" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.1.0" + }, + "license": null + }, "music-player-client 0.1.1": { "name": "music-player-client", "version": "0.1.1", @@ -21415,12 +21619,16 @@ ], "deps": { "common": [ + { + "id": "anyhow 1.0.68", + "target": "anyhow" + }, { "id": "futures-util 0.3.25", "target": "futures_util" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -21514,7 +21722,7 @@ "target": "mdns" }, { - "id": "mdns-sd 0.5.9", + "id": "mdns-sd 0.5.10", "target": "mdns_sd" }, { @@ -21522,7 +21730,7 @@ "target": "owo_colors" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -21571,7 +21779,7 @@ "target": "sea_orm" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -21619,6 +21827,10 @@ ], "deps": { "common": [ + { + "id": "anyhow 1.0.68", + "target": "anyhow" + }, { "id": "async-graphql 4.0.16", "target": "async_graphql" @@ -21648,7 +21860,11 @@ "target": "md5" }, { - "id": "once_cell 1.16.0", + "id": "mdns-sd 0.5.10", + "target": "mdns_sd" + }, + { + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -21660,7 +21876,7 @@ "target": "sea_orm" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -21672,7 +21888,7 @@ "target": "tide" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -21738,7 +21954,7 @@ "target": "sea_orm_migration" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -21827,9 +22043,13 @@ "target": "thiserror" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, + { + "id": "url 2.3.1", + "target": "url" + }, { "id": "zerocopy 0.6.1", "target": "zerocopy" @@ -21913,7 +22133,7 @@ "target": "sea_orm" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -22018,11 +22238,11 @@ "target": "sea_orm" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -22105,7 +22325,7 @@ "target": "md5" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -22149,6 +22369,10 @@ ], "deps": { "common": [ + { + "id": "anyhow 1.0.68", + "target": "anyhow" + }, { "id": "itertools 0.10.5", "target": "itertools" @@ -22170,7 +22394,7 @@ "target": "tempfile" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -22255,6 +22479,10 @@ "id": "md5 0.7.0", "target": "md5" }, + { + "id": "mdns-sd 0.5.10", + "target": "mdns_sd" + }, { "id": "tantivy 0.18.1", "target": "tantivy" @@ -22328,6 +22556,10 @@ "id": "dirs 4.0.0", "target": "dirs" }, + { + "id": "futures-util 0.3.25", + "target": "futures_util" + }, { "id": "mime_guess 2.0.4", "target": "mime_guess" @@ -22345,11 +22577,11 @@ "target": "sea_orm" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -22357,10 +22589,6 @@ }, "deps_dev": { "common": [ - { - "id": "futures-util 0.3.25", - "target": "futures_util" - }, { "id": "serde_json 1.0.91", "target": "serde_json" @@ -22376,7 +22604,7 @@ "proc_macro_deps": { "common": [ { - "id": "serde_derive 1.0.151", + "id": "serde_derive 1.0.152", "target": "serde_derive" } ], @@ -22811,13 +23039,13 @@ }, "license": "MIT/Apache-2.0" }, - "nom 7.1.1": { + "nom 7.1.2": { "name": "nom", - "version": "7.1.1", + "version": "7.1.2", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/nom/7.1.1/download", - "sha256": "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" + "url": "https://crates.io/api/v1/crates/nom/7.1.2/download", + "sha256": "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" } }, "targets": [ @@ -22858,7 +23086,7 @@ "selects": {} }, "edition": "2018", - "version": "7.1.1" + "version": "7.1.2" }, "license": "MIT" }, @@ -23809,13 +24037,13 @@ }, "license": "MIT OR Apache-2.0" }, - "once_cell 1.16.0": { + "once_cell 1.17.0": { "name": "once_cell", - "version": "1.16.0", + "version": "1.17.0", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/once_cell/1.16.0/download", - "sha256": "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + "url": "https://crates.io/api/v1/crates/once_cell/1.17.0/download", + "sha256": "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" } }, "targets": [ @@ -23844,7 +24072,7 @@ "std" ], "edition": "2021", - "version": "1.16.0" + "version": "1.17.0" }, "license": "MIT OR Apache-2.0" }, @@ -24442,7 +24670,7 @@ "target": "libc" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -25063,13 +25291,13 @@ }, "license": "MIT OR Apache-2.0" }, - "pest 2.5.1": { + "pest 2.5.2": { "name": "pest", - "version": "2.5.1", + "version": "2.5.2", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/pest/2.5.1/download", - "sha256": "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" + "url": "https://crates.io/api/v1/crates/pest/2.5.2/download", + "sha256": "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4" } }, "targets": [ @@ -25110,17 +25338,17 @@ "selects": {} }, "edition": "2021", - "version": "2.5.1" + "version": "2.5.2" }, "license": "MIT/Apache-2.0" }, - "pest_derive 2.5.1": { + "pest_derive 2.5.2": { "name": "pest_derive", - "version": "2.5.1", + "version": "2.5.2", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/pest_derive/2.5.1/download", - "sha256": "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344" + "url": "https://crates.io/api/v1/crates/pest_derive/2.5.2/download", + "sha256": "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603" } }, "targets": [ @@ -25149,28 +25377,28 @@ "deps": { "common": [ { - "id": "pest 2.5.1", + "id": "pest 2.5.2", "target": "pest" }, { - "id": "pest_generator 2.5.1", + "id": "pest_generator 2.5.2", "target": "pest_generator" } ], "selects": {} }, "edition": "2021", - "version": "2.5.1" + "version": "2.5.2" }, "license": "MIT/Apache-2.0" }, - "pest_generator 2.5.1": { + "pest_generator 2.5.2": { "name": "pest_generator", - "version": "2.5.1", + "version": "2.5.2", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/pest_generator/2.5.1/download", - "sha256": "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c" + "url": "https://crates.io/api/v1/crates/pest_generator/2.5.2/download", + "sha256": "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7" } }, "targets": [ @@ -25198,11 +25426,11 @@ "deps": { "common": [ { - "id": "pest 2.5.1", + "id": "pest 2.5.2", "target": "pest" }, { - "id": "pest_meta 2.5.1", + "id": "pest_meta 2.5.2", "target": "pest_meta" }, { @@ -25221,17 +25449,17 @@ "selects": {} }, "edition": "2021", - "version": "2.5.1" + "version": "2.5.2" }, "license": "MIT/Apache-2.0" }, - "pest_meta 2.5.1": { + "pest_meta 2.5.2": { "name": "pest_meta", - "version": "2.5.1", + "version": "2.5.2", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/pest_meta/2.5.1/download", - "sha256": "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20" + "url": "https://crates.io/api/v1/crates/pest_meta/2.5.2/download", + "sha256": "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065" } }, "targets": [ @@ -25256,18 +25484,18 @@ "deps": { "common": [ { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { - "id": "pest 2.5.1", + "id": "pest 2.5.2", "target": "pest" } ], "selects": {} }, "edition": "2021", - "version": "2.5.1" + "version": "2.5.2" }, "license": "MIT/Apache-2.0" }, @@ -26100,7 +26328,7 @@ "target": "line_wrap" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -26464,7 +26692,7 @@ "target": "postgres_protocol" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde", "alias": "serde_1" }, @@ -26629,13 +26857,13 @@ }, "license": "MIT OR Apache-2.0" }, - "prettyplease 0.1.22": { + "prettyplease 0.1.23": { "name": "prettyplease", - "version": "0.1.22", + "version": "0.1.23", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/prettyplease/0.1.22/download", - "sha256": "2c8992a85d8e93a28bdf76137db888d3874e3b230dee5ed8bebac4c9f7617773" + "url": "https://crates.io/api/v1/crates/prettyplease/0.1.23/download", + "sha256": "e97e3215779627f01ee256d2fad52f3d95e8e1c11e9fc6fd08f7cd455d5d5c78" } }, "targets": [ @@ -26672,7 +26900,7 @@ "deps": { "common": [ { - "id": "prettyplease 0.1.22", + "id": "prettyplease 0.1.23", "target": "build_script_build" }, { @@ -26687,7 +26915,7 @@ "selects": {} }, "edition": "2021", - "version": "0.1.22" + "version": "0.1.23" }, "build_script_attrs": { "data_glob": [ @@ -26770,7 +26998,7 @@ "deps": { "common": [ { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -27207,7 +27435,7 @@ "target": "petgraph" }, { - "id": "prettyplease 0.1.22", + "id": "prettyplease 0.1.23", "target": "prettyplease" }, { @@ -28863,13 +29091,13 @@ "target": "libc" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" } ], "cfg(any(target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"illumos\", target_os = \"netbsd\", target_os = \"openbsd\", target_os = \"solaris\"))": [ { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" } ], @@ -29158,7 +29386,7 @@ "target": "bitflags" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -29461,7 +29689,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -29471,7 +29699,7 @@ "proc_macro_deps": { "common": [ { - "id": "serde_derive 1.0.151", + "id": "serde_derive 1.0.152", "target": "serde_derive" } ], @@ -29565,7 +29793,7 @@ "target": "build_script_build" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -29830,6 +30058,67 @@ }, "license": "Apache-2.0/ISC/MIT" }, + "rustls-native-certs 0.6.2": { + "name": "rustls-native-certs", + "version": "0.6.2", + "repository": { + "Http": { + "url": "https://crates.io/api/v1/crates/rustls-native-certs/0.6.2/download", + "sha256": "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" + } + }, + "targets": [ + { + "Library": { + "crate_name": "rustls_native_certs", + "crate_root": "src/lib.rs", + "srcs": { + "include": [ + "**/*.rs" + ], + "exclude": [] + } + } + } + ], + "library_target_name": "rustls_native_certs", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "rustls-pemfile 1.0.1", + "target": "rustls_pemfile" + } + ], + "selects": { + "cfg(all(unix, not(target_os = \"macos\")))": [ + { + "id": "openssl-probe 0.1.5", + "target": "openssl_probe" + } + ], + "cfg(target_os = \"macos\")": [ + { + "id": "security-framework 2.7.0", + "target": "security_framework" + } + ], + "cfg(windows)": [ + { + "id": "schannel 0.1.20", + "target": "schannel" + } + ] + } + }, + "edition": "2018", + "version": "0.6.2" + }, + "license": "Apache-2.0/ISC/MIT" + }, "rustls-pemfile 1.0.1": { "name": "rustls-pemfile", "version": "1.0.1", @@ -30337,7 +30626,7 @@ "target": "log" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -30357,7 +30646,7 @@ "target": "strum" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -31130,6 +31419,117 @@ }, "license": "MIT" }, + "security-framework 2.7.0": { + "name": "security-framework", + "version": "2.7.0", + "repository": { + "Http": { + "url": "https://crates.io/api/v1/crates/security-framework/2.7.0/download", + "sha256": "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" + } + }, + "targets": [ + { + "Library": { + "crate_name": "security_framework", + "crate_root": "src/lib.rs", + "srcs": { + "include": [ + "**/*.rs" + ], + "exclude": [] + } + } + } + ], + "library_target_name": "security_framework", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": [ + "OSX_10_9", + "default" + ], + "deps": { + "common": [ + { + "id": "bitflags 1.3.2", + "target": "bitflags" + }, + { + "id": "core-foundation 0.9.3", + "target": "core_foundation" + }, + { + "id": "core-foundation-sys 0.8.3", + "target": "core_foundation_sys" + }, + { + "id": "libc 0.2.139", + "target": "libc" + }, + { + "id": "security-framework-sys 2.6.1", + "target": "security_framework_sys" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "2.7.0" + }, + "license": "MIT OR Apache-2.0" + }, + "security-framework-sys 2.6.1": { + "name": "security-framework-sys", + "version": "2.6.1", + "repository": { + "Http": { + "url": "https://crates.io/api/v1/crates/security-framework-sys/2.6.1/download", + "sha256": "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" + } + }, + "targets": [ + { + "Library": { + "crate_name": "security_framework_sys", + "crate_root": "src/lib.rs", + "srcs": { + "include": [ + "**/*.rs" + ], + "exclude": [] + } + } + } + ], + "library_target_name": "security_framework_sys", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": [ + "OSX_10_9" + ], + "deps": { + "common": [ + { + "id": "core-foundation-sys 0.8.3", + "target": "core_foundation_sys" + }, + { + "id": "libc 0.2.139", + "target": "libc" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "2.6.1" + }, + "license": "MIT OR Apache-2.0" + }, "selectors 0.22.0": { "name": "selectors", "version": "0.22.0", @@ -31389,7 +31789,7 @@ "target": "build_script_build" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -31448,7 +31848,7 @@ "deps": { "common": [ { - "id": "pest 2.5.1", + "id": "pest 2.5.2", "target": "pest" } ], @@ -31492,13 +31892,13 @@ }, "license": "MIT/Apache-2.0" }, - "serde 1.0.151": { + "serde 1.0.152": { "name": "serde", - "version": "1.0.151", + "version": "1.0.152", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/serde/1.0.151/download", - "sha256": "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" + "url": "https://crates.io/api/v1/crates/serde/1.0.152/download", + "sha256": "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" } }, "targets": [ @@ -31543,7 +31943,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "build_script_build" } ], @@ -31553,13 +31953,13 @@ "proc_macro_deps": { "common": [ { - "id": "serde_derive 1.0.151", + "id": "serde_derive 1.0.152", "target": "serde_derive" } ], "selects": {} }, - "version": "1.0.151" + "version": "1.0.152" }, "build_script_attrs": { "data_glob": [ @@ -31568,13 +31968,13 @@ }, "license": "MIT OR Apache-2.0" }, - "serde_derive 1.0.151": { + "serde_derive 1.0.152": { "name": "serde_derive", - "version": "1.0.151", + "version": "1.0.152", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/serde_derive/1.0.151/download", - "sha256": "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" + "url": "https://crates.io/api/v1/crates/serde_derive/1.0.152/download", + "sha256": "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" } }, "targets": [ @@ -31622,7 +32022,7 @@ "target": "quote" }, { - "id": "serde_derive 1.0.151", + "id": "serde_derive 1.0.152", "target": "build_script_build" }, { @@ -31633,7 +32033,7 @@ "selects": {} }, "edition": "2015", - "version": "1.0.151" + "version": "1.0.152" }, "build_script_attrs": { "data_glob": [ @@ -31673,7 +32073,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -31740,7 +32140,7 @@ "target": "ryu" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -31798,7 +32198,7 @@ "target": "percent_encoding" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -31906,7 +32306,7 @@ "target": "ryu" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -31953,7 +32353,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -32058,7 +32458,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -33220,7 +33620,7 @@ "target": "libc" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -33562,7 +33962,7 @@ "target": "itertools" }, { - "id": "nom 7.1.1", + "id": "nom 7.1.2", "target": "nom" }, { @@ -33803,7 +34203,7 @@ "target": "num_bigint" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -33823,7 +34223,7 @@ "target": "rustls_pemfile" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -33947,7 +34347,7 @@ "target": "heck" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -34029,11 +34429,11 @@ "deps": { "common": [ { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -34369,7 +34769,7 @@ "target": "quote" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -34383,7 +34783,7 @@ "proc_macro_deps": { "common": [ { - "id": "serde_derive 1.0.151", + "id": "serde_derive 1.0.152", "target": "serde_derive" } ], @@ -34436,7 +34836,7 @@ "target": "quote" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -34458,7 +34858,7 @@ "proc_macro_deps": { "common": [ { - "id": "serde_derive 1.0.151", + "id": "serde_derive 1.0.152", "target": "serde_derive" } ], @@ -34570,7 +34970,7 @@ "target": "debug_unreachable" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -34586,7 +34986,7 @@ "target": "precomputed_hash" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -34951,7 +35351,7 @@ "target": "mime_guess" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -34959,7 +35359,7 @@ "target": "pin_project_lite" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -35025,7 +35425,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde", "alias": "serde1_lib" } @@ -36370,7 +36770,7 @@ "target": "num_cpus" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -36398,7 +36798,7 @@ "target": "rust_stemmers" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -36634,7 +37034,7 @@ "target": "combine" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -36723,7 +37123,7 @@ "target": "raw_window_handle" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -36810,7 +37210,7 @@ "target": "ndk_sys" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" } ], @@ -37034,11 +37434,11 @@ "target": "http" }, { - "id": "ignore 0.4.18", + "id": "ignore 0.4.19", "target": "ignore" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -37058,7 +37458,7 @@ "target": "semver" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -37102,7 +37502,7 @@ "target": "thiserror" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -37182,7 +37582,7 @@ "target": "heck" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" } ], @@ -37226,7 +37626,7 @@ "target": "anyhow" }, { - "id": "cargo_toml 0.13.0", + "id": "cargo_toml 0.13.3", "target": "cargo_toml" }, { @@ -37331,7 +37731,7 @@ "target": "semver" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -37502,7 +37902,7 @@ "target": "raw_window_handle" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -37757,7 +38157,7 @@ "target": "semver" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -38188,7 +38588,7 @@ "deps": { "common": [ { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" } ], @@ -38288,7 +38688,7 @@ "target": "route_recognizer" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -38367,7 +38767,7 @@ "target": "pin_project" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -38600,7 +39000,7 @@ "target": "itoa" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -38893,13 +39293,13 @@ }, "license": "MIT OR Apache-2.0 OR Zlib" }, - "tokio 1.23.0": { + "tokio 1.24.0": { "name": "tokio", - "version": "1.23.0", + "version": "1.24.0", "repository": { "Http": { - "url": "https://crates.io/api/v1/crates/tokio/1.23.0/download", - "sha256": "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" + "url": "https://crates.io/api/v1/crates/tokio/1.24.0/download", + "sha256": "7125661431c26622a80ca5051a2f936c9a678318e0351007b0cc313143024e5c" } }, "targets": [ @@ -38986,7 +39386,7 @@ "target": "pin_project_lite" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "build_script_build" } ], @@ -39031,7 +39431,7 @@ ], "selects": {} }, - "version": "1.23.0" + "version": "1.24.0" }, "build_script_attrs": { "data_glob": [ @@ -39084,7 +39484,7 @@ "target": "pin_project_lite" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -39185,7 +39585,7 @@ "target": "rustls" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -39244,7 +39644,7 @@ "target": "pin_project_lite" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" } ], @@ -39299,7 +39699,7 @@ "target": "log" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -39367,7 +39767,7 @@ "target": "pin_project_lite" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -39416,7 +39816,7 @@ "deps": { "common": [ { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -39532,7 +39932,7 @@ "alias": "prost1" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -39621,7 +40021,7 @@ "deps": { "common": [ { - "id": "prettyplease 0.1.22", + "id": "prettyplease 0.1.23", "target": "prettyplease" }, { @@ -39809,7 +40209,7 @@ "target": "slab" }, { - "id": "tokio 1.23.0", + "id": "tokio 1.24.0", "target": "tokio" }, { @@ -40147,7 +40547,7 @@ "deps": { "common": [ { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" } ], @@ -40328,7 +40728,7 @@ "target": "nu_ansi_term" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -40494,7 +40894,7 @@ "target": "crossterm" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -41228,7 +41628,7 @@ "target": "percent_encoding" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -41385,7 +41785,7 @@ "target": "getrandom" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" } ], @@ -41517,7 +41917,7 @@ "alias": "erased_serde1" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde", "alias": "serde1_lib" }, @@ -42000,7 +42400,7 @@ "target": "cfg_if" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -42075,7 +42475,7 @@ "target": "log" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -42492,7 +42892,7 @@ "target": "libc" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { @@ -42928,7 +43328,7 @@ "target": "regex" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -43066,7 +43466,7 @@ "selects": { "cfg(windows)": [ { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" } ] @@ -45417,11 +45817,11 @@ "target": "log" }, { - "id": "once_cell 1.16.0", + "id": "once_cell 1.17.0", "target": "once_cell" }, { - "id": "serde 1.0.151", + "id": "serde 1.0.152", "target": "serde" }, { @@ -46197,6 +46597,7 @@ "app 0.1.0": "webui/musicplayer/src-tauri", "music-player 0.2.0-alpha.8": "", "music-player-addons 0.1.0": "addons", + "music-player-audio 0.1.0": "audio", "music-player-client 0.1.1": "client", "music-player-discovery 0.1.1": "discovery", "music-player-entity 0.1.5": "entity", @@ -46815,4 +47216,4 @@ "x86_64-uwp-windows-gnu": [], "x86_64-uwp-windows-msvc": [] } -} +} \ No newline at end of file diff --git a/client/BUILD b/client/BUILD index 6a1b3cda..39a48cb0 100644 --- a/client/BUILD +++ b/client/BUILD @@ -19,6 +19,7 @@ rust_library( "//playback:music_player_playback", "//storage:music_player_storage", "//tracklist:music_player_tracklist", + "//types:music_player_types", "@crate_index//:tonic", "@crate_index//:futures-util", "@crate_index//:url", diff --git a/client/Cargo.toml b/client/Cargo.toml index 6a433c71..32e70f64 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -22,6 +22,10 @@ version = "0.1.9" path = "../settings" version = "0.1.1" +[dependencies.music-player-types] +path = "../types" +version = "0.1.1" + [dev-dependencies.music-player-playback] path = "../playback" version = "0.1.7" @@ -40,4 +44,5 @@ tokio-tungstenite = "0.17.2" tonic = "0.8.1" url = "2.3.1" tokio = { version = "1.21.2", features = ["full"] } +anyhow = "1.0.67" diff --git a/client/src/library.rs b/client/src/library.rs index dc4f8287..87501893 100644 --- a/client/src/library.rs +++ b/client/src/library.rs @@ -1,11 +1,11 @@ +use anyhow::Error; use music_player_server::api::{ metadata::v1alpha1::{Album, Artist, Track}, music::v1alpha1::{ library_service_client::LibraryServiceClient, GetAlbumDetailsRequest, GetAlbumsRequest, - GetArtistDetailsRequest, GetArtistsRequest, GetTracksRequest, + GetArtistDetailsRequest, GetArtistsRequest, GetTrackDetailsRequest, GetTracksRequest, }, }; -use music_player_settings::{read_settings, Settings}; use tonic::transport::Channel; pub struct LibraryClient { @@ -13,45 +13,49 @@ pub struct LibraryClient { } impl LibraryClient { - pub async fn new(port: u16) -> Result> { - let config = read_settings().unwrap(); - let settings = config.try_deserialize::().unwrap(); - let url = format!("http://{}:{}", settings.host, port); + pub async fn new(host: String, port: u16) -> Result { + let url = format!("tcp://{}:{}", host, port); let client = LibraryServiceClient::connect(url).await?; Ok(Self { client }) } - pub async fn album(&mut self, id: &str) -> Result, Box> { + pub async fn album(&mut self, id: &str) -> Result, Error> { let request = tonic::Request::new(GetAlbumDetailsRequest { id: id.to_string() }); let response = self.client.get_album_details(request).await?; Ok(response.into_inner().album) } - pub async fn albums(&mut self) -> Result, Box> { - let request = tonic::Request::new(GetAlbumsRequest {}); + pub async fn albums(&mut self, offset: i32, limit: i32) -> Result, Error> { + let request = tonic::Request::new(GetAlbumsRequest { offset, limit }); let response = self.client.get_albums(request).await?; Ok(response.into_inner().albums.into_iter().collect()) } - pub async fn artist(&mut self, id: &str) -> Result, Box> { + pub async fn artist(&mut self, id: &str) -> Result, Error> { let request = tonic::Request::new(GetArtistDetailsRequest { id: id.to_string() }); let response = self.client.get_artist_details(request).await?; Ok(response.into_inner().artist) } - pub async fn artists(&mut self) -> Result, Box> { - let request = tonic::Request::new(GetArtistsRequest {}); + pub async fn artists(&mut self, offset: i32, limit: i32) -> Result, Error> { + let request = tonic::Request::new(GetArtistsRequest { offset, limit }); let response = self.client.get_artists(request).await?; Ok(response.into_inner().artists.into_iter().collect()) } - pub async fn songs(&mut self) -> Result, Box> { - let request = tonic::Request::new(GetTracksRequest {}); + pub async fn songs(&mut self, offset: i32, limit: i32) -> Result, Error> { + let request = tonic::Request::new(GetTracksRequest { offset, limit }); let response = self.client.get_tracks(request).await?; Ok(response.into_inner().tracks.into_iter().collect()) } - pub async fn search(&mut self, query: &str) -> Result<(), Box> { + pub async fn song(&mut self, id: &str) -> Result, Error> { + let request = tonic::Request::new(GetTrackDetailsRequest { id: id.to_string() }); + let response = self.client.get_track_details(request).await?; + Ok(response.into_inner().track) + } + + pub async fn search(&mut self, query: &str) -> Result<(), Error> { todo!() } } diff --git a/client/src/playback.rs b/client/src/playback.rs index ba17d291..d500c582 100644 --- a/client/src/playback.rs +++ b/client/src/playback.rs @@ -1,3 +1,4 @@ +use anyhow::Error; use music_player_server::api::{ metadata::v1alpha1::Track, music::v1alpha1::{ @@ -5,67 +6,61 @@ use music_player_server::api::{ NextRequest, PauseRequest, PlayRequest, PreviousRequest, StopRequest, }, }; -use music_player_settings::{read_settings, Settings}; -use tonic::{codegen::http::request, transport::Channel}; - +use tonic::transport::Channel; pub struct PlaybackClient { client: PlaybackServiceClient, } impl PlaybackClient { - pub async fn new(port: u16) -> Result> { - let config = read_settings().unwrap(); - let settings = config.try_deserialize::().unwrap(); - let url = format!("http://{}:{}", settings.host, port); + pub async fn new(host: String, port: u16) -> Result { + let url = format!("tcp://{}:{}", host, port); let client = PlaybackServiceClient::connect(url).await?; Ok(Self { client }) } - pub async fn play(&mut self) -> Result<(), Box> { + pub async fn play(&mut self) -> Result<(), Error> { let request = tonic::Request::new(PlayRequest {}); self.client.play(request).await?; Ok(()) } - pub async fn pause(&mut self) -> Result<(), Box> { + pub async fn pause(&mut self) -> Result<(), Error> { let request = tonic::Request::new(PauseRequest {}); self.client.pause(request).await?; Ok(()) } - pub async fn stop(&mut self) -> Result<(), Box> { + pub async fn stop(&mut self) -> Result<(), Error> { let request = tonic::Request::new(StopRequest {}); self.client.stop(request).await?; Ok(()) } - pub async fn next(&mut self) -> Result<(), Box> { + pub async fn next(&mut self) -> Result<(), Error> { let request = tonic::Request::new(NextRequest {}); self.client.next(request).await?; Ok(()) } - pub async fn prev(&mut self) -> Result<(), Box> { + pub async fn prev(&mut self) -> Result<(), Error> { let request = tonic::Request::new(PreviousRequest {}); self.client.previous(request).await?; Ok(()) } - pub async fn seek(&mut self, position: u32) -> Result<(), Box> { + pub async fn seek(&mut self, position: u32) -> Result<(), Error> { Ok(()) } - pub async fn set_volume(&mut self, volume: u32) -> Result<(), Box> { + pub async fn set_volume(&mut self, volume: u32) -> Result<(), Error> { Ok(()) } - pub async fn status(&mut self) -> Result<(), Box> { + pub async fn status(&mut self) -> Result<(), Error> { Ok(()) } - pub async fn current( - &mut self, - ) -> Result<(Option, u32, u32, bool), Box> { + pub async fn current(&mut self) -> Result<(Option, u32, u32, bool), Error> { let request = tonic::Request::new(GetCurrentlyPlayingSongRequest {}); let response = self.client.get_currently_playing_song(request).await?; let response = response.into_inner(); diff --git a/client/src/playlist.rs b/client/src/playlist.rs index 5db64135..4005cf48 100644 --- a/client/src/playlist.rs +++ b/client/src/playlist.rs @@ -1,35 +1,63 @@ -use music_player_server::api::music::v1alpha1::playlist_service_client::PlaylistServiceClient; +use anyhow::{Error, Ok}; +use music_player_server::api::music::v1alpha1::{ + playlist_service_client::PlaylistServiceClient, FindAllRequest, GetPlaylistDetailsRequest, +}; use music_player_settings::{read_settings, Settings}; +use music_player_types::types::Playlist; use tonic::transport::Channel; - pub struct PlaylistClient { client: PlaylistServiceClient, } impl PlaylistClient { - pub async fn new(port: u16) -> Result> { - let config = read_settings().unwrap(); - let settings = config.try_deserialize::().unwrap(); - let url = format!("http://{}:{}", settings.host, port); + pub async fn new(host: String, port: u16) -> Result { + let url = format!("tcp://{}:{}", host, port); let client = PlaylistServiceClient::connect(url).await?; Ok(Self { client }) } - pub async fn add(&self, id: &str) {} + pub async fn find(&mut self, id: &str) -> Result { + let request = tonic::Request::new(GetPlaylistDetailsRequest { id: id.to_string() }); + let response = self.client.get_playlist_details(request).await?; + Ok(response.into_inner().into()) + } + + pub async fn add(&mut self, id: &str) { + todo!() + } - pub async fn list_songs(&self) {} + pub async fn list_songs(&mut self) { + todo!() + } - pub async fn clear(&self, id: &str) {} + pub async fn clear(&mut self, id: &str) { + todo!() + } - pub async fn list_all(&self) {} + pub async fn list_all(&mut self) -> Result, Error> { + let request = tonic::Request::new(FindAllRequest {}); + let response = self.client.find_all(request).await?; + let playlists = response.into_inner().playlists; + Ok(playlists.into_iter().map(Into::into).collect()) + } - pub async fn play(&self, id: &str) {} + pub async fn play(&mut self, id: &str) { + todo!() + } - pub async fn remove(&self, id: &str) {} + pub async fn remove(&mut self, id: &str) { + todo!() + } - pub async fn shuffle(&self) {} + pub async fn shuffle(&mut self) { + todo!() + } - pub async fn create(&self, name: &str) {} + pub async fn create(&mut self, name: &str) { + todo!() + } - pub async fn delete_playlist(&self, id: &str) {} + pub async fn delete_playlist(&mut self, id: &str) { + todo!() + } } diff --git a/client/src/tests/library.rs b/client/src/tests/library.rs index 79719a8b..f57fa69e 100644 --- a/client/src/tests/library.rs +++ b/client/src/tests/library.rs @@ -5,7 +5,8 @@ use crate::library::LibraryClient; #[tokio::test] async fn album() -> Result<(), Box> { let port = env::var("MUSIC_PLAYER_PORT").unwrap_or_else(|_| "50051".to_string()); - let mut client = LibraryClient::new(port.parse().unwrap()).await?; + let host = env::var("MUSIC_PLAYER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); + let mut client = LibraryClient::new(host, port.parse().unwrap()).await?; let response = client.album("216ccc791352fbbffc11268b984db19a").await?; let response = response.unwrap(); assert_eq!(response.id, "216ccc791352fbbffc11268b984db19a"); @@ -18,8 +19,9 @@ async fn album() -> Result<(), Box> { #[tokio::test] async fn albums() -> Result<(), Box> { let port = env::var("MUSIC_PLAYER_PORT").unwrap_or_else(|_| "50051".to_string()); - let mut client = LibraryClient::new(port.parse().unwrap()).await?; - let response = client.albums().await?; + let host = "0.0.0.0".to_owned(); + let mut client = LibraryClient::new(host, port.parse().unwrap()).await?; + let response = client.albums(0, 100).await?; assert_eq!(response.len(), 1); assert_eq!(response[0].id, "216ccc791352fbbffc11268b984db19a"); assert_eq!(response[0].title, "2014 Forest Hills Drive"); @@ -31,7 +33,8 @@ async fn albums() -> Result<(), Box> { #[tokio::test] async fn artist() -> Result<(), Box> { let port = env::var("MUSIC_PLAYER_PORT").unwrap_or_else(|_| "50051".to_string()); - let mut client = LibraryClient::new(port.parse().unwrap()).await?; + let host = env::var("MUSIC_PLAYER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); + let mut client = LibraryClient::new(host, port.parse().unwrap()).await?; let response = client.artist("b03cc90c455d92d8e9a0ce331e6de54d").await?; let response = response.unwrap(); assert_eq!(response.id, "b03cc90c455d92d8e9a0ce331e6de54d"); @@ -42,8 +45,9 @@ async fn artist() -> Result<(), Box> { #[tokio::test] async fn artists() -> Result<(), Box> { let port = env::var("MUSIC_PLAYER_PORT").unwrap_or_else(|_| "50051".to_string()); - let mut client = LibraryClient::new(port.parse().unwrap()).await?; - let response = client.artists().await?; + let host = env::var("MUSIC_PLAYER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); + let mut client = LibraryClient::new(host, port.parse().unwrap()).await?; + let response = client.artists(0, 100).await?; assert_eq!(response.len(), 1); assert_eq!(response[0].id, "b03cc90c455d92d8e9a0ce331e6de54d"); assert_eq!(response[0].name, "J. Cole"); @@ -53,8 +57,9 @@ async fn artists() -> Result<(), Box> { #[tokio::test] async fn songs() -> Result<(), Box> { let port = env::var("MUSIC_PLAYER_PORT").unwrap_or_else(|_| "50051".to_string()); - let mut client = LibraryClient::new(port.parse().unwrap()).await?; - let response = client.songs().await?; + let host = env::var("MUSIC_PLAYER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); + let mut client = LibraryClient::new(host, port.parse().unwrap()).await?; + let response = client.songs(0, 100).await?; assert_eq!(response.len(), 2); assert_eq!(response[0].id, "dd77dd0ea2de5208e4987001a59ba8e4"); assert_eq!(response[0].title, "Fire Squad"); diff --git a/client/src/tests/playback.rs b/client/src/tests/playback.rs index 40181af0..dfc29637 100644 --- a/client/src/tests/playback.rs +++ b/client/src/tests/playback.rs @@ -16,8 +16,10 @@ use crate::{playback::PlaybackClient, tests::setup_new_params, tracklist::Trackl #[tokio::test] async fn play() -> Result<(), Box> { + let host = "0.0.0.0".to_owned(); + let port = 4081; let (backend, audio_format, cmd_tx, cmd_rx, tracklist, db, addr, _url) = - setup_new_params(4081).await; + setup_new_params(port).await; let (_, _) = Player::new( move || backend(None, audio_format), |_| {}, @@ -43,13 +45,13 @@ async fn play() -> Result<(), Box> { }); tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let mut client = TracklistClient::new(4081).await?; + let mut client = TracklistClient::new(host.clone(), port).await?; client.add("3ac1f1651b6ef6d5f3f55b711e3bfcd1").await?; tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let mut client = PlaybackClient::new(4081).await.unwrap(); + let mut client = PlaybackClient::new(host.clone(), port).await.unwrap(); client.pause().await.unwrap(); @@ -72,8 +74,10 @@ async fn play() -> Result<(), Box> { #[tokio::test] async fn pause() -> Result<(), Box> { + let host = "0.0.0.0".to_owned(); + let port = 4079; let (backend, audio_format, cmd_tx, cmd_rx, tracklist, db, addr, _url) = - setup_new_params(4079).await; + setup_new_params(port).await; let (_, _) = Player::new( move || backend(None, audio_format), |_| {}, @@ -99,13 +103,13 @@ async fn pause() -> Result<(), Box> { }); tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let mut client = TracklistClient::new(4079).await?; + let mut client = TracklistClient::new(host.clone(), port).await?; client.add("3ac1f1651b6ef6d5f3f55b711e3bfcd1").await?; tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let mut client = PlaybackClient::new(4079).await.unwrap(); + let mut client = PlaybackClient::new(host.clone(), port).await.unwrap(); let (_, _, _, is_playing) = client.current().await?; assert_eq!(is_playing, true); @@ -125,8 +129,10 @@ async fn pause() -> Result<(), Box> { #[tokio::test] async fn stop() -> Result<(), Box> { + let host = "0.0.0.0".to_owned(); + let port = 4078; let (backend, audio_format, cmd_tx, cmd_rx, tracklist, db, addr, _url) = - setup_new_params(4078).await; + setup_new_params(port).await; let (_, _) = Player::new( move || backend(None, audio_format), |_| {}, @@ -152,13 +158,13 @@ async fn stop() -> Result<(), Box> { }); tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let mut client = TracklistClient::new(4078).await?; + let mut client = TracklistClient::new(host.clone(), port).await?; client.add("3ac1f1651b6ef6d5f3f55b711e3bfcd1").await?; tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let mut client = PlaybackClient::new(4078).await.unwrap(); + let mut client = PlaybackClient::new(host.clone(), port).await.unwrap(); client.stop().await.unwrap(); @@ -176,8 +182,10 @@ async fn stop() -> Result<(), Box> { #[tokio::test] async fn next() -> Result<(), Box> { + let host = "0.0.0.0".to_owned(); + let port = 4082; let (backend, audio_format, cmd_tx, cmd_rx, tracklist, db, addr, _url) = - setup_new_params(4082).await; + setup_new_params(port).await; let (_, _) = Player::new( move || backend(None, audio_format), |_| {}, @@ -203,13 +211,13 @@ async fn next() -> Result<(), Box> { }); tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let mut client = TracklistClient::new(4082).await?; + let mut client = TracklistClient::new(host.clone(), port).await?; client.add("3ac1f1651b6ef6d5f3f55b711e3bfcd1").await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; client.add("dd77dd0ea2de5208e4987001a59ba8e4").await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - let mut client = PlaybackClient::new(4082).await?; + let mut client = PlaybackClient::new(host.clone(), port).await?; client.next().await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; @@ -227,8 +235,10 @@ async fn next() -> Result<(), Box> { #[tokio::test] async fn prev() -> Result<(), Box> { + let host = "0.0.0.0".to_owned(); + let port = 4083; let (backend, audio_format, cmd_tx, cmd_rx, tracklist, db, addr, _url) = - setup_new_params(4083).await; + setup_new_params(port).await; let (_, _) = Player::new( move || backend(None, audio_format), |_| {}, @@ -254,13 +264,13 @@ async fn prev() -> Result<(), Box> { }); tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let mut client = TracklistClient::new(4083).await?; + let mut client = TracklistClient::new(host.clone(), port).await?; client.add("3ac1f1651b6ef6d5f3f55b711e3bfcd1").await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; client.add("dd77dd0ea2de5208e4987001a59ba8e4").await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - let mut client = PlaybackClient::new(4083).await?; + let mut client = PlaybackClient::new(host.clone(), port).await?; client.next().await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; client.prev().await?; @@ -279,8 +289,10 @@ async fn prev() -> Result<(), Box> { #[tokio::test] async fn current() -> Result<(), Box> { + let host = "0.0.0.0".to_owned(); + let port = 4084; let (backend, audio_format, cmd_tx, cmd_rx, tracklist, db, addr, _url) = - setup_new_params(4084).await; + setup_new_params(port).await; let (_, _) = Player::new( move || backend(None, audio_format), |_| {}, @@ -306,10 +318,10 @@ async fn current() -> Result<(), Box> { }); tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let mut client = TracklistClient::new(4084).await?; + let mut client = TracklistClient::new(host.clone(), port).await?; client.add("3ac1f1651b6ef6d5f3f55b711e3bfcd1").await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - let mut client = PlaybackClient::new(4084).await?; + let mut client = PlaybackClient::new(host.clone(), port).await?; let (current_track, _, _, _) = client.current().await?; assert_ne!(current_track, None); diff --git a/client/src/tests/tracklist.rs b/client/src/tests/tracklist.rs index 49083831..153dd275 100644 --- a/client/src/tests/tracklist.rs +++ b/client/src/tests/tracklist.rs @@ -17,8 +17,10 @@ use crate::{tests::setup_new_params, tracklist::TracklistClient}; #[tokio::test] async fn add() -> Result<(), Box> { + let host = "0.0.0.0".to_owned(); + let port = 4086; let (backend, audio_format, cmd_tx, cmd_rx, tracklist, db, addr, _url) = - setup_new_params(4086).await; + setup_new_params(port).await; let (_, _) = Player::new( move || backend(None, audio_format), @@ -45,7 +47,7 @@ async fn add() -> Result<(), Box> { }); tokio::time::sleep(std::time::Duration::from_millis(100)).await; - let mut client = TracklistClient::new(4086).await?; + let mut client = TracklistClient::new(host, port).await?; client.add("3ac1f1651b6ef6d5f3f55b711e3bfcd1").await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; @@ -63,8 +65,10 @@ async fn add() -> Result<(), Box> { #[tokio::test] async fn list() -> Result<(), Box> { + let host = "0.0.0.0".to_owned(); + let port = 4087; let (backend, audio_format, cmd_tx, cmd_rx, tracklist, db, addr, _url) = - setup_new_params(4087).await; + setup_new_params(port).await; let (_, _) = Player::new( move || backend(None, audio_format), |_| {}, @@ -90,7 +94,7 @@ async fn list() -> Result<(), Box> { }); tokio::time::sleep(std::time::Duration::from_millis(100)).await; - let mut client = TracklistClient::new(4087).await?; + let mut client = TracklistClient::new(host, port).await?; let (previous_tracks, next_tracks) = client.list().await?; assert_eq!(previous_tracks.len(), 0); assert_eq!(next_tracks.len(), 0); diff --git a/client/src/tracklist.rs b/client/src/tracklist.rs index 7c73fe97..035b2e35 100644 --- a/client/src/tracklist.rs +++ b/client/src/tracklist.rs @@ -1,3 +1,4 @@ +use anyhow::Error; use music_player_server::api::{ metadata::v1alpha1::Track, music::v1alpha1::{ @@ -5,23 +6,19 @@ use music_player_server::api::{ GetTracklistTracksRequest, PlayTrackAtRequest, RemoveTrackRequest, }, }; -use music_player_settings::{read_settings, Settings}; use tonic::transport::Channel; - pub struct TracklistClient { client: TracklistServiceClient, } impl TracklistClient { - pub async fn new(port: u16) -> Result> { - let config = read_settings().unwrap(); - let settings = config.try_deserialize::().unwrap(); - let url = format!("http://{}:{}", settings.host, port); + pub async fn new(host: String, port: u16) -> Result { + let url = format!("tcp://{}:{}", host, port); let client = TracklistServiceClient::connect(url).await?; Ok(Self { client }) } - pub async fn add(&mut self, id: &str) -> Result<(), Box> { + pub async fn add(&mut self, id: &str) -> Result<(), Error> { let request = tonic::Request::new(AddTrackRequest { track: Some(Track { id: id.to_string(), @@ -33,7 +30,7 @@ impl TracklistClient { Ok(()) } - pub async fn add_tracks(&mut self, ids: &[&str]) -> Result<(), Box> { + pub async fn add_tracks(&mut self, ids: &[&str]) -> Result<(), Error> { let request = tonic::Request::new(AddTrackRequest { ..Default::default() }); @@ -41,7 +38,7 @@ impl TracklistClient { Ok(()) } - pub async fn clear(&mut self) -> Result<(), Box> { + pub async fn clear(&mut self) -> Result<(), Error> { let request = tonic::Request::new(ClearTracklistRequest { ..Default::default() }); @@ -49,7 +46,7 @@ impl TracklistClient { Ok(()) } - pub async fn list(&mut self) -> Result<(Vec, Vec), Box> { + pub async fn list(&mut self) -> Result<(Vec, Vec), Error> { let request = tonic::Request::new(GetTracklistTracksRequest { ..Default::default() }); @@ -58,7 +55,7 @@ impl TracklistClient { Ok((response.previous_tracks, response.next_tracks)) } - pub async fn remove(&mut self, id: &str) -> Result<(), Box> { + pub async fn remove(&mut self, id: &str) -> Result<(), Error> { let request = tonic::Request::new(RemoveTrackRequest { ..Default::default() }); @@ -66,7 +63,7 @@ impl TracklistClient { Ok(()) } - pub async fn play_track_at(&mut self, index: usize) -> Result<(), Box> { + pub async fn play_track_at(&mut self, index: usize) -> Result<(), Error> { let request = tonic::Request::new(PlayTrackAtRequest { index: index as u32, }); diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 32963028..875f420c 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -4,12 +4,12 @@ mod tests; use async_stream::stream; use futures_util::Stream; use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo}; -use owo_colors::OwoColorize; use std::thread; use music_player_settings::{read_settings, Settings}; pub const SERVICE_NAME: &'static str = "_music-player._tcp.local."; +pub const XBMC_SERVICE_NAME: &'static str = "_xbmc-jsonrpc-h._tcp.local."; pub struct MdnsResponder { responder: libmdns::Responder, @@ -26,11 +26,15 @@ impl MdnsResponder { } pub fn register_service(&mut self, name: &str, port: u16) { + let config = read_settings().unwrap(); + let settings = config.try_deserialize::().unwrap(); + let device_name = format!("device_name={}", settings.device_name); + self.svc.push(self.responder.register( "_music-player._tcp".to_owned(), name.to_owned(), port, - &["path=/"], + &["path=/", device_name.as_str()], )); } } @@ -60,12 +64,16 @@ pub fn register(name: &str, port: u16) { builder.init(); */ + let config = read_settings().unwrap(); + let settings = config.try_deserialize::().unwrap(); + let device_name = format!("device_name={}", settings.device_name); + let responder = libmdns::Responder::new().unwrap(); let _svc = responder.register( "_music-player._tcp".to_owned(), name.to_owned(), port, - &["path=/"], + &["path=/", device_name.as_str()], ); loop { @@ -81,13 +89,6 @@ pub fn discover(service_name: &str) -> impl Stream { while let Ok(event) = receiver.recv() { match event { ServiceEvent::ServiceResolved(info) => { - println!( - "{} - {} - {:?} - port: {}", - info.get_fullname().bright_green(), - info.get_hostname().to_lowercase(), - info.get_addresses(), - info.get_port() - ); yield info; } _ => {} diff --git a/entity/src/album.rs b/entity/src/album.rs index 33710e45..8acad5e8 100644 --- a/entity/src/album.rs +++ b/entity/src/album.rs @@ -1,4 +1,4 @@ -use music_player_types::types::Song; +use music_player_types::types::{Album as AlbumType, RemoteCoverUrl, Song, Track as TrackType}; use sea_orm::{entity::prelude::*, ActiveValue}; use serde::{Deserialize, Serialize}; @@ -58,3 +58,52 @@ impl From<&Song> for ActiveModel { } } } + +impl From for Model { + fn from(album: AlbumType) -> Self { + let tracks: Vec = album + .clone() + .tracks + .into_iter() + .map(|track| TrackType { + album: Some(album.clone()), + ..track + }) + .collect(); + Self { + id: album.id.clone(), + title: album.title, + cover: album.cover, + artist: album.artist, + artist_id: album.artist_id, + year: album.year, + tracks: tracks.into_iter().map(Into::into).collect(), + } + } +} + +impl Into for Model { + fn into(self) -> AlbumType { + AlbumType { + id: self.id, + title: self.title, + cover: self.cover, + artist: self.artist, + artist_id: self.artist_id, + year: self.year, + tracks: self.tracks.into_iter().map(Into::into).collect(), + } + } +} + +impl RemoteCoverUrl for Model { + fn with_remote_cover_url(&self, base_url: &str) -> Self { + Self { + cover: self + .cover + .clone() + .map(|cover| format!("{}/covers/{}", base_url, cover)), + ..self.clone() + } + } +} diff --git a/entity/src/artist.rs b/entity/src/artist.rs index eb67f90d..6c8b4c00 100644 --- a/entity/src/artist.rs +++ b/entity/src/artist.rs @@ -1,4 +1,4 @@ -use music_player_types::types::Song; +use music_player_types::types::{Artist as ArtistType, RemoteTrackUrl, Song, RemoteCoverUrl}; use sea_orm::{entity::prelude::*, ActiveValue}; use serde::{Deserialize, Serialize}; @@ -45,3 +45,49 @@ impl From<&Song> for ActiveModel { } } } + +impl From for Model { + fn from(artist: ArtistType) -> Self { + Self { + id: artist.id.clone(), + name: artist.name, + ..Default::default() + } + } +} + +impl Into for Model { + fn into(self) -> ArtistType { + ArtistType { + id: self.id, + name: self.name, + ..Default::default() + } + } +} + +impl RemoteCoverUrl for Model { + fn with_remote_cover_url(&self, base_url: &str) -> Self { + Self { + albums: self + .albums + .iter() + .map(|album| album.with_remote_cover_url(base_url)) + .collect(), + ..self.clone() + } + } +} + +impl RemoteTrackUrl for Model { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + tracks: self + .tracks + .iter() + .map(|track| track.with_remote_track_url(base_url)) + .collect(), + ..self.clone() + } + } +} diff --git a/entity/src/lib.rs b/entity/src/lib.rs index 2a7eaceb..a0869e9f 100644 --- a/entity/src/lib.rs +++ b/entity/src/lib.rs @@ -11,6 +11,7 @@ pub mod playlist_tracks; pub mod track; pub mod select_result { + use music_player_types::types::{Album, Artist, Track}; use sea_orm::FromQueryResult; #[derive(Debug, FromQueryResult, Clone)] @@ -32,4 +33,30 @@ pub mod select_result { pub track_genre: Option, pub track_uri: String, } + + impl Into for PlaylistTrack { + fn into(self) -> Track { + Track { + id: self.track_id, + title: self.track_title, + duration: Some(self.track_duration), + track_number: self.track_number, + uri: self.track_uri, + artists: vec![Artist { + id: self.artist_id, + name: self.artist_name, + ..Default::default() + }], + album: Some(Album { + id: self.album_id, + title: self.album_title, + cover: self.album_cover, + year: self.album_year, + ..Default::default() + }), + artist: self.track_artist, + ..Default::default() + } + } + } } diff --git a/entity/src/playlist.rs b/entity/src/playlist.rs index e8718d24..865c84bb 100644 --- a/entity/src/playlist.rs +++ b/entity/src/playlist.rs @@ -1,3 +1,4 @@ +use music_player_types::types::Playlist as PlaylistType; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; @@ -47,3 +48,14 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} + +impl Into for Model { + fn into(self) -> PlaylistType { + PlaylistType { + id: self.id, + name: self.name, + description: self.description, + tracks: self.tracks.into_iter().map(Into::into).collect(), + } + } +} diff --git a/entity/src/track.rs b/entity/src/track.rs index c9ef8901..51d159cb 100644 --- a/entity/src/track.rs +++ b/entity/src/track.rs @@ -1,4 +1,4 @@ -use music_player_types::types::Song; +use music_player_types::types::{RemoteTrackUrl, Song, Track as TrackType}; use sea_orm::{entity::prelude::*, ActiveValue}; use serde::{Deserialize, Serialize}; @@ -170,3 +170,55 @@ impl From for Model { } } } + +impl From for Model { + fn from(track: TrackType) -> Self { + let track_album = track.album.unwrap(); + Self { + id: track.id, + title: track.title, + artist: track.artist, + uri: track.uri, + album_id: Some(track_album.id.clone()), + artist_id: if track.artists.is_empty() { + None + } else { + Some(track.artists[0].id.clone()) + }, + duration: track.duration, + album: album::Model { + id: track_album.id, + title: track_album.title, + cover: track_album.cover, + year: track_album.year, + ..Default::default() + }, + artists: track.artists.into_iter().map(Into::into).collect(), + ..Default::default() + } + } +} + +impl Into for Model { + fn into(self) -> TrackType { + TrackType { + id: self.id, + title: self.title, + artist: self.artist, + uri: self.uri, + duration: self.duration, + album: Some(self.album.into()), + artists: self.artists.into_iter().map(Into::into).collect(), + ..Default::default() + } + } +} + +impl RemoteTrackUrl for Model { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + uri: format!("{}/tracks/{}", base_url, self.id), + ..self.clone() + } + } +} diff --git a/graphql/BUILD b/graphql/BUILD index 734eb0ac..fb5955e9 100644 --- a/graphql/BUILD +++ b/graphql/BUILD @@ -9,6 +9,7 @@ rust_library( "src/schema/objects/album.rs", "src/schema/objects/artist.rs", "src/schema/objects/current_track.rs", + "src/schema/objects/device.rs", "src/schema/objects/folder.rs", "src/schema/objects/lyrics.rs", "src/schema/objects/mod.rs", @@ -19,6 +20,7 @@ rust_library( "src/schema/objects/tracklist.rs", "src/schema/addons.rs", "src/schema/core.rs", + "src/schema/devices.rs", "src/schema/history.rs", "src/schema/library.rs", "src/schema/mixer.rs", @@ -37,6 +39,8 @@ rust_library( "//entity:music_player_entity", "//tracklist:music_player_tracklist", "//types:music_player_types", + "//addons:music_player_addons", + "//discovery:music_player_discovery", "@crate_index//:async-graphql", "@crate_index//:rand", "@crate_index//:chrono", @@ -45,5 +49,6 @@ rust_library( "@crate_index//:slab", "@crate_index//:futures-util", "@crate_index//:once_cell", + "@crate_index//:url", ] + all_crate_deps(), ) diff --git a/graphql/Cargo.toml b/graphql/Cargo.toml index 4ca471fe..c6958401 100644 --- a/graphql/Cargo.toml +++ b/graphql/Cargo.toml @@ -39,6 +39,14 @@ version = "0.1.5" path = "../types" version = "0.1.1" +[dependencies.music-player-discovery] +path = "../discovery" +version = "0.1.1" + +[dependencies.music-player-addons] +path = "../addons" +version = "0.1.0" + [dev-dependencies.music-player-migration] path = "../migration" version = "0.1.4" @@ -61,3 +69,5 @@ futures-channel = "0.3.25" once_cell = "1.16.0" slab = "0.4.7" serde = { version = "1.0.148", features = ["serde_derive"] } +mdns-sd = "0.5.9" +anyhow = "1.0.67" diff --git a/graphql/src/lib.rs b/graphql/src/lib.rs index fd308598..75d5064e 100644 --- a/graphql/src/lib.rs +++ b/graphql/src/lib.rs @@ -1,10 +1,85 @@ #[cfg(test)] mod tests; - +use crate::simple_broker::SimpleBroker; use async_graphql::Schema; +use futures_util::StreamExt; +use music_player_discovery::{discover, SERVICE_NAME, XBMC_SERVICE_NAME}; +use music_player_entity::track as track_entity; +use music_player_playback::player::PlayerCommand; +use music_player_types::types::Device; +use rand::seq::SliceRandom; use schema::{Mutation, Query, Subscription}; +use std::{ + sync::{Arc, Mutex}, + thread, +}; +use tokio::sync::mpsc::UnboundedSender; pub mod schema; pub mod simple_broker; pub type MusicPlayerSchema = Schema; + +pub async fn scan_devices() -> Result>>, Box> +{ + let devices: Arc>> = Arc::new(std::sync::Mutex::new(Vec::new())); + let mp_devices = Arc::clone(&devices); + let xbmc_devices = Arc::clone(&devices); + thread::spawn(move || { + tokio::runtime::Runtime::new().unwrap().block_on(async { + let services = discover(SERVICE_NAME); + tokio::pin!(services); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + while let Some(info) = services.next().await { + let device = Device::from(info.clone()); + let mut mp_devices = mp_devices.lock().unwrap(); + if mp_devices + .iter() + .find(|d| d.id == device.id && d.service == device.service) + .is_none() + { + mp_devices.push(device.clone()); + SimpleBroker::::publish(device.clone()); + } + } + }); + }); + + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + thread::spawn(move || { + tokio::runtime::Runtime::new().unwrap().block_on(async { + let services = discover(XBMC_SERVICE_NAME); + tokio::pin!(services); + while let Some(info) = services.next().await { + xbmc_devices + .lock() + .unwrap() + .push(Device::from(info.clone())); + SimpleBroker::::publish(Device::from(info.clone())); + } + }); + }); + + Ok(devices) +} + +pub fn load_tracks( + player_cmd: &Arc>>, + mut tracks: Vec, + position: Option, + shuffle: bool, +) { + if shuffle { + tracks.shuffle(&mut rand::thread_rng()); + } + let player_cmd_tx = player_cmd.lock().unwrap(); + player_cmd_tx.send(PlayerCommand::Stop).unwrap(); + player_cmd_tx.send(PlayerCommand::Clear).unwrap(); + player_cmd_tx + .send(PlayerCommand::LoadTracklist { tracks }) + .unwrap(); + player_cmd_tx + .send(PlayerCommand::PlayTrackAt(position.unwrap_or(0) as usize)) + .unwrap(); +} diff --git a/graphql/src/schema/devices.rs b/graphql/src/schema/devices.rs new file mode 100644 index 00000000..2ea09677 --- /dev/null +++ b/graphql/src/schema/devices.rs @@ -0,0 +1,176 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + thread, +}; + +use async_graphql::*; + +use futures_util::Stream; +use music_player_addons::CurrentDevice; +use tokio::sync::Mutex as TokioMutex; + +use crate::simple_broker::SimpleBroker; + +use music_player_types::types::{self, Connected}; + +use super::{ + connect_to, connect_to_current_device, + objects::device::{App, ConnectedDevice, Device, DisconnectedDevice}, +}; + +#[derive(Default)] +pub struct DevicesQuery; + +#[Object] +impl DevicesQuery { + async fn connected_device(&self, ctx: &Context<'_>) -> Result { + let connected_device = ctx + .data::>>>() + .unwrap(); + let connected_device = connected_device.lock().unwrap().clone(); + match connected_device.get("current_device") { + Some(device) => Ok(Device { + is_connected: true, + ..device.clone().into() + }), + None => Err(Error::new("No device connected")), + } + } + async fn list_devices( + &self, + ctx: &Context<'_>, + filter: Option, + ) -> Result, Error> { + let connected_device = ctx + .data::>>>() + .unwrap(); + let devices = ctx.data::>>>().unwrap(); + let devices = devices.lock().unwrap().clone(); + let connected_device = connected_device.lock().unwrap().clone(); + let current_device = connected_device.get("current_device"); + + let devices = match filter { + Some(App::MusicPlayer) => devices + .into_iter() + .filter(|device| device.app == "music-player") + .collect(), + Some(App::XBMC) => devices + .into_iter() + .filter(|device| device.app == "xbmc") + .collect(), + None => devices, + }; + + let devices = devices + .iter() + .map(|srv| types::Device::from(srv.clone()).is_connected(current_device)) + .map(Into::into) + .collect(); + Ok(devices) + } +} + +#[derive(Default)] +pub struct DevicesMutation; + +#[Object] +impl DevicesMutation { + async fn connect_to_device(&self, ctx: &Context<'_>, id: ID) -> Result { + let devices = ctx.data::>>>().unwrap(); + let devices = devices.lock().unwrap().clone(); + let connected_device = ctx + .data::>>>() + .unwrap(); + let io_device = ctx.data::>>().unwrap(); + let mut io_device = io_device.lock().await; + + let base_url = match devices.clone().into_iter().find(|device| { + device.id == id.to_string() && (device.service == "http" || device.app == "xbmc") + }) { + Some(device) => Some(format!("http://{}:{}", device.host, device.port)), + None => None, + }; + + match devices.into_iter().find(|device| { + device.id == id.to_string() && (device.service == "grpc" || device.app == "xbmc") + }) { + Some(device) => { + let current_device = types::Device::from(device.clone()) + .is_connected(Some(&device.clone())) + .with_base_url(base_url); + connected_device + .lock() + .unwrap() + .insert("current_device".to_string(), current_device.clone()); + + let source = connect_to( + types::Device::from(device.clone()).is_connected(Some(&device.clone())), + ) + .await?; + + match source { + Some(source) => io_device.set_source(source), + None => return Err(Error::new("No source found")), + } + + SimpleBroker::::publish(device.clone().into()); + + Ok(types::Device::from(device.clone()) + .is_connected(Some(&device.clone())) + .into()) + } + None => Err(Error::new("Device not found")), + } + } + + async fn disconnect_from_device(&self, ctx: &Context<'_>) -> Result, Error> { + let connected_device = ctx + .data::>>>() + .unwrap(); + let io_device = ctx.data::>>().unwrap(); + let mut io_device = io_device.lock().await; + let mut connected_device = connected_device.lock().unwrap(); + match connected_device.remove("current_device") { + Some(device) => { + io_device.clear_source(); + SimpleBroker::::publish(device.clone().into()); + Ok(Some(device.clone().into())) + } + None => Ok(None), + } + } +} + +#[derive(Default)] +pub struct DevicesSubscription; + +#[Subscription] +impl DevicesSubscription { + async fn on_new_device(&self, ctx: &Context<'_>) -> impl Stream { + let connected_device = ctx + .data::>>>() + .unwrap(); + let devices = ctx.data::>>>().unwrap(); + let devices = devices.lock().unwrap().clone(); + let connected_device = connected_device.lock().unwrap().clone(); + + thread::spawn(move || { + let current_device = connected_device.get("current_device"); + + thread::sleep(std::time::Duration::from_secs(1)); + devices.into_iter().for_each(|device| { + SimpleBroker::::publish(device.is_connected(current_device).into()); + }); + }); + SimpleBroker::::subscribe() + } + + async fn on_connected(&self, ctx: &Context<'_>) -> impl Stream { + SimpleBroker::::subscribe() + } + + async fn on_disconnected(&self, ctx: &Context<'_>) -> impl Stream { + SimpleBroker::::subscribe() + } +} diff --git a/graphql/src/schema/library.rs b/graphql/src/schema/library.rs index e0ccce0c..717ba3d0 100644 --- a/graphql/src/schema/library.rs +++ b/graphql/src/schema/library.rs @@ -1,13 +1,15 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc, sync::Mutex as StdMutex}; use async_graphql::{futures_util::FutureExt, *}; +use music_player_addons::CurrentDevice; use music_player_entity::{album as album_entity, artist as artist_entity, track as track_entity}; use music_player_scanner::scan_directory; -use music_player_storage::Database; -use sea_orm::{ - ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, - QuerySelect, +use music_player_storage::{ + repo::{album::AlbumRepository, artist::ArtistRepository, track::TrackRepository}, + Database, }; +use music_player_types::types::{self, RemoteCoverUrl, RemoteTrackUrl}; +use sea_orm::{ActiveModelTrait, ActiveValue}; use tokio::sync::Mutex; use super::objects::{album::Album, artist::Artist, search_result::SearchResult, track::Track}; @@ -17,152 +19,223 @@ pub struct LibraryQuery; #[Object] impl LibraryQuery { - async fn tracks(&self, ctx: &Context<'_>) -> Result, Error> { - let db = ctx.data::>>().unwrap(); - let results: Vec<(track_entity::Model, Vec)> = - track_entity::Entity::find() - .limit(100) - .order_by_asc(track_entity::Column::Title) - .find_with_related(artist_entity::Entity) - .all(db.lock().await.get_connection()) - .await?; + async fn tracks( + &self, + ctx: &Context<'_>, + offset: Option, + limit: Option, + ) -> Result, Error> { + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; - let albums: Vec<(track_entity::Model, Option)> = - track_entity::Entity::find() - .limit(100) - .order_by_asc(track_entity::Column::Title) - .find_also_related(album_entity::Entity) - .all(db.lock().await.get_connection()) + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let result = source + .tracks(offset.unwrap_or(0), limit.unwrap_or(100)) .await?; - let albums: Vec> = albums - .into_iter() - .map(|(_track, album)| album.clone()) - .collect(); - let mut albums = albums.into_iter(); - - Ok(results - .into_iter() - .map(|(track, artists)| { - let album = albums.next().unwrap().unwrap(); - Track::from(track_entity::Model { - artists, - album, - ..track + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); + + let tracks: Vec = result.into_iter().map(Into::into).collect(); + + return Ok(tracks + .into_iter() + .map(|track| { + track + .with_remote_track_url(base_url.as_str()) + .with_remote_cover_url(base_url.as_str()) }) - }) - .collect()) + .collect()); + } + + let db = ctx.data::>>().unwrap(); + + let results = TrackRepository::new(db.lock().await.get_connection()) + .find_all(100) + .await?; + + Ok(results.into_iter().map(Into::into).collect()) } - async fn artists(&self, ctx: &Context<'_>) -> Result, Error> { + async fn artists( + &self, + ctx: &Context<'_>, + offset: Option, + limit: Option, + ) -> Result, Error> { + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; + + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let artists = source + .artists(offset.unwrap_or(0), limit.unwrap_or(100)) + .await?; + + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); + + return Ok(artists + .into_iter() + .map(|artist| { + artist + .with_remote_cover_url(base_url.as_str()) + .with_remote_track_url(base_url.as_str()) + }) + .map(Into::into) + .collect()); + } + let db = ctx.data::>>().unwrap(); - let results = artist_entity::Entity::find() - .order_by_asc(artist_entity::Column::Name) - .all(db.lock().await.get_connection()) + + let results = ArtistRepository::new(db.lock().await.get_connection()) + .find_all() .await?; Ok(results.into_iter().map(Into::into).collect()) } - async fn albums(&self, ctx: &Context<'_>) -> Result, Error> { + async fn albums( + &self, + ctx: &Context<'_>, + offset: Option, + limit: Option, + ) -> Result, Error> { + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; + + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let albums = source + .albums(offset.unwrap_or(0), limit.unwrap_or(100)) + .await?; + + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); + + let result: Vec = albums.into_iter().map(Into::into).collect(); + + return Ok(result + .into_iter() + .map(|album| { + album + .with_remote_cover_url(base_url.as_str()) + .with_remote_track_url(base_url.as_str()) + }) + .collect()); + } + let db = ctx.data::>>().unwrap(); - let results = album_entity::Entity::find() - .order_by_asc(album_entity::Column::Title) - .all(db.lock().await.get_connection()) + + let results = AlbumRepository::new(db.lock().await.get_connection()) + .find_all() .await?; + Ok(results.into_iter().map(Into::into).collect()) } async fn track(&self, ctx: &Context<'_>, id: ID) -> Result { - let db = ctx.data::>>().unwrap(); + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; let id = id.to_string(); - let results: Vec<(track_entity::Model, Vec)> = - track_entity::Entity::find() - .filter(track_entity::Column::Id.eq(id.clone())) - .find_with_related(artist_entity::Entity) - .all(db.lock().await.get_connection()) - .await?; - if results.len() == 0 { - return Err(Error::new("Track not found")); - } - let track = results[0].0.clone(); - let album = - album_entity::Entity::find_by_id(track.album_id.unwrap_or_default().to_string()) - .one(db.lock().await.get_connection()) - .await?; - Ok(track_entity::Model { - artists: results[0].1.clone(), - album: album.unwrap(), - id: track.id, - title: track.title, - duration: track.duration, - uri: track.uri, - artist: track.artist, - ..Default::default() + + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let track = source.track(&id).await?; + + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); + + return Ok(Track::from(track) + .with_remote_track_url(base_url.as_str()) + .with_remote_cover_url(base_url.as_str())); } - .into()) + + let db = ctx.data::>>().unwrap(); + + let track = TrackRepository::new(db.lock().await.get_connection()) + .find(&id) + .await?; + + Ok(track.into()) } async fn artist(&self, ctx: &Context<'_>, id: ID) -> Result { - let db = ctx.data::>>().unwrap(); + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; let id = id.to_string(); - let result = artist_entity::Entity::find_by_id(id.clone()) - .one(db.lock().await.get_connection()) - .await?; - if result.is_none() { - return Err(Error::new("Artist not found")); + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let artist = source.artist(&id).await?; + + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); + + return Ok(artist + .with_remote_track_url(base_url.as_str()) + .with_remote_cover_url(base_url.as_str()) + .into()); } - let mut artist = result.unwrap(); - let results: Vec<(track_entity::Model, Option)> = - track_entity::Entity::find() - .filter(track_entity::Column::ArtistId.eq(id.clone())) - .order_by_asc(track_entity::Column::Title) - .find_also_related(album_entity::Entity) - .all(db.lock().await.get_connection()) - .await?; + let db = ctx.data::>>().unwrap(); - artist.tracks = results - .into_iter() - .map(|(track, album)| { - let mut track = track; - track.artists = vec![artist.clone()]; - track.album = album.unwrap(); - track - }) - .collect(); - - artist.albums = album_entity::Entity::find() - .filter(album_entity::Column::ArtistId.eq(id.clone())) - .order_by_asc(album_entity::Column::Title) - .all(db.lock().await.get_connection()) + let artist = ArtistRepository::new(db.lock().await.get_connection()) + .find(&id) .await?; Ok(artist.into()) } async fn album(&self, ctx: &Context<'_>, id: ID) -> Result { - let db = ctx.data::>>().unwrap(); - let result = album_entity::Entity::find_by_id(id.to_string()) - .one(db.lock().await.get_connection()) - .await?; - if result.is_none() { - return Err(Error::new("Album not found")); + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + + let id = id.to_string(); + + let mut device = current_device.lock().await; + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let album = source.album(&id).await?; + + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); + + return Ok(Album::from(album) + .with_remote_cover_url(base_url.as_str()) + .with_remote_track_url(base_url.as_str())); } - let mut album = result.unwrap(); - let mut tracks = album - .find_related(track_entity::Entity) - .order_by_asc(track_entity::Column::Track) - .all(db.lock().await.get_connection()) + + let db = ctx.data::>>().unwrap(); + + let album = AlbumRepository::new(db.lock().await.get_connection()) + .find(&id) .await?; - for track in &mut tracks { - track.artists = track - .find_related(artist_entity::Entity) - .all(db.lock().await.get_connection()) - .await?; - } - album.tracks = tracks; + Ok(album.into()) } diff --git a/graphql/src/schema/mod.rs b/graphql/src/schema/mod.rs index 3ba26af8..37bfcd65 100644 --- a/graphql/src/schema/mod.rs +++ b/graphql/src/schema/mod.rs @@ -1,6 +1,12 @@ +use std::collections::HashMap; + +use anyhow::Error; use async_graphql::{Enum, MergedObject, MergedSubscription}; +use music_player_addons::{local::Local, Browseable}; +use music_player_types::types::Device; use self::{ + devices::{DevicesMutation, DevicesQuery, DevicesSubscription}, library::{LibraryMutation, LibraryQuery}, mixer::{MixerMutation, MixerQuery}, playback::{PlaybackMutation, PlaybackQuery, PlaybackSubscription}, @@ -10,6 +16,7 @@ use self::{ pub mod addons; pub mod core; +pub mod devices; pub mod history; pub mod library; pub mod mixer; @@ -20,6 +27,7 @@ pub mod tracklist; #[derive(MergedObject, Default)] pub struct Query( + DevicesQuery, LibraryQuery, MixerQuery, PlaybackQuery, @@ -29,6 +37,7 @@ pub struct Query( #[derive(MergedObject, Default)] pub struct Mutation( + DevicesMutation, LibraryMutation, MixerMutation, PlaybackMutation, @@ -41,6 +50,7 @@ pub struct Subscription( PlaybackSubscription, PlaylistSubscription, TracklistSubscription, + DevicesSubscription, ); #[derive(Enum, Eq, PartialEq, Copy, Clone)] @@ -52,3 +62,22 @@ pub enum MutationType { Moved, Updated, } + +pub async fn connect_to_current_device( + devices: HashMap, +) -> Result>, Error> { + match devices.get("current_device") { + Some(current_device) => { + let mut local: Local = current_device.clone().into(); + local.connect().await?; + Ok(Some(Box::new(local))) + } + None => Ok(None), + } +} + +pub async fn connect_to(device: Device) -> Result>, Error> { + let mut local: Local = device.clone().into(); + local.connect().await?; + Ok(Some(Box::new(local))) +} diff --git a/graphql/src/schema/objects/album.rs b/graphql/src/schema/objects/album.rs index d3df6970..b2f453e9 100644 --- a/graphql/src/schema/objects/album.rs +++ b/graphql/src/schema/objects/album.rs @@ -1,6 +1,6 @@ use async_graphql::*; use music_player_entity::album::Model; -use music_player_types::types::Album as AlbumType; +use music_player_types::types::{Album as AlbumType, RemoteCoverUrl, RemoteTrackUrl}; use serde::Serialize; use super::track::Track; @@ -52,6 +52,31 @@ impl Album { } } +impl RemoteCoverUrl for Album { + fn with_remote_cover_url(&self, base_url: &str) -> Self { + Self { + cover: self + .cover + .clone() + .map(|cover| format!("{}/covers/{}", base_url, cover)), + ..self.clone() + } + } +} + +impl RemoteTrackUrl for Album { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + tracks: self + .tracks + .iter() + .map(|track| track.with_remote_track_url(base_url)) + .collect(), + ..self.clone() + } + } +} + impl From for Album { fn from(model: Model) -> Self { Self { @@ -74,6 +99,7 @@ impl From for Album { cover: album.cover, artist: album.artist, year: album.year, + tracks: album.tracks.into_iter().map(Into::into).collect(), ..Default::default() } } diff --git a/graphql/src/schema/objects/artist.rs b/graphql/src/schema/objects/artist.rs index 3fd9d354..5c5451e3 100644 --- a/graphql/src/schema/objects/artist.rs +++ b/graphql/src/schema/objects/artist.rs @@ -1,7 +1,7 @@ use super::{album::Album, track::Track}; use async_graphql::*; use music_player_entity::artist::Model; -use music_player_types::types::Artist as ArtistType; +use music_player_types::types::{Artist as ArtistType, RemoteTrackUrl}; use serde::Serialize; #[derive(Default, Clone, Serialize)] @@ -67,12 +67,29 @@ impl From for Artist { } } } + impl From for Artist { fn from(artist: ArtistType) -> Self { Self { id: ID(artist.id), name: artist.name, + picture: artist.picture.unwrap_or_default(), + albums: artist.albums.into_iter().map(Into::into).collect(), + songs: artist.songs.into_iter().map(Into::into).collect(), ..Default::default() } } } + +impl RemoteTrackUrl for Artist { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + songs: self + .songs + .iter() + .map(|track| track.with_remote_track_url(base_url)) + .collect(), + ..self.clone() + } + } +} diff --git a/graphql/src/schema/objects/device.rs b/graphql/src/schema/objects/device.rs new file mode 100644 index 00000000..c630fd6a --- /dev/null +++ b/graphql/src/schema/objects/device.rs @@ -0,0 +1,178 @@ +use async_graphql::*; +use music_player_discovery::{SERVICE_NAME, XBMC_SERVICE_NAME}; +use music_player_types::types; +use serde::Serialize; + +#[derive(Enum, Copy, Clone, Eq, PartialEq)] +pub enum App { + MusicPlayer, + XBMC, +} + +#[derive(Default, Clone, Serialize)] +pub struct Device { + pub id: ID, + pub name: String, + pub host: String, + pub port: u16, + pub service: String, + pub app: String, + pub is_connected: bool, +} + +#[Object] +impl Device { + async fn id(&self) -> &str { + &self.id + } + + async fn name(&self) -> &str { + &self.name + } + + async fn host(&self) -> &str { + &self.host + } + + async fn port(&self) -> u16 { + self.port + } + + async fn service(&self) -> &str { + &self.service + } + + async fn app(&self) -> &str { + &self.app + } + + async fn is_connected(&self) -> bool { + self.is_connected + } +} + +impl From for Device { + fn from(device: types::Device) -> Self { + Self { + id: ID::from(device.id), + name: device.name, + host: device.host, + port: device.port, + service: device.service, + app: device.app, + is_connected: false, + } + } +} + +#[derive(Default, Clone, Serialize)] +pub struct ConnectedDevice { + pub id: ID, + pub name: String, + pub host: String, + pub port: u16, + pub service: String, + pub app: String, + pub is_connected: bool, +} + +#[Object] +impl ConnectedDevice { + async fn id(&self) -> &str { + &self.id + } + + async fn name(&self) -> &str { + &self.name + } + + async fn host(&self) -> &str { + &self.host + } + + async fn port(&self) -> u16 { + self.port + } + + async fn service(&self) -> &str { + &self.service + } + + async fn app(&self) -> &str { + &self.app + } + + async fn is_connected(&self) -> bool { + self.is_connected + } +} + +impl From for ConnectedDevice { + fn from(device: types::Device) -> Self { + Self { + id: ID::from(device.id), + name: device.name, + host: device.host, + port: device.port, + service: device.service, + app: device.app, + is_connected: true, + } + } +} + +#[derive(Default, Clone, Serialize)] +pub struct DisconnectedDevice { + pub id: ID, + pub name: String, + pub host: String, + pub port: u16, + pub service: String, + pub app: String, + pub is_connected: bool, +} + +#[Object] +impl DisconnectedDevice { + async fn id(&self) -> &str { + &self.id + } + + async fn name(&self) -> &str { + &self.name + } + + async fn host(&self) -> &str { + &self.host + } + + async fn port(&self) -> u16 { + self.port + } + + async fn service(&self) -> &str { + &self.service + } + + async fn app(&self) -> &str { + &self.app + } + + async fn is_connected(&self) -> bool { + self.is_connected + } +} + +impl From for DisconnectedDevice { + fn from(device: types::Device) -> Self { + Self { + id: ID::from(device.id), + name: device.name, + host: device.host, + port: device.port, + service: device.service, + app: device.app, + is_connected: false, + } + } +} diff --git a/graphql/src/schema/objects/mod.rs b/graphql/src/schema/objects/mod.rs index 5d345a8b..fbdcc3cb 100644 --- a/graphql/src/schema/objects/mod.rs +++ b/graphql/src/schema/objects/mod.rs @@ -8,3 +8,4 @@ pub mod playlist; pub mod search_result; pub mod track; pub mod tracklist; +pub mod device; \ No newline at end of file diff --git a/graphql/src/schema/objects/playlist.rs b/graphql/src/schema/objects/playlist.rs index ed1a7919..240f4f45 100644 --- a/graphql/src/schema/objects/playlist.rs +++ b/graphql/src/schema/objects/playlist.rs @@ -1,5 +1,6 @@ use async_graphql::*; use music_player_entity::{playlist::Model, select_result}; +use music_player_types::types::{Playlist as PlaylistType, RemoteTrackUrl}; use super::track::Track; @@ -54,3 +55,28 @@ impl From> for Playlist { } } } + +impl From for Playlist { + fn from(playlist: PlaylistType) -> Self { + Self { + id: ID(playlist.id), + name: playlist.name, + description: playlist.description, + tracks: playlist.tracks.into_iter().map(Into::into).collect(), + } + } +} + +impl RemoteTrackUrl for Playlist { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + tracks: self + .tracks + .clone() + .into_iter() + .map(|track| track.with_remote_track_url(base_url)) + .collect(), + ..self.clone() + } + } +} diff --git a/graphql/src/schema/objects/track.rs b/graphql/src/schema/objects/track.rs index c827d47d..db7a4916 100644 --- a/graphql/src/schema/objects/track.rs +++ b/graphql/src/schema/objects/track.rs @@ -1,6 +1,7 @@ use async_graphql::*; use music_player_entity::{select_result, track::Model}; -use music_player_types::types::SimplifiedSong as TrackType; +use music_player_types::types::{self, RemoteTrackUrl}; +use music_player_types::types::{RemoteCoverUrl, SimplifiedSong as TrackType}; use serde::Serialize; use super::{album::Album, artist::Artist}; @@ -87,6 +88,30 @@ impl Track { } } +impl RemoteTrackUrl for Track { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + uri: format!("{}/tracks/{}", base_url, self.id.to_string()), + ..self.clone() + } + } +} + +impl RemoteCoverUrl for Track { + fn with_remote_cover_url(&self, base_url: &str) -> Self { + Self { + album: Album { + cover: match self.album.cover { + Some(ref cover) => Some(format!("{}/covers/{}", base_url, cover)), + None => None, + }, + ..self.album.clone() + }, + ..self.clone() + } + } +} + impl From for Track { fn from(model: Model) -> Self { Self { @@ -132,6 +157,46 @@ impl From for Track { } } +impl From for Track { + fn from(track: types::Track) -> Self { + Self { + id: ID(track.id), + title: track.title, + uri: track.uri, + duration: track.duration, + track_number: track.track_number, + artist: track.artist, + album: match track.album.clone() { + Some(album) => album.into(), + None => Default::default(), + }, + artists: track + .artists + .clone() + .into_iter() + .map(|artist| artist.into()) + .collect(), + album_title: match track.album.clone() { + Some(album) => album.title, + None => String::new(), + }, + album_id: match track.album.clone() { + Some(album) => album.id, + None => String::new(), + }, + artist_id: match track.artists.clone().first() { + Some(artist) => artist.id.clone(), + None => String::new(), + }, + cover: match track.album { + Some(album) => album.cover, + None => None, + }, + ..Default::default() + } + } +} + impl From for Track { fn from(result: select_result::PlaylistTrack) -> Self { Self { diff --git a/graphql/src/schema/playback.rs b/graphql/src/schema/playback.rs index cf765f84..341a8a0d 100644 --- a/graphql/src/schema/playback.rs +++ b/graphql/src/schema/playback.rs @@ -35,16 +35,6 @@ impl PlaybackQuery { return Ok(response); } - if track.is_none() { - let response = CurrentlyPlayingSong { - track: None, - index: 0, - position_ms: 0, - is_playing: false, - }; - return Ok(response); - } - let track = track.unwrap(); Ok(CurrentlyPlayingSong { diff --git a/graphql/src/schema/playlist.rs b/graphql/src/schema/playlist.rs index a7ee8410..71e4bbe0 100644 --- a/graphql/src/schema/playlist.rs +++ b/graphql/src/schema/playlist.rs @@ -3,12 +3,13 @@ use std::sync::Arc; use async_graphql::*; use cuid::cuid; use futures_util::Stream; +use music_player_addons::CurrentDevice; use music_player_entity::{ album as album_entity, artist as artist_entity, folder as folder_entity, playlist as playlist_entity, playlist_tracks as playlist_tracks_entity, select_result, track as track_entity, }; -use music_player_storage::Database; +use music_player_storage::{repo::playlist::PlaylistRepository, Database}; use sea_orm::{ ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, JoinType, ModelTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait, @@ -31,70 +32,37 @@ impl PlaylistQuery { let db = ctx.data::>>().unwrap(); let db = db.lock().await; - match playlist_tracks_entity::Entity::find() - .filter(playlist_tracks_entity::Column::PlaylistId.eq(id.to_string())) - .one(db.get_connection()) - .await? - { - Some(_) => { - let results = playlist_entity::Entity::find_by_id(id.to_string()) - .select_only() - .column(playlist_entity::Column::Id) - .column(playlist_entity::Column::Name) - .column(playlist_entity::Column::Description) - .column_as(artist_entity::Column::Id, "artist_id") - .column_as(artist_entity::Column::Name, "artist_name") - .column_as(album_entity::Column::Id, "album_id") - .column_as(album_entity::Column::Title, "album_title") - .column_as(album_entity::Column::Cover, "album_cover") - .column_as(album_entity::Column::Year, "album_year") - .column_as(track_entity::Column::Id, "track_id") - .column_as(track_entity::Column::Title, "track_title") - .column_as(track_entity::Column::Duration, "track_duration") - .column_as(track_entity::Column::Track, "track_number") - .column_as(track_entity::Column::Artist, "track_artist") - .column_as(track_entity::Column::Uri, "track_uri") - .column_as(track_entity::Column::Genre, "track_genre") - .join_rev( - JoinType::LeftJoin, - playlist_tracks_entity::Entity::belongs_to(playlist_entity::Entity) - .from(playlist_tracks_entity::Column::PlaylistId) - .to(playlist_entity::Column::Id) - .into(), - ) - .join( - JoinType::LeftJoin, - playlist_tracks_entity::Relation::Track.def(), - ) - .join(JoinType::LeftJoin, track_entity::Relation::Album.def()) - .join(JoinType::LeftJoin, track_entity::Relation::Artist.def()) - .into_model::() - .all(db.get_connection()) - .await?; + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; - if results.len() == 0 { - return Err(Error::new("Playlist not found")); - } - Ok(results.into()) - } - None => { - let result = playlist_entity::Entity::find_by_id(id.to_string()) - .one(db.get_connection()) - .await?; - if result.is_none() { - return Err(Error::new("Playlist not found")); - } - Ok(result.unwrap().into()) - } + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let result = source.playlist(&id).await?; + return Ok(result.into()); } + + let result = PlaylistRepository::new(db.get_connection()) + .find(id.as_str()) + .await?; + + Ok(result.into()) } async fn playlists(&self, ctx: &Context<'_>) -> Result, Error> { let db = ctx.data::>>().unwrap(); let db = db.lock().await; - playlist_entity::Entity::find() - .order_by_asc(playlist_entity::Column::Name) - .all(db.get_connection()) + + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; + + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let result = source.playlists(0, 10).await?; + return Ok(result.into_iter().map(Into::into).collect()); + } + + PlaylistRepository::new(db.get_connection()) + .find_all() .await .map(|playlists| playlists.into_iter().map(Into::into).collect()) .map_err(|e| Error::new(e.to_string())) @@ -103,10 +71,8 @@ impl PlaylistQuery { async fn main_playlists(&self, ctx: &Context<'_>) -> Result, Error> { let db = ctx.data::>>().unwrap(); let db = db.lock().await; - playlist_entity::Entity::find() - .order_by_asc(playlist_entity::Column::Name) - .filter(playlist_entity::Column::FolderId.is_null()) - .all(db.get_connection()) + PlaylistRepository::new(db.get_connection()) + .main_playlists() .await .map(|playlists| playlists.into_iter().map(Into::into).collect()) .map_err(|e| Error::new(e.to_string())) @@ -115,10 +81,8 @@ impl PlaylistQuery { async fn recent_playlists(&self, ctx: &Context<'_>) -> Result, Error> { let db = ctx.data::>>().unwrap(); let db = db.lock().await; - playlist_entity::Entity::find() - .order_by_desc(playlist_entity::Column::CreatedAt) - .limit(10) - .all(db.get_connection()) + PlaylistRepository::new(db.get_connection()) + .recent_playlists() .await .map(|playlists| playlists.into_iter().map(Into::into).collect()) .map_err(|e| Error::new(e.to_string())) diff --git a/graphql/src/schema/tracklist.rs b/graphql/src/schema/tracklist.rs index 1c810774..7dfadbb6 100644 --- a/graphql/src/schema/tracklist.rs +++ b/graphql/src/schema/tracklist.rs @@ -1,27 +1,38 @@ -use std::sync::Arc; - use async_graphql::*; use futures_util::Stream; +use music_player_addons::CurrentDevice; use music_player_entity::{ album as album_entity, artist as artist_entity, playlist as playlist_entity, playlist_tracks as playlist_tracks_entity, select_result, track as track_entity, }; use music_player_playback::player::PlayerCommand; +use music_player_storage::repo::album::AlbumRepository; +use music_player_storage::repo::artist::ArtistRepository; +use music_player_storage::repo::playlist::PlaylistRepository; +use music_player_storage::repo::track::TrackRepository; use music_player_storage::Database; use music_player_tracklist::Tracklist as TracklistState; +use music_player_types::types::{self, RemoteCoverUrl, RemoteTrackUrl}; use rand::seq::SliceRandom; use sea_orm::{ ColumnTrait, EntityTrait, JoinType, ModelTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait, }; +use std::sync::Mutex as StdMutex; +use std::{collections::HashMap, sync::Arc}; use tokio::sync::{mpsc::UnboundedSender, Mutex}; +use crate::load_tracks; use crate::simple_broker::SimpleBroker; -use super::{objects::{ - track::{Track, TrackInput}, - tracklist::Tracklist, -}, MutationType}; +use super::objects::album::Album; +use super::{ + objects::{ + track::{Track, TrackInput}, + tracklist::Tracklist, + }, + MutationType, +}; #[derive(Default)] pub struct TracklistQuery; @@ -29,7 +40,7 @@ pub struct TracklistQuery; #[Object] impl TracklistQuery { async fn tracklist_tracks(&self, ctx: &Context<'_>) -> Result { - let state = ctx.data::>>().unwrap(); + let state = ctx.data::>>().unwrap(); let (previous_tracks, next_tracks) = state.lock().unwrap().tracks(); let response = Tracklist { @@ -60,14 +71,24 @@ pub struct TracklistMutation; #[Object] impl TracklistMutation { async fn add_track(&self, ctx: &Context<'_>, track: TrackInput) -> Result, Error> { - let state = ctx.data::>>().unwrap(); + let state = ctx.data::>>().unwrap(); let player_cmd = ctx .data::>>>() .unwrap(); let db = ctx.data::>>().unwrap(); + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; let id = track.id.to_string(); + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + // TODO: call grpc to add track to tracklist + } + let result: Vec<(track_entity::Model, Vec)> = track_entity::Entity::find_by_id(id.clone()) .find_with_related(artist_entity::Entity) @@ -119,7 +140,7 @@ impl TracklistMutation { async fn clear_tracklist(&self, ctx: &Context<'_>) -> Result { let player_cmd = ctx - .data::>>>() + .data::>>>() .unwrap(); player_cmd .lock() @@ -138,7 +159,7 @@ impl TracklistMutation { } async fn remove_track(&self, ctx: &Context<'_>, position: u32) -> Result { - let state = ctx.data::>>().unwrap(); + let state = ctx.data::>>().unwrap(); let player_cmd = ctx .data::>>>() .unwrap(); @@ -162,7 +183,7 @@ impl TracklistMutation { async fn play_track_at(&self, ctx: &Context<'_>, position: u32) -> Result { let player_cmd = ctx - .data::>>>() + .data::>>>() .unwrap(); player_cmd .lock() @@ -174,40 +195,43 @@ impl TracklistMutation { async fn shuffle(&self, ctx: &Context<'_>) -> Result { let _player_cmd = ctx - .data::>>>() + .data::>>>() .unwrap(); todo!() } async fn play_next(&self, ctx: &Context<'_>, id: ID) -> Result { let db = ctx.data::>>().unwrap(); + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; + let id = id.to_string(); - let results: Vec<(track_entity::Model, Vec)> = - track_entity::Entity::find() - .filter(track_entity::Column::Id.eq(id.clone())) - .find_with_related(artist_entity::Entity) - .all(db.lock().await.get_connection()) + + let track: track_entity::Model; + + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let result = source.track(&id).await?; + + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); + + track = result + .with_remote_track_url(base_url.as_str()) + .with_remote_cover_url(base_url.as_str()) + .into(); + } else { + track = TrackRepository::new(db.lock().await.get_connection()) + .find(&id) .await?; - if results.len() == 0 { - return Err(Error::new("Track not found")); } - let track = results[0].0.clone(); - let album = - album_entity::Entity::find_by_id(track.album_id.unwrap_or_default().to_string()) - .one(db.lock().await.get_connection()) - .await?; - let track = track_entity::Model { - artists: results[0].1.clone(), - album: album.unwrap(), - id: track.id, - title: track.title, - duration: track.duration, - uri: track.uri, - artist: track.artist, - ..Default::default() - }; + let player_cmd = ctx - .data::>>>() + .data::>>>() .unwrap(); player_cmd .lock() @@ -224,43 +248,39 @@ impl TracklistMutation { position: Option, shuffle: bool, ) -> Result { - let db = ctx.data::>>().unwrap(); - let result = album_entity::Entity::find_by_id(id.to_string()) - .one(db.lock().await.get_connection()) - .await?; - if result.is_none() { - return Err(Error::new("Album not found")); - } - let album = result.unwrap(); - let mut tracks = album - .find_related(track_entity::Entity) - .order_by_asc(track_entity::Column::Track) - .all(db.lock().await.get_connection()) - .await?; - for track in &mut tracks { - track.artists = track - .find_related(artist_entity::Entity) - .all(db.lock().await.get_connection()) - .await?; - track.album = album.clone(); - } let player_cmd = ctx - .data::>>>() + .data::>>>() + .unwrap(); + let db = ctx.data::>>().unwrap(); + let connected_device = ctx + .data::>>>() .unwrap(); - let player_cmd_tx = player_cmd.lock().unwrap(); - player_cmd_tx.send(PlayerCommand::Stop).unwrap(); - player_cmd_tx.send(PlayerCommand::Clear).unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; - if shuffle { - tracks.shuffle(&mut rand::thread_rng()); + let id = id.to_string(); + + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let album = source.album(&id).await?; + + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); + + let album: album_entity::Model = album + .with_remote_cover_url(base_url.as_str()) + .with_remote_track_url(base_url.as_str()) + .into(); + let tracks = album.tracks; + load_tracks(player_cmd, tracks, position, shuffle); + return Ok(true); } - player_cmd_tx - .send(PlayerCommand::LoadTracklist { tracks }) - .unwrap(); - player_cmd_tx - .send(PlayerCommand::PlayTrackAt(position.unwrap_or(0) as usize)) - .unwrap(); + let result = AlbumRepository::new(db.lock().await.get_connection()) + .find(&id) + .await?; + load_tracks(player_cmd, result.tracks, position, shuffle); Ok(true) } @@ -271,55 +291,36 @@ impl TracklistMutation { position: Option, shuffle: bool, ) -> Result { + let player_cmd = ctx + .data::>>>() + .unwrap(); let db = ctx.data::>>().unwrap(); + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; let id = id.to_string(); - let result = artist_entity::Entity::find_by_id(id.clone()) - .one(db.lock().await.get_connection()) - .await?; - - if result.is_none() { - return Err(Error::new("Artist not found")); - } - - let mut artist = result.unwrap(); - let results: Vec<(track_entity::Model, Option)> = - track_entity::Entity::find() - .filter(track_entity::Column::ArtistId.eq(id.clone())) - .order_by_asc(track_entity::Column::Title) - .find_also_related(album_entity::Entity) - .all(db.lock().await.get_connection()) - .await?; - artist.tracks = results - .into_iter() - .map(|(track, album)| { - let mut track = track; - track.artists = vec![artist.clone()]; - track.album = album.unwrap(); - track - }) - .collect(); + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let artist = source.artist(&id).await?; - let player_cmd = ctx - .data::>>>() - .unwrap(); - let player_cmd_tx = player_cmd.lock().unwrap(); - player_cmd_tx.send(PlayerCommand::Stop).unwrap(); - player_cmd_tx.send(PlayerCommand::Clear).unwrap(); + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); - if shuffle { - artist.tracks.shuffle(&mut rand::thread_rng()); + let artist: artist_entity::Model = + artist.with_remote_track_url(base_url.as_str()).into(); + load_tracks(player_cmd, artist.tracks, position, shuffle); + return Ok(true); } - player_cmd_tx - .send(PlayerCommand::LoadTracklist { - tracks: artist.tracks, - }) - .unwrap(); - player_cmd_tx - .send(PlayerCommand::PlayTrackAt(position.unwrap_or(0) as usize)) - .unwrap(); + let artist = ArtistRepository::new(db.lock().await.get_connection()) + .find(&id) + .await?; + load_tracks(player_cmd, artist.tracks, position, shuffle); Ok(true) } @@ -330,85 +331,42 @@ impl TracklistMutation { position: Option, shuffle: bool, ) -> Result { + let player_cmd = ctx + .data::>>>() + .unwrap(); let db = ctx.data::>>().unwrap(); let db = db.lock().await; + let connected_device = ctx + .data::>>>() + .unwrap(); + let current_device = ctx.data::>>().unwrap(); + let mut device = current_device.lock().await; + let id = id.to_string(); - let result = match playlist_tracks_entity::Entity::find() - .filter(playlist_tracks_entity::Column::PlaylistId.eq(id.to_string())) - .one(db.get_connection()) - .await? - { - Some(_) => { - let results = playlist_entity::Entity::find_by_id(id.to_string()) - .select_only() - .column(playlist_entity::Column::Id) - .column(playlist_entity::Column::Name) - .column(playlist_entity::Column::Description) - .column_as(artist_entity::Column::Id, "artist_id") - .column_as(artist_entity::Column::Name, "artist_name") - .column_as(album_entity::Column::Id, "album_id") - .column_as(album_entity::Column::Title, "album_title") - .column_as(album_entity::Column::Cover, "album_cover") - .column_as(album_entity::Column::Year, "album_year") - .column_as(track_entity::Column::Id, "track_id") - .column_as(track_entity::Column::Title, "track_title") - .column_as(track_entity::Column::Duration, "track_duration") - .column_as(track_entity::Column::Track, "track_number") - .column_as(track_entity::Column::Artist, "track_artist") - .column_as(track_entity::Column::Uri, "track_uri") - .column_as(track_entity::Column::Genre, "track_genre") - .join_rev( - JoinType::LeftJoin, - playlist_tracks_entity::Entity::belongs_to(playlist_entity::Entity) - .from(playlist_tracks_entity::Column::PlaylistId) - .to(playlist_entity::Column::Id) - .into(), - ) - .join( - JoinType::LeftJoin, - playlist_tracks_entity::Relation::Track.def(), - ) - .join(JoinType::LeftJoin, track_entity::Relation::Album.def()) - .join(JoinType::LeftJoin, track_entity::Relation::Artist.def()) - .into_model::() - .all(db.get_connection()) - .await?; - - if results.len() == 0 { - return Err(Error::new("Playlist not found")); - } - results - } - None => { - let result = playlist_entity::Entity::find_by_id(id.to_string()) - .one(db.get_connection()) - .await?; - if result.is_none() { - return Err(Error::new("Playlist not found")); - } - vec![] - } - }; + if device.source.is_some() { + let source = device.source.as_mut().unwrap(); + let result = source.playlist(&id).await?; + + let device = connected_device.lock().unwrap(); + let device = device.get("current_device").unwrap(); + let base_url = device.base_url.as_ref().unwrap(); - let mut tracks: Vec = result.into_iter().map(Into::into).collect(); + let tracks = result.with_remote_track_url(base_url.as_str()).tracks; + let tracks: Vec = tracks.into_iter().map(Into::into).collect(); - if shuffle { - tracks.shuffle(&mut rand::thread_rng()); + load_tracks(player_cmd, tracks, position, shuffle); + return Ok(true); } - let player_cmd = ctx - .data::>>>() - .unwrap(); - let player_cmd_tx = player_cmd.lock().unwrap(); - player_cmd_tx.send(PlayerCommand::Stop).unwrap(); - player_cmd_tx.send(PlayerCommand::Clear).unwrap(); - player_cmd_tx - .send(PlayerCommand::LoadTracklist { tracks }) - .unwrap(); - player_cmd_tx - .send(PlayerCommand::PlayTrackAt(position.unwrap_or(0) as usize)) - .unwrap(); + let playlist = PlaylistRepository::new(db.get_connection()) + .find(id.as_str()) + .await?; + + let tracks: Vec = + playlist.tracks.into_iter().map(Into::into).collect(); + + load_tracks(player_cmd, tracks, position, shuffle); Ok(true) } diff --git a/graphql/src/tests/mod.rs b/graphql/src/tests/mod.rs index f80da745..2b892ab5 100644 --- a/graphql/src/tests/mod.rs +++ b/graphql/src/tests/mod.rs @@ -1,6 +1,7 @@ -use std::{env, sync::Arc}; +use std::{collections::HashMap, env, sync::Arc}; use async_graphql::Schema; +use music_player_addons::CurrentDevice; use music_player_playback::{ audio_backend::{self, rodio::RodioSink, Sink}, config::AudioFormat, @@ -8,12 +9,14 @@ use music_player_playback::{ }; use music_player_storage::Database; use music_player_tracklist::Tracklist; +use music_player_types::types::Device; use tokio::sync::{ mpsc::{UnboundedReceiver, UnboundedSender}, Mutex, }; use crate::{ + scan_devices, schema::{Mutation, Query, Subscription}, MusicPlayerSchema, }; @@ -38,6 +41,10 @@ pub async fn setup_schema() -> ( let cmd_tx = Arc::new(std::sync::Mutex::new(cmd_tx)); let cmd_rx = Arc::new(std::sync::Mutex::new(cmd_rx)); let tracklist = Arc::new(std::sync::Mutex::new(Tracklist::new_empty())); + let devices = scan_devices().await.unwrap(); + let connected_device: HashMap = HashMap::new(); + let connected_device = Arc::new(std::sync::Mutex::new(connected_device)); + let current_device = Arc::new(Mutex::new(CurrentDevice::new())); env::set_var("MUSIC_PLAYER_APPLICATION_DIRECTORY", "/tmp"); env::set_var("MUSIC_PLAYER_MUSIC_DIRECTORY", "/tmp/audio"); @@ -56,6 +63,9 @@ pub async fn setup_schema() -> ( .data(db) .data(Arc::clone(&cmd_tx)) .data(Arc::clone(&tracklist)) + .data(Arc::clone(&devices)) + .data(Arc::clone(&connected_device)) + .data(Arc::clone(¤t_device)) .finish(), Arc::clone(&cmd_tx), Arc::clone(&cmd_rx), diff --git a/graphql/src/tests/playback.rs b/graphql/src/tests/playback.rs index 7122d0e4..3639f9fa 100644 --- a/graphql/src/tests/playback.rs +++ b/graphql/src/tests/playback.rs @@ -64,7 +64,7 @@ async fn currently_playing_song() { ) .await; - thread::sleep(Duration::from_secs(1)); + thread::sleep(Duration::from_secs(2)); let resp = schema .execute( diff --git a/playback/BUILD b/playback/BUILD index cf039b14..51e9e02d 100644 --- a/playback/BUILD +++ b/playback/BUILD @@ -23,6 +23,7 @@ rust_library( "src/player.rs", ], deps = [ + "//audio:music_player_audio", "//tracklist:music_player_tracklist", "//entity:music_player_entity", "@crate_index//:rand", @@ -36,6 +37,7 @@ rust_library( "@crate_index//:rand_distr", "@crate_index//:zerocopy", "@crate_index//:librespot-protocol", + "@crate_index//:url", ] + all_crate_deps(), proc_macro_deps = [ "@crate_index//:async-trait", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 30d6d094..a3bf5c16 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -17,6 +17,10 @@ version = "0.1.4" path = "../entity" version = "0.1.5" +[dependencies.music-player-audio] +path = "../audio" +version = "0.1.0" + [dependencies] cpal = "0.14.0" futures-util = "0.3.24" @@ -34,3 +38,4 @@ tokio = { version = "1.21.0", features = ["full"] } zerocopy = "0.6.1" owo-colors = "3.5.0" async-trait = "0.1.57" +url = "2.3.1" diff --git a/playback/src/formatter.rs b/playback/src/formatter.rs index b6235d94..a8798e9d 100644 --- a/playback/src/formatter.rs +++ b/playback/src/formatter.rs @@ -7,10 +7,15 @@ use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::{ColorMode, MetadataOptions, MetadataRevision, Tag, Value, Visual}; use symphonia::core::probe::Hint; use symphonia::core::units::TimeBase; +use url::Url; use log::info; pub fn print_format(path: &str) { + if Url::parse(path).is_ok() { + println!("+ {}", path.magenta()); + return; + } let mut hint = Hint::new(); let source = Box::new(File::open(Path::new(path)).unwrap()); diff --git a/playback/src/player.rs b/playback/src/player.rs index a1ba2042..df621bda 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use futures_util::{future::FusedFuture, Future}; use log::{error, trace}; +use music_player_audio::fetch::{AudioFile, Subfile}; use music_player_entity::track::Model as Track; use music_player_tracklist::{PlaybackState, Tracklist}; use parking_lot::Mutex; @@ -18,7 +19,7 @@ use std::{ }; use symphonia::core::{errors::Error, io::MediaSourceStream, probe::Hint}; use tokio::{ - runtime::Runtime, + runtime::{Handle, Runtime}, sync::mpsc::{self, UnboundedReceiver}, }; @@ -28,7 +29,6 @@ use crate::{ decoder::{symphonia_decoder::SymphoniaDecoder, AudioDecoder}, dither::{mk_ditherer, TriangularDitherer}, formatter, - metadata::audio::AudioFileFormat, }; const PRELOAD_NEXT_TRACK_BEFORE_END: u64 = 30000; @@ -414,49 +414,14 @@ impl PlayerInternal { } fn load_track(&self, song: &str) -> Option { - // Create a hint to help the format registry guess what format reader is appropriate. - let mut hint = Hint::new(); - - let path = Path::new(song); - - // Provide the file extension as a hint. - if let Some(extension) = path.extension() { - if let Some(extension_str) = extension.to_str() { - hint.with_extension(extension_str); - } - } - - let source = Box::new(File::open(path).unwrap()); - - // Create the media source stream using the boxed media source from above. - let mss = MediaSourceStream::new(source, Default::default()); - - let symphonia_decoder = |mss: MediaSourceStream, hint| { - SymphoniaDecoder::new(mss, hint).map(|mut decoder| { - // For formats other that Vorbis, we'll try getting normalisation data from - // ReplayGain metadata fields, if present. - Box::new(decoder) as Decoder - }) - }; - - let decoder_type = symphonia_decoder(mss, hint); - - let decoder = match decoder_type { - Ok(decoder) => decoder, - Err(e) => { - panic!("Failed to create decoder: {}", e); - } - }; - return Some(PlayerLoadedTrackData { - decoder, - bytes_per_second: 0, - duration_ms: 0, - stream_position_ms: 0, - is_explicit: false, - }); + let handle = Handle::current(); + let song = song.to_string(); + thread::spawn(move || handle.block_on(PlayerTrackLoader::load(&song))) + .join() + .unwrap() } - fn start_playback(&mut self, track_id: &str, loaded_track: PlayerLoadedTrackData) { + fn start_playback(&mut self, _track_id: &str, loaded_track: PlayerLoadedTrackData) { self.ensure_sink_running(); self.send_event(PlayerEvent::Playing {}); @@ -624,10 +589,6 @@ impl PlayerInternal { struct PlayerLoadedTrackData { decoder: Decoder, - bytes_per_second: usize, - duration_ms: u32, - stream_position_ms: u32, - is_explicit: bool, } type Decoder = Box; @@ -720,27 +681,67 @@ impl PlayerState { } } -pub struct PlayerTrackLoader {} +pub struct PlayerTrackLoader; impl PlayerTrackLoader { - fn stream_data_rate(&self, format: AudioFileFormat) -> usize { - let kbps = match format { - AudioFileFormat::OGG_VORBIS_96 => 12, - AudioFileFormat::OGG_VORBIS_160 => 20, - AudioFileFormat::OGG_VORBIS_320 => 40, - AudioFileFormat::MP3_256 => 32, - AudioFileFormat::MP3_320 => 40, - AudioFileFormat::MP3_160 => 20, - AudioFileFormat::MP3_96 => 12, - AudioFileFormat::MP3_160_ENC => 20, - AudioFileFormat::MP4_128_DUAL => todo!(), - AudioFileFormat::OTHER3 => todo!(), - AudioFileFormat::AAC_160 => todo!(), - AudioFileFormat::AAC_320 => todo!(), - AudioFileFormat::MP4_128 => todo!(), - AudioFileFormat::OTHER5 => todo!(), + async fn load(song: &str) -> Option { + let bytes_per_second = 40 * 1024; // 320kbps + let audio_file = match AudioFile::open(&song, bytes_per_second).await { + Ok(audio_file) => audio_file, + Err(e) => { + println!("Error: {}", e); + return None; + } }; - kbps * 1024 + + match audio_file.get_stream_loader_controller() { + Ok(stream_loader_controller) => { + stream_loader_controller.set_stream_mode(); + let audio_file = + match Subfile::new(audio_file, 0, stream_loader_controller.len() as u64) { + Ok(audio_file) => audio_file, + Err(e) => { + println!("Error: {}", e); + return None; + } + }; + + let symphonia_decoder = |audio_file, format| { + SymphoniaDecoder::new(audio_file, format) + .map(|decoder| Box::new(decoder) as Decoder) + }; + + println!(">> loading ..."); + + let mut format = Hint::new(); + + match stream_loader_controller.mime_type() { + Some(mime_type) => { + format.mime_type(&mime_type); + } + None => { + println!("No mime type"); + } + } + + let decoder_type = symphonia_decoder(audio_file, format); + + let decoder = match decoder_type { + Ok(decoder) => decoder, + Err(e) => { + panic!("Failed to create decoder: {}", e); + } + }; + + println!(">> loaded ..."); + + return Some(PlayerLoadedTrackData { decoder }); + } + Err(e) => { + println!("Error: {}", e); + return None; + } + } } } diff --git a/server/BUILD b/server/BUILD index d363a253..4f91ab9d 100644 --- a/server/BUILD +++ b/server/BUILD @@ -27,6 +27,7 @@ rust_library( "//settings:music_player_settings", "//storage:music_player_storage", "//entity:music_player_entity", + "//types:music_player_types", "//tracklist:music_player_tracklist", "@crate_index//:tonic", "@crate_index//:tonic-web", diff --git a/server/Cargo.toml b/server/Cargo.toml index 56c4e977..3728397b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -35,6 +35,10 @@ version = "0.1.5" path = "../tracklist" version = "0.1.5" +[dependencies.music-player-types] +path = "../types" +version = "0.1.1" + [dependencies] owo-colors = "3.5.0" prost = "0.11.0" diff --git a/server/proto/metadata/v1alpha1/artist.proto b/server/proto/metadata/v1alpha1/artist.proto index e7916a07..d040f34a 100644 --- a/server/proto/metadata/v1alpha1/artist.proto +++ b/server/proto/metadata/v1alpha1/artist.proto @@ -7,6 +7,7 @@ import "metadata/v1alpha1/album.proto"; message ArtistSong { string id = 1; string title = 2; + string artist = 3; repeated Artist artists = 4; float duration = 5; int32 disc_number = 6; diff --git a/server/proto/music/v1alpha1/library.proto b/server/proto/music/v1alpha1/library.proto index ca9f395b..ad9d0fd2 100644 --- a/server/proto/music/v1alpha1/library.proto +++ b/server/proto/music/v1alpha1/library.proto @@ -14,15 +14,24 @@ message SearchRequest { string query = 1; } message SearchResponse {} -message GetAlbumsRequest {} +message GetAlbumsRequest { + int32 limit = 1; + int32 offset = 2; +} message GetAlbumsResponse { repeated metadata.v1alpha1.Album albums = 1; } -message GetArtistsRequest {} +message GetArtistsRequest { + int32 limit = 1; + int32 offset = 2; +} message GetArtistsResponse { repeated metadata.v1alpha1.Artist artists = 1; } -message GetTracksRequest {} +message GetTracksRequest { + int32 limit = 1; + int32 offset = 2; +} message GetTracksResponse { repeated metadata.v1alpha1.Track tracks = 1; } diff --git a/server/proto/music/v1alpha1/playlist.proto b/server/proto/music/v1alpha1/playlist.proto index b7eb679c..c8151952 100644 --- a/server/proto/music/v1alpha1/playlist.proto +++ b/server/proto/music/v1alpha1/playlist.proto @@ -72,7 +72,8 @@ message GetPlaylistDetailsRequest { string id = 1; } message GetPlaylistDetailsResponse { string id = 1; string name = 2; - repeated metadata.v1alpha1.Track tracks = 3; + string description = 3; + repeated metadata.v1alpha1.Track tracks = 4; } message CreateFolderRequest { diff --git a/server/src/api/metadata.v1alpha1.rs b/server/src/api/metadata.v1alpha1.rs index 08b7f4eb..fd5549c9 100644 --- a/server/src/api/metadata.v1alpha1.rs +++ b/server/src/api/metadata.v1alpha1.rs @@ -49,6 +49,8 @@ pub struct ArtistSong { pub id: ::prost::alloc::string::String, #[prost(string, tag = "2")] pub title: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub artist: ::prost::alloc::string::String, #[prost(message, repeated, tag = "4")] pub artists: ::prost::alloc::vec::Vec, #[prost(float, tag = "5")] diff --git a/server/src/api/music.v1alpha1.rs b/server/src/api/music.v1alpha1.rs index 7c5d243f..7f0b1c46 100644 --- a/server/src/api/music.v1alpha1.rs +++ b/server/src/api/music.v1alpha1.rs @@ -894,7 +894,12 @@ pub struct SearchRequest { pub struct SearchResponse {} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetAlbumsRequest {} +pub struct GetAlbumsRequest { + #[prost(int32, tag = "1")] + pub limit: i32, + #[prost(int32, tag = "2")] + pub offset: i32, +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAlbumsResponse { @@ -903,7 +908,12 @@ pub struct GetAlbumsResponse { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetArtistsRequest {} +pub struct GetArtistsRequest { + #[prost(int32, tag = "1")] + pub limit: i32, + #[prost(int32, tag = "2")] + pub offset: i32, +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetArtistsResponse { @@ -912,7 +922,12 @@ pub struct GetArtistsResponse { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetTracksRequest {} +pub struct GetTracksRequest { + #[prost(int32, tag = "1")] + pub limit: i32, + #[prost(int32, tag = "2")] + pub offset: i32, +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTracksResponse { @@ -2990,7 +3005,9 @@ pub struct GetPlaylistDetailsResponse { pub id: ::prost::alloc::string::String, #[prost(string, tag = "2")] pub name: ::prost::alloc::string::String, - #[prost(message, repeated, tag = "3")] + #[prost(string, tag = "3")] + pub description: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "4")] pub tracks: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/server/src/lib.rs b/server/src/lib.rs index 6cb316c9..26dff670 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -15,8 +15,9 @@ pub mod api { #[path = ""] pub mod music { use music_player_entity::folder; + use music_player_types::types::Playlist; - use self::v1alpha1::GetFolderDetailsResponse; + use self::v1alpha1::{GetFolderDetailsResponse, GetPlaylistDetailsResponse}; #[path = "music.v1alpha1.rs"] pub mod v1alpha1; @@ -31,12 +32,35 @@ pub mod api { } } } + + impl Into for GetPlaylistDetailsResponse { + fn into(self) -> Playlist { + Playlist { + id: self.id, + name: self.name, + description: Some(self.description), + tracks: self.tracks.into_iter().map(Into::into).collect(), + } + } + } + + impl From for GetPlaylistDetailsResponse { + fn from(playlist: Playlist) -> Self { + Self { + id: playlist.id, + name: playlist.name, + description: playlist.description.unwrap_or_default(), + tracks: playlist.tracks.into_iter().map(Into::into).collect(), + } + } + } } #[path = ""] pub mod objects { use self::v1alpha1::Playlist; use music_player_entity::playlist; + use music_player_types::types; #[path = "objects.v1alpha1.rs"] pub mod v1alpha1; @@ -52,11 +76,23 @@ pub mod api { } } } + + impl Into for Playlist { + fn into(self) -> types::Playlist { + types::Playlist { + id: self.id, + name: self.name, + description: Some(self.description), + tracks: self.tracks.into_iter().map(Into::into).collect(), + } + } + } } #[path = ""] pub mod metadata { use music_player_entity::{album, artist, track}; + use music_player_types::types; use self::v1alpha1::{Album, Artist, ArtistSong, Song, SongArtist, Track}; @@ -69,6 +105,7 @@ pub mod api { id: model.id, name: model.name, songs: model.tracks.into_iter().map(Into::into).collect(), + albums: model.albums.into_iter().map(Into::into).collect(), ..Default::default() } } @@ -117,6 +154,19 @@ pub mod api { } } + impl From for Song { + fn from(track: types::Track) -> Self { + Self { + id: track.id, + title: track.title, + duration: track.duration.unwrap_or_default(), + track_number: track.track_number.unwrap_or_default() as i32, + artists: track.artists.into_iter().map(Into::into).collect(), + ..Default::default() + } + } + } + impl From for SongArtist { fn from(model: artist::Model) -> Self { Self { @@ -127,6 +177,16 @@ pub mod api { } } + impl From for SongArtist { + fn from(artist: types::Artist) -> Self { + Self { + id: artist.id, + name: artist.name, + ..Default::default() + } + } + } + impl From for ArtistSong { fn from(model: track::Model) -> Self { Self { @@ -136,6 +196,140 @@ pub mod api { track_number: i32::try_from(model.track.unwrap_or_default()).unwrap(), artists: model.artists.into_iter().map(Into::into).collect(), album: Some(model.album.into()), + artist: model.artist, + ..Default::default() + } + } + } + + impl Into for ArtistSong { + fn into(self) -> types::Track { + types::Track { + id: self.id, + title: self.title, + duration: Some(self.duration), + track_number: Some(u32::try_from(self.track_number).unwrap_or_default()), + disc_number: u32::try_from(self.disc_number).unwrap_or_default(), + artists: self.artists.into_iter().map(Into::into).collect(), + album: match self.album { + Some(album) => Some(album.into()), + None => None, + }, + artist: self.artist, + ..Default::default() + } + } + } + + impl Into for Track { + fn into(self) -> types::Track { + types::Track { + id: self.id, + title: self.title, + uri: self.uri, + duration: Some(self.duration), + track_number: Some(u32::try_from(self.track_number).unwrap_or_default()), + disc_number: u32::try_from(self.disc_number).unwrap_or_default(), + artists: self.artists.into_iter().map(Into::into).collect(), + artist: self.artist, + album: match self.album { + Some(album) => Some(album.into()), + None => None, + }, + } + } + } + + impl From for Track { + fn from(track: types::Track) -> Self { + Self { + id: track.id, + title: track.title, + uri: track.uri, + duration: track.duration.unwrap_or_default(), + track_number: i32::try_from(track.track_number.unwrap_or_default()).unwrap(), + disc_number: i32::try_from(track.disc_number).unwrap(), + artists: track.artists.into_iter().map(Into::into).collect(), + artist: track.artist, + album: match track.album { + Some(album) => Some(album.into()), + None => None, + }, + ..Default::default() + } + } + } + + impl Into for Artist { + fn into(self) -> types::Artist { + types::Artist { + id: self.id, + name: self.name, + picture: Some(self.picture), + albums: self.albums.into_iter().map(Into::into).collect(), + songs: self.songs.into_iter().map(Into::into).collect(), + } + } + } + + impl From for Artist { + fn from(artist: types::Artist) -> Self { + Self { + id: artist.id, + name: artist.name, + picture: artist.picture.unwrap_or_default(), + ..Default::default() + } + } + } + + impl Into for SongArtist { + fn into(self) -> types::Artist { + types::Artist { + id: self.id, + name: self.name, + ..Default::default() + } + } + } + + impl Into for Song { + fn into(self) -> types::Track { + types::Track { + id: self.id, + title: self.title, + duration: Some(self.duration), + track_number: Some(u32::try_from(self.track_number).unwrap_or_default()), + disc_number: u32::try_from(self.disc_number).unwrap_or_default(), + artists: self.artists.into_iter().map(Into::into).collect(), + ..Default::default() + } + } + } + + impl Into for Album { + fn into(self) -> types::Album { + types::Album { + id: self.id, + title: self.title, + cover: Some(self.cover), + artist: self.artist.clone(), + year: Some(u32::try_from(self.year).unwrap_or_default()), + artist_id: Some(format!("{:x}", md5::compute(self.artist.as_str()))), + tracks: self.tracks.into_iter().map(Into::into).collect(), + } + } + } + + impl From for Album { + fn from(album: types::Album) -> Self { + Self { + id: album.id, + title: album.title, + cover: album.cover.unwrap_or_default(), + artist: album.artist, + year: i32::try_from(album.year.unwrap_or_default()).unwrap_or_default(), + tracks: album.tracks.into_iter().map(Into::into).collect(), ..Default::default() } } diff --git a/server/src/library.rs b/server/src/library.rs index c60bc426..3e779fd3 100644 --- a/server/src/library.rs +++ b/server/src/library.rs @@ -1,12 +1,14 @@ use futures::future::FutureExt; use music_player_entity::{album, artist, artist_tracks, track}; use music_player_scanner::scan_directory; +use music_player_storage::repo::album::AlbumRepository; +use music_player_storage::repo::artist::ArtistRepository; +use music_player_storage::repo::track::TrackRepository; use music_player_storage::Database; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder}; +use sea_orm::ActiveModelTrait; use std::sync::Arc; use tokio::sync::Mutex; -use crate::api::metadata::v1alpha1::{Album, Artist, ArtistSong, Song, SongArtist, Track}; use crate::api::music::v1alpha1::{ library_service_server::LibraryService, GetAlbumDetailsRequest, GetAlbumDetailsResponse, GetAlbumsRequest, GetAlbumsResponse, GetArtistDetailsRequest, GetArtistDetailsResponse, @@ -78,11 +80,11 @@ impl LibraryService for Library { &self, _request: tonic::Request, ) -> Result, tonic::Status> { - let results = artist::Entity::find() - .order_by_asc(artist::Column::Name) - .all(self.db.lock().await.get_connection()) + let results = ArtistRepository::new(&self.db.lock().await.get_connection()) + .find_all() .await .map_err(|e| tonic::Status::internal(e.to_string()))?; + let response = GetArtistsResponse { artists: results.into_iter().map(Into::into).collect(), }; @@ -93,11 +95,11 @@ impl LibraryService for Library { &self, _request: tonic::Request, ) -> Result, tonic::Status> { - let results = album::Entity::find() - .order_by_asc(album::Column::Title) - .all(self.db.lock().await.get_connection()) + let results = AlbumRepository::new(&self.db.lock().await.get_connection()) + .find_all() .await .map_err(|e| tonic::Status::internal(e.to_string()))?; + let response = GetAlbumsResponse { albums: results.into_iter().map(Into::into).collect(), }; @@ -108,38 +110,13 @@ impl LibraryService for Library { &self, _request: tonic::Request, ) -> Result, tonic::Status> { - let results: Vec<(track::Model, Vec)> = track::Entity::find() - .order_by_asc(track::Column::Title) - .find_with_related(artist::Entity) - .all(self.db.lock().await.get_connection()) - .await - .map_err(|e| tonic::Status::internal(e.to_string()))?; - - let albums: Vec<(track::Model, Option)> = track::Entity::find() - .order_by_asc(track::Column::Title) - .find_also_related(album::Entity) - .all(self.db.lock().await.get_connection()) + let tracks = TrackRepository::new(&self.db.lock().await.get_connection()) + .find_all(100) .await .map_err(|e| tonic::Status::internal(e.to_string()))?; - let albums: Vec> = albums - .into_iter() - .map(|(_track, album)| album.clone()) - .collect(); - let mut albums = albums.into_iter(); - let response = GetTracksResponse { - tracks: results - .into_iter() - .map(|(track, artists)| { - let album = albums.next().unwrap().unwrap(); - Track { - artists: artists.into_iter().map(Into::into).collect(), - album: Some(album.into()), - ..track.into() - } - }) - .collect(), + tracks: tracks.into_iter().map(Into::into).collect(), }; Ok(tonic::Response::new(response)) } @@ -149,26 +126,11 @@ impl LibraryService for Library { request: tonic::Request, ) -> Result, tonic::Status> { let id = request.into_inner().id; - let result: Vec<(track::Model, Vec)> = track::Entity::find_by_id(id.clone()) - .find_with_related(artist::Entity) - .all(self.db.lock().await.get_connection()) + + let track = TrackRepository::new(&self.db.lock().await.get_connection()) + .find(&id) .await .map_err(|e| tonic::Status::internal(e.to_string()))?; - if result.len() == 0 { - return Err(tonic::Status::not_found("Track not found")); - } - - let (mut track, artists) = result.into_iter().next().unwrap(); - track.artists = artists; - - let result: Vec<(track::Model, Option)> = - track::Entity::find_by_id(id.clone()) - .find_also_related(album::Entity) - .all(self.db.lock().await.get_connection()) - .await - .map_err(|e| tonic::Status::internal(e.to_string()))?; - let (_, album) = result.into_iter().next().unwrap(); - track.album = album.unwrap(); Ok(tonic::Response::new(GetTrackDetailsResponse { track: Some(track.into()), @@ -179,27 +141,11 @@ impl LibraryService for Library { &self, request: tonic::Request, ) -> Result, tonic::Status> { - let result: Vec<(album::Model, Vec)> = - album::Entity::find_by_id(request.into_inner().id) - .find_with_related(track::Entity) - .order_by_asc(track::Column::Track) - .all(self.db.lock().await.get_connection()) - .await - .map_err(|e| tonic::Status::internal(e.to_string()))?; - if result.len() == 0 { - return Err(tonic::Status::not_found("Album not found")); - } - let (mut album, mut tracks) = result.into_iter().next().unwrap(); - - for track in &mut tracks { - track.artists = track - .find_related(artist::Entity) - .all(self.db.lock().await.get_connection()) - .await - .map_err(|e| tonic::Status::internal(e.to_string()))?; - } - - album.tracks = tracks; + let id = request.into_inner().id; + let album = AlbumRepository::new(&self.db.lock().await.get_connection()) + .find(&id) + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; Ok(tonic::Response::new(GetAlbumDetailsResponse { album: Some(album.into()), @@ -211,38 +157,9 @@ impl LibraryService for Library { request: tonic::Request, ) -> Result, tonic::Status> { let id = request.into_inner().id; - let result = artist::Entity::find_by_id(id.clone()) - .one(self.db.lock().await.get_connection()) - .await - .map_err(|e| tonic::Status::internal(e.to_string()))?; - - if result.is_none() { - return Err(tonic::Status::not_found("Artist not found")); - } - - let mut artist = result.unwrap(); - let results: Vec<(track::Model, Option)> = track::Entity::find() - .filter(track::Column::ArtistId.eq(id.to_owned())) - .order_by_asc(track::Column::Title) - .find_also_related(album::Entity) - .all(self.db.lock().await.get_connection()) - .await - .map_err(|e| tonic::Status::internal(e.to_string()))?; - artist.tracks = results - .into_iter() - .map(|(track, album)| { - let mut track = track; - track.artists = vec![artist.clone()]; - track.album = album.unwrap(); - track - }) - .collect(); - - artist.albums = album::Entity::find() - .filter(album::Column::ArtistId.eq(id.clone())) - .order_by_asc(album::Column::Title) - .all(self.db.lock().await.get_connection()) + let artist = ArtistRepository::new(&self.db.lock().await.get_connection()) + .find(&id) .await .map_err(|e| tonic::Status::internal(e.to_string()))?; diff --git a/server/src/playlist.rs b/server/src/playlist.rs index b9aad019..68561fb6 100644 --- a/server/src/playlist.rs +++ b/server/src/playlist.rs @@ -1,5 +1,5 @@ use music_player_entity::{playlist, playlist_tracks, track}; -use music_player_storage::Database; +use music_player_storage::{repo::playlist::PlaylistRepository, Database}; use sea_orm::{ ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, Set, }; @@ -203,71 +203,24 @@ impl PlaylistService for Playlist { &self, _request: tonic::Request, ) -> Result, tonic::Status> { - playlist::Entity::find() - .all(self.db.lock().await.get_connection()) + let result = PlaylistRepository::new(self.db.lock().await.get_connection()) + .find_all() .await - .map(|playlists| { - tonic::Response::new(FindAllResponse { - playlists: playlists - .into_iter() - .map(|playlist| GetPlaylistDetailsResponse { - id: playlist.id, - name: playlist.name, - ..Default::default() - }) - .collect(), - ..Default::default() - }) - }) - .map_err(|e| tonic::Status::internal(e.to_string()))?; - - let response = FindAllResponse { - ..Default::default() - }; - Ok(tonic::Response::new(response)) + .map_err(|_| tonic::Status::internal("Failed to get playlist"))?; + Ok(tonic::Response::new(FindAllResponse { + playlists: result.into_iter().map(Into::into).collect(), + })) } async fn get_playlist_details( &self, request: tonic::Request, ) -> Result, tonic::Status> { - let result = playlist::Entity::find_by_id(request.get_ref().id.clone()) - .one(self.db.lock().await.get_connection()) - .await; - match result { - Ok(playlist) => { - if playlist.is_none() { - return Err(tonic::Status::not_found("Playlist not found")); - } - playlist - .clone() - .unwrap() - .find_related(track::Entity) - .all(self.db.lock().await.get_connection()) - .await - .map(|tracks| { - tonic::Response::new(GetPlaylistDetailsResponse { - id: playlist.clone().unwrap().id, - name: playlist.clone().unwrap().name, - tracks: tracks - .into_iter() - .map(|track| Track { - id: track.id, - title: track.title, - uri: track.uri, - duration: track.duration.unwrap_or_default(), - disc_number: i32::try_from(track.track.unwrap_or_default()) - .unwrap(), - ..Default::default() - }) - .collect(), - ..Default::default() - }) - }) - .map_err(|_| tonic::Status::internal("Failed to get playlist items")) - } - Err(_) => return Err(tonic::Status::internal("Failed to get playlist")), - } + let result = PlaylistRepository::new(self.db.lock().await.get_connection()) + .find(&request.get_ref().id) + .await + .map_err(|_| tonic::Status::internal("Failed to get playlist"))?; + Ok(tonic::Response::new(result.into())) } async fn create_folder( diff --git a/server/src/tests/library.rs b/server/src/tests/library.rs index 5366b8ef..131088e8 100644 --- a/server/src/tests/library.rs +++ b/server/src/tests/library.rs @@ -82,7 +82,10 @@ async fn get_artists() -> Result<(), Box> { let mut client = LibraryServiceClient::connect(url).await.unwrap(); - let request = tonic::Request::new(GetArtistsRequest {}); + let request = tonic::Request::new(GetArtistsRequest { + offset: 0, + limit: 10, + }); let response = client.get_artists(request).await?; let response = response.into_inner(); @@ -112,7 +115,10 @@ async fn get_albums() { tokio::time::sleep(std::time::Duration::from_millis(100)).await; let mut client = LibraryServiceClient::connect(url).await.unwrap(); - let request = tonic::Request::new(GetAlbumsRequest {}); + let request = tonic::Request::new(GetAlbumsRequest { + offset: 0, + limit: 10, + }); let response = client.get_albums(request).await.unwrap(); let response = response.into_inner(); assert_eq!(response.albums.len(), 1); @@ -141,7 +147,10 @@ async fn get_tracks() { tokio::time::sleep(std::time::Duration::from_millis(100)).await; let mut client = LibraryServiceClient::connect(url).await.unwrap(); - let request = tonic::Request::new(GetTracksRequest {}); + let request = tonic::Request::new(GetTracksRequest { + offset: 0, + limit: 10, + }); let response = client.get_tracks(request).await.unwrap(); let response = response.into_inner(); diff --git a/settings/src/lib.rs b/settings/src/lib.rs index 351a56df..134fc4cb 100644 --- a/settings/src/lib.rs +++ b/settings/src/lib.rs @@ -109,11 +109,13 @@ pub fn get_application_directory() -> String { let playlists = format!("{}/playlists", path); let tracks = format!("{}/tracks", path); let covers = format!("{}/covers", path); + let cache = format!("{}/cache", path); fs::create_dir_all(&albums).unwrap(); fs::create_dir_all(&artists).unwrap(); fs::create_dir_all(&playlists).unwrap(); fs::create_dir_all(&tracks).unwrap(); fs::create_dir_all(&covers).unwrap(); + fs::create_dir_all(&cache).unwrap(); path } diff --git a/src/args.rs b/src/args.rs index af22d80e..87fac4d2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,6 +1,7 @@ use std::{env, fs, sync::Mutex}; use clap::ArgMatches; +use futures::StreamExt; use music_player_client::{ library::LibraryClient, playback::PlaybackClient, playlist::PlaylistClient, tracklist::TracklistClient, @@ -52,7 +53,7 @@ pub async fn parse_args(matches: ArgMatches) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box>) { async fn connect_to_server() -> bool { let config = read_settings().unwrap(); let settings = config.try_deserialize::().unwrap(); - match LibraryClient::new(settings.port).await { + match LibraryClient::new(settings.host, settings.port).await { Ok(_) => true, Err(_) => false, } diff --git a/src/network.rs b/src/network.rs index 5e53dc31..59ffa6e1 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,3 +1,4 @@ +use anyhow::Error; use music_player_client::{ library::LibraryClient, playback::PlaybackClient, tracklist::TracklistClient, ws_client::WebsocketClient, @@ -38,14 +39,14 @@ pub struct Network<'a> { } impl<'a> Network<'a> { - pub async fn new(app: &'a Arc>) -> Result, Box> { + pub async fn new(app: &'a Arc>) -> Result, Error> { let config = read_settings().unwrap(); let settings = config.try_deserialize::().unwrap(); - let library = LibraryClient::new(settings.port).await?; - let playback = PlaybackClient::new(settings.port).await?; - let tracklist = TracklistClient::new(settings.port).await?; - let playlist = PlaybackClient::new(settings.port).await?; + let library = LibraryClient::new(settings.host.clone(), settings.port).await?; + let playback = PlaybackClient::new(settings.host.clone(), settings.port).await?; + let tracklist = TracklistClient::new(settings.host.clone(), settings.port).await?; + let playlist = PlaybackClient::new(settings.host.clone(), settings.port).await?; let _ws = WebsocketClient::new().await; Ok(Network { app, @@ -55,10 +56,7 @@ impl<'a> Network<'a> { playlist, }) } - pub async fn handle_network_event( - &mut self, - io_event: IoEvent, - ) -> Result<(), Box> { + pub async fn handle_network_event(&mut self, io_event: IoEvent) -> Result<(), Error> { match io_event { IoEvent::PlayTrack(track_id) => self.play_track(track_id).await, IoEvent::NextTrack => self.next_track().await, @@ -79,21 +77,21 @@ impl<'a> Network<'a> { } } - async fn play_track(&mut self, track_id: String) -> Result<(), Box> { + async fn play_track(&mut self, track_id: String) -> Result<(), Error> { self.tracklist.add(&track_id).await?; Ok(()) } - async fn next_track(&mut self) -> Result<(), Box> { + async fn next_track(&mut self) -> Result<(), Error> { self.playback.next().await } - async fn previous_track(&mut self) -> Result<(), Box> { + async fn previous_track(&mut self) -> Result<(), Error> { self.playback.prev().await } - async fn get_tracks(&mut self) -> Result<(), Box> { - let tracks = self.library.songs().await?; + async fn get_tracks(&mut self) -> Result<(), Error> { + let tracks = self.library.songs(0, 10000).await?; let mut app = self.app.lock().await; app.track_table = TrackTable { tracks, @@ -102,8 +100,8 @@ impl<'a> Network<'a> { Ok(()) } - async fn get_albums(&mut self) -> Result<(), Box> { - let albums = self.library.albums().await?; + async fn get_albums(&mut self) -> Result<(), Error> { + let albums = self.library.albums(0, 10000).await?; let mut app = self.app.lock().await; app.album_table = AlbumTable { albums, @@ -112,7 +110,7 @@ impl<'a> Network<'a> { Ok(()) } - async fn get_album(&mut self, id: String) -> Result<(), Box> { + async fn get_album(&mut self, id: String) -> Result<(), Error> { let album = self.library.album(&id).await?; let mut app = self.app.lock().await; let tracks = album @@ -134,8 +132,8 @@ impl<'a> Network<'a> { Ok(()) } - async fn get_artists(&mut self) -> Result<(), Box> { - let artists = self.library.artists().await?; + async fn get_artists(&mut self) -> Result<(), Error> { + let artists = self.library.artists(0, 10000).await?; let mut app = self.app.lock().await; app.artist_table = ArtistTable { artists, @@ -144,7 +142,7 @@ impl<'a> Network<'a> { Ok(()) } - async fn get_artist(&mut self, id: String) -> Result<(), Box> { + async fn get_artist(&mut self, id: String) -> Result<(), Error> { let artist = self.library.artist(&id).await?; let mut app = self.app.lock().await; let tracks = artist @@ -171,7 +169,7 @@ impl<'a> Network<'a> { Ok(()) } - async fn get_play_queue(&mut self) -> Result<(), Box> { + async fn get_play_queue(&mut self) -> Result<(), Error> { let (played_tracks, next_tracks) = self.tracklist.list().await?; let mut app = self.app.lock().await; app.track_table = TrackTable { @@ -181,23 +179,23 @@ impl<'a> Network<'a> { Ok(()) } - async fn get_album_tracks(&mut self, id: String) -> Result<(), Box> { + async fn get_album_tracks(&mut self, id: String) -> Result<(), Error> { todo!() } - async fn add_item_to_queue(&mut self, id: String) -> Result<(), Box> { + async fn add_item_to_queue(&mut self, id: String) -> Result<(), Error> { todo!() } - async fn shuffle(&mut self, enable: bool) -> Result<(), Box> { + async fn shuffle(&mut self, enable: bool) -> Result<(), Error> { todo!() } - async fn repeat(&mut self, enable: bool) -> Result<(), Box> { + async fn repeat(&mut self, enable: bool) -> Result<(), Error> { todo!() } - async fn get_current_playback(&mut self) -> Result<(), Box> { + async fn get_current_playback(&mut self) -> Result<(), Error> { let (track, index, position_ms, is_playing) = self.playback.current().await?; let mut app = self.app.lock().await; app.instant_since_last_current_playback_poll = Instant::now(); @@ -210,7 +208,7 @@ impl<'a> Network<'a> { Ok(()) } - async fn toggle_playback(&mut self) -> Result<(), Box> { + async fn toggle_playback(&mut self) -> Result<(), Error> { let (_, _, _, is_playing) = self.playback.current().await?; if is_playing { return self.playback.pause().await; @@ -218,7 +216,7 @@ impl<'a> Network<'a> { self.playback.play().await } - async fn play_track_at(&mut self, index: usize) -> Result<(), Box> { + async fn play_track_at(&mut self, index: usize) -> Result<(), Error> { self.tracklist.play_track_at(index).await } } diff --git a/storage/BUILD b/storage/BUILD index 28c82446..9b8900d6 100644 --- a/storage/BUILD +++ b/storage/BUILD @@ -7,6 +7,12 @@ rust_library( name = "music_player_storage", srcs = [ "src/lib.rs", + "src/repo/mod.rs", + "src/repo/album.rs", + "src/repo/artist.rs", + "src/repo/track.rs", + "src/repo/playlist.rs", + "src/repo/folder.rs", "src/searcher/album.rs", "src/searcher/artist.rs", "src/searcher/mod.rs", @@ -15,6 +21,7 @@ rust_library( deps = [ "//settings:music_player_settings", "//types:music_player_types", + "//entity:music_player_entity", "@crate_index//:tantivy", "@crate_index//:itertools", ] + all_crate_deps(), diff --git a/storage/Cargo.toml b/storage/Cargo.toml index 3eab8dd6..b524d93b 100644 --- a/storage/Cargo.toml +++ b/storage/Cargo.toml @@ -16,6 +16,10 @@ version = "0.1.1" path = "../types" version = "0.1.1" +[dependencies.music-player-entity] +path = "../entity" +version = "0.1.5" + [dependencies] itertools = "0.10.5" sea-orm = { version = "0.9.2", features = ["runtime-tokio-rustls", "sqlx-sqlite"] } @@ -23,3 +27,4 @@ tantivy = "0.18.0" tempfile = "3.3.0" md5 = "0.7.0" tokio = { version = "1.22.0", features = ["test-util", "macros"] } +anyhow = "1.0.68" diff --git a/storage/src/lib.rs b/storage/src/lib.rs index 92d28a6d..ade50bc2 100644 --- a/storage/src/lib.rs +++ b/storage/src/lib.rs @@ -8,6 +8,8 @@ use sea_orm::DatabaseConnection; pub mod searcher; +pub mod repo; + #[derive(Clone)] pub struct Database { pub connection: DatabaseConnection, diff --git a/storage/src/repo/album.rs b/storage/src/repo/album.rs new file mode 100644 index 00000000..ca400b2c --- /dev/null +++ b/storage/src/repo/album.rs @@ -0,0 +1,45 @@ +use anyhow::Error; +use music_player_entity::{album as album_entity, artist as artist_entity, track as track_entity}; +use sea_orm::{DatabaseConnection, EntityTrait, ModelTrait, QueryOrder}; + +pub struct AlbumRepository { + db: DatabaseConnection, +} + +impl AlbumRepository { + pub fn new(db: &DatabaseConnection) -> Self { + Self { db: db.clone() } + } + + pub async fn find(&self, id: &str) -> Result { + let result = album_entity::Entity::find_by_id(id.to_string()) + .one(&self.db) + .await?; + if result.is_none() { + return Err(Error::msg("Album not found")); + } + let mut album = result.unwrap(); + let mut tracks = album + .find_related(track_entity::Entity) + .order_by_asc(track_entity::Column::Track) + .all(&self.db) + .await?; + for track in &mut tracks { + track.artists = track + .find_related(artist_entity::Entity) + .all(&self.db) + .await?; + track.album = album.clone(); + } + album.tracks = tracks; + Ok(album) + } + + pub async fn find_all(&self) -> Result, Error> { + let results = album_entity::Entity::find() + .order_by_asc(album_entity::Column::Title) + .all(&self.db) + .await?; + Ok(results) + } +} diff --git a/storage/src/repo/artist.rs b/storage/src/repo/artist.rs new file mode 100644 index 00000000..34bce34c --- /dev/null +++ b/storage/src/repo/artist.rs @@ -0,0 +1,57 @@ +use anyhow::Error; +use music_player_entity::{album as album_entity, artist as artist_entity, track as track_entity}; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder}; + +pub struct ArtistRepository { + db: DatabaseConnection, +} + +impl ArtistRepository { + pub fn new(db: &DatabaseConnection) -> Self { + Self { db: db.clone() } + } + + pub async fn find(&self, id: &str) -> Result { + let result = artist_entity::Entity::find_by_id(id.to_string()) + .one(&self.db) + .await?; + + if result.is_none() { + return Err(Error::msg("Artist not found")); + } + + let mut artist = result.unwrap(); + let results: Vec<(track_entity::Model, Option)> = + track_entity::Entity::find() + .filter(track_entity::Column::ArtistId.eq(id.clone())) + .order_by_asc(track_entity::Column::Title) + .find_also_related(album_entity::Entity) + .all(&self.db) + .await?; + + artist.tracks = results + .into_iter() + .map(|(track, album)| { + let mut track = track; + track.artists = vec![artist.clone()]; + track.album = album.unwrap(); + track + }) + .collect(); + + artist.albums = album_entity::Entity::find() + .filter(album_entity::Column::ArtistId.eq(id.clone())) + .order_by_asc(album_entity::Column::Title) + .all(&self.db) + .await?; + Ok(artist) + } + + pub async fn find_all(&self) -> Result, Error> { + let results = artist_entity::Entity::find() + .order_by_asc(artist_entity::Column::Name) + .all(&self.db) + .await?; + Ok(results) + } +} diff --git a/storage/src/repo/folder.rs b/storage/src/repo/folder.rs new file mode 100644 index 00000000..60dc3b2a --- /dev/null +++ b/storage/src/repo/folder.rs @@ -0,0 +1,15 @@ +use sea_orm::DatabaseConnection; + +pub struct FolderRepository { + db: DatabaseConnection, +} + +impl FolderRepository { + pub fn new(db: &DatabaseConnection) -> Self { + Self { db: db.clone() } + } + + pub async fn find(&self) {} + + pub async fn find_all(&self) {} +} diff --git a/storage/src/repo/mod.rs b/storage/src/repo/mod.rs new file mode 100644 index 00000000..f1637abd --- /dev/null +++ b/storage/src/repo/mod.rs @@ -0,0 +1,5 @@ +pub mod album; +pub mod artist; +pub mod folder; +pub mod playlist; +pub mod track; diff --git a/storage/src/repo/playlist.rs b/storage/src/repo/playlist.rs new file mode 100644 index 00000000..4f6a2c02 --- /dev/null +++ b/storage/src/repo/playlist.rs @@ -0,0 +1,121 @@ +use anyhow::Error; +use music_player_entity::{ + album as album_entity, artist as artist_entity, playlist as playlist_entity, + playlist_tracks as playlist_tracks_entity, select_result, track as track_entity, +}; +use music_player_types::types::Playlist; +use sea_orm::{ + ColumnTrait, DatabaseConnection, EntityTrait, JoinType, QueryFilter, QueryOrder, QuerySelect, + RelationTrait, +}; + +pub struct PlaylistRepository { + db: DatabaseConnection, +} + +impl PlaylistRepository { + pub fn new(db: &DatabaseConnection) -> Self { + Self { db: db.clone() } + } + + pub async fn find(&self, id: &str) -> Result { + let playlist = playlist_entity::Entity::find_by_id(id.to_string()) + .one(&self.db) + .await?; + if playlist.is_none() { + return Err(Error::msg("Playlist not found")); + } + let result = match playlist_tracks_entity::Entity::find() + .filter(playlist_tracks_entity::Column::PlaylistId.eq(id.to_string())) + .one(&self.db) + .await? + { + Some(_) => { + let results = playlist_entity::Entity::find_by_id(id.to_string()) + .select_only() + .column(playlist_entity::Column::Id) + .column(playlist_entity::Column::Name) + .column(playlist_entity::Column::Description) + .column_as(artist_entity::Column::Id, "artist_id") + .column_as(artist_entity::Column::Name, "artist_name") + .column_as(album_entity::Column::Id, "album_id") + .column_as(album_entity::Column::Title, "album_title") + .column_as(album_entity::Column::Cover, "album_cover") + .column_as(album_entity::Column::Year, "album_year") + .column_as(track_entity::Column::Id, "track_id") + .column_as(track_entity::Column::Title, "track_title") + .column_as(track_entity::Column::Duration, "track_duration") + .column_as(track_entity::Column::Track, "track_number") + .column_as(track_entity::Column::Artist, "track_artist") + .column_as(track_entity::Column::Uri, "track_uri") + .column_as(track_entity::Column::Genre, "track_genre") + .join_rev( + JoinType::LeftJoin, + playlist_tracks_entity::Entity::belongs_to(playlist_entity::Entity) + .from(playlist_tracks_entity::Column::PlaylistId) + .to(playlist_entity::Column::Id) + .into(), + ) + .join( + JoinType::LeftJoin, + playlist_tracks_entity::Relation::Track.def(), + ) + .join(JoinType::LeftJoin, track_entity::Relation::Album.def()) + .join(JoinType::LeftJoin, track_entity::Relation::Artist.def()) + .into_model::() + .all(&self.db) + .await?; + + if results.len() == 0 { + return Err(Error::msg("Playlist not found")); + } + results + } + None => { + let result = playlist_entity::Entity::find_by_id(id.to_string()) + .one(&self.db) + .await?; + if result.is_none() { + return Err(Error::msg("Playlist not found")); + } + vec![] + } + }; + let playlist = playlist.unwrap(); + Ok(Playlist { + id: playlist.id, + name: playlist.name, + description: playlist.description, + tracks: result.into_iter().map(Into::into).collect(), + }) + } + + pub async fn find_all(&self) -> Result, Error> { + playlist_entity::Entity::find() + .order_by_asc(playlist_entity::Column::Name) + .all(&self.db) + .await + .map(|playlists| playlists.into_iter().map(Into::into).collect()) + .map_err(|e| Error::msg(e.to_string())) + } + + pub async fn main_playlists(&self) -> Result, Error> { + playlist_entity::Entity::find() + .order_by_asc(playlist_entity::Column::Name) + .filter(playlist_entity::Column::FolderId.is_null()) + .all(&self.db) + .await + .map(|playlists| playlists.into_iter().map(Into::into).collect()) + .map_err(|e| Error::msg(e.to_string())) + } + + pub async fn recent_playlists(&self) -> Result, Error> { + playlist_entity::Entity::find() + .order_by_desc(playlist_entity::Column::CreatedAt) + .limit(10) + .all(&self.db) + .await + .map(|playlists| playlists.into_iter().map(Into::into).collect()) + .map_err(|e| Error::msg(e.to_string())) + } +} diff --git a/storage/src/repo/track.rs b/storage/src/repo/track.rs new file mode 100644 index 00000000..401a82a8 --- /dev/null +++ b/storage/src/repo/track.rs @@ -0,0 +1,77 @@ +use anyhow::Error; +use music_player_entity::{album as album_entity, artist as artist_entity, track as track_entity}; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; + +pub struct TrackRepository { + db: DatabaseConnection, +} + +impl TrackRepository { + pub fn new(db: &DatabaseConnection) -> Self { + Self { db: db.clone() } + } + + pub async fn find(&self, id: &str) -> Result { + let results: Vec<(track_entity::Model, Vec)> = + track_entity::Entity::find() + .filter(track_entity::Column::Id.eq(id)) + .find_with_related(artist_entity::Entity) + .all(&self.db) + .await?; + if results.len() == 0 { + return Err(Error::msg("Track not found")); + } + let track = results[0].0.clone(); + let album = + album_entity::Entity::find_by_id(track.album_id.unwrap_or_default().to_string()) + .one(&self.db) + .await?; + Ok(track_entity::Model { + artists: results[0].1.clone(), + album: album.unwrap(), + id: track.id, + title: track.title, + duration: track.duration, + uri: track.uri, + artist: track.artist, + track: track.track, + ..Default::default() + }) + } + + pub async fn find_all(&self, limit: u64) -> Result, Error> { + let results: Vec<(track_entity::Model, Vec)> = + track_entity::Entity::find() + .limit(limit) + .order_by_asc(track_entity::Column::Title) + .find_with_related(artist_entity::Entity) + .all(&self.db) + .await?; + + let albums: Vec<(track_entity::Model, Option)> = + track_entity::Entity::find() + .limit(limit) + .order_by_asc(track_entity::Column::Title) + .find_also_related(album_entity::Entity) + .all(&self.db) + .await?; + + let albums: Vec> = albums + .into_iter() + .map(|(_track, album)| album.clone()) + .collect(); + let mut albums = albums.into_iter(); + + Ok(results + .into_iter() + .map(|(track, artists)| { + let album = albums.next().unwrap().unwrap(); + track_entity::Model { + artists, + album, + ..track + } + }) + .collect()) + } +} diff --git a/types/BUILD b/types/BUILD index 74904a77..5f0649ca 100644 --- a/types/BUILD +++ b/types/BUILD @@ -10,6 +10,8 @@ rust_library( "src/types.rs", ], deps = [ + "//discovery:music_player_discovery", "@crate_index//:tantivy", + "@crate_index//:mdns-sd", ] + all_crate_deps(), ) \ No newline at end of file diff --git a/types/Cargo.toml b/types/Cargo.toml index 08a3d5c6..c3bdc173 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -7,9 +7,14 @@ license = "MIT" authors = ["Tsiry Sandratraina "] description = "The types module of music player" +[dependencies.music-player-discovery] +path = "../discovery" +version = "0.1.1" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] lofty = "0.9.0" md5 = "0.7.0" tantivy = "0.18.1" +mdns-sd = "0.5.9" diff --git a/types/src/types.rs b/types/src/types.rs index 729af7ab..b7cc9866 100644 --- a/types/src/types.rs +++ b/types/src/types.rs @@ -1,6 +1,8 @@ use std::time::Duration; use lofty::{Accessor, FileProperties, ItemKey, Tag}; +use mdns_sd::ServiceInfo; +use music_player_discovery::{SERVICE_NAME, XBMC_SERVICE_NAME}; use tantivy::{ schema::{Schema, SchemaBuilder, STORED, STRING, TEXT}, Document, @@ -45,6 +47,7 @@ pub struct Album { pub artist_id: Option, pub year: Option, pub cover: Option, + pub tracks: Vec, } #[derive(Debug, Clone, Default)] @@ -52,6 +55,8 @@ pub struct Artist { pub id: String, pub name: String, pub picture: Option, + pub albums: Vec, + pub songs: Vec, } impl From for Album { @@ -286,3 +291,224 @@ impl Song { self.clone() } } + +#[derive(Default, Clone)] +pub struct Device { + pub id: String, + pub name: String, + pub host: String, + pub port: u16, + pub service: String, + pub app: String, + pub is_connected: bool, + pub base_url: Option, +} + +impl Device { + pub fn with_base_url(&mut self, base_url: Option) -> Self { + self.base_url = base_url; + self.clone() + } +} + +impl From for Device { + fn from(srv: ServiceInfo) -> Self { + if srv.get_fullname().contains("xbmc") { + return Self { + id: srv.get_fullname().to_owned(), + name: srv + .get_fullname() + .replace(XBMC_SERVICE_NAME, "") + .replace(".", "") + .to_owned(), + host: srv + .get_hostname() + .split_at(srv.get_hostname().len() - 1) + .0 + .to_owned(), + port: srv.get_port(), + service: srv.get_fullname().to_owned(), + app: "xbmc".to_owned(), + is_connected: false, + base_url: None, + }; + } + + if srv.get_fullname().contains(SERVICE_NAME) { + let device_id = srv + .get_fullname() + .replace(SERVICE_NAME, "") + .split("-") + .collect::>()[1] + .replace(".", "") + .to_owned(); + return Self { + id: device_id.clone(), + name: srv + .get_properties() + .get("device_name") + .unwrap_or(&device_id.clone()) + .to_owned(), + host: srv + .get_hostname() + .split_at(srv.get_hostname().len() - 1) + .0 + .to_owned(), + port: srv.get_port(), + service: srv.get_fullname().split("-").collect::>()[0].to_owned(), + app: "music-player".to_owned(), + is_connected: false, + base_url: None, + }; + } + + Self { + ..Default::default() + } + } +} + +pub trait Connected { + fn is_connected(&self, current: Option<&Device>) -> Self; +} + +impl Connected for Device { + fn is_connected(&self, current: Option<&Device>) -> Self { + match current { + Some(current) => Self { + is_connected: self.id == current.id, + ..self.clone() + }, + None => Self { + is_connected: false, + ..self.clone() + }, + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct Track { + pub id: String, + pub title: String, + pub duration: Option, + pub disc_number: u32, + pub track_number: Option, + pub uri: String, + pub artists: Vec, + pub album: Option, + pub artist: String, +} + +#[derive(Default, Clone)] +pub struct Playlist { + pub id: String, + pub name: String, + pub description: Option, + pub tracks: Vec, +} + +#[derive(Default, Clone)] +pub struct Folder { + pub id: String, + pub name: String, + pub playlists: Vec, +} + +pub trait RemoteTrackUrl { + fn with_remote_track_url(&self, base_url: &str) -> Self; +} + +pub trait RemoteCoverUrl { + fn with_remote_cover_url(&self, base_url: &str) -> Self; +} + +impl RemoteTrackUrl for Track { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + uri: format!("{}/tracks/{}", base_url, self.id), + ..self.clone() + } + } +} + +impl RemoteCoverUrl for Track { + fn with_remote_cover_url(&self, base_url: &str) -> Self { + Self { + album: match self.album { + Some(ref album) => Some(album.with_remote_cover_url(base_url)), + None => None, + }, + ..self.clone() + } + } +} + +impl RemoteCoverUrl for Album { + fn with_remote_cover_url(&self, base_url: &str) -> Self { + Self { + cover: match self.cover { + Some(ref cover) => Some(format!("{}/covers/{}", base_url, cover)), + None => None, + }, + ..self.clone() + } + } +} + +impl RemoteTrackUrl for Album { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + tracks: self + .tracks + .iter() + .map(|track| track.with_remote_track_url(base_url)) + .collect(), + ..self.clone() + } + } +} + +impl RemoteCoverUrl for Artist { + fn with_remote_cover_url(&self, base_url: &str) -> Self { + Self { + albums: self + .albums + .iter() + .map(|album| album.with_remote_cover_url(base_url)) + .collect(), + songs: self + .songs + .iter() + .map(|track| track.with_remote_cover_url(base_url)) + .collect(), + ..self.clone() + } + } +} + +impl RemoteTrackUrl for Artist { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + songs: self + .songs + .iter() + .map(|track| track.with_remote_track_url(base_url)) + .collect(), + ..self.clone() + } + } +} + +impl RemoteTrackUrl for Playlist { + fn with_remote_track_url(&self, base_url: &str) -> Self { + Self { + tracks: self + .tracks + .iter() + .map(|track| track.with_remote_track_url(base_url)) + .collect(), + ..self.clone() + } + } +} diff --git a/webui/BUILD b/webui/BUILD index af34437c..84a1f4d3 100644 --- a/webui/BUILD +++ b/webui/BUILD @@ -13,19 +13,21 @@ rust_library( "musicplayer/build/**", ]), deps = [ - "//graphql:music_player_graphql", - "//settings:music_player_settings", - "//storage:music_player_storage", - "//playback:music_player_playback", - "//tracklist:music_player_tracklist", - "//entity:music_player_entity", - "//scanner:music_player_scanner", - "@crate_index//:actix-web", - "@crate_index//:async-graphql", - "@crate_index//:async-graphql-actix-web", - "@crate_index//:mime_guess", - "@crate_index//:rust-embed", - "@crate_index//:actix-files", - "@crate_index//:actix-cors", + "//graphql:music_player_graphql", + "//settings:music_player_settings", + "//storage:music_player_storage", + "//playback:music_player_playback", + "//tracklist:music_player_tracklist", + "//entity:music_player_entity", + "//scanner:music_player_scanner", + "//types:music_player_types", + "//addons:music_player_addons", + "@crate_index//:actix-web", + "@crate_index//:async-graphql", + "@crate_index//:async-graphql-actix-web", + "@crate_index//:mime_guess", + "@crate_index//:rust-embed", + "@crate_index//:actix-files", + "@crate_index//:actix-cors", ] + all_crate_deps(), ) diff --git a/webui/Cargo.toml b/webui/Cargo.toml index 04767bc9..65e0340d 100644 --- a/webui/Cargo.toml +++ b/webui/Cargo.toml @@ -37,6 +37,18 @@ version = "0.1.5" path = "../scanner" version = "0.1.5" +[dependencies.music-player-discovery] +path = "../discovery" +version = "0.1.1" + +[dependencies.music-player-types] +path = "../types" +version = "0.1.1" + +[dependencies.music-player-addons] +path = "../addons" +version = "0.1.0" + [dev-dependencies] futures-util = "0.3.25" serde_json = "1.0.89" @@ -56,4 +68,5 @@ serde_derive = "1.0.145" serde = { version = "1.0.145", features = ["serde_derive"] } tokio = { version = "1.21.2", features = ["full"] } actix-cors = "0.6.3" -sea-orm = { version = "0.9.3", features = ["runtime-tokio-rustls", "sqlx-sqlite"] } \ No newline at end of file +sea-orm = { version = "0.9.3", features = ["runtime-tokio-rustls", "sqlx-sqlite"] } +futures-util = "0.3.25" diff --git a/webui/musicplayer/build/asset-manifest.json b/webui/musicplayer/build/asset-manifest.json index 88251622..13db7820 100644 --- a/webui/musicplayer/build/asset-manifest.json +++ b/webui/musicplayer/build/asset-manifest.json @@ -1,7 +1,7 @@ { "files": { - "main.css": "/static/css/main.56b69c4b.css", - "main.js": "/static/js/main.a15735af.js", + "main.css": "/static/css/main.5e35567c.css", + "main.js": "/static/js/main.de307e91.js", "static/js/787.26bf0a29.chunk.js": "/static/js/787.26bf0a29.chunk.js", "static/media/RockfordSans-ExtraBold.otf": "/static/media/RockfordSans-ExtraBold.1513e8fd97078bfb7708.otf", "static/media/RockfordSans-Bold.otf": "/static/media/RockfordSans-Bold.c9f599ae01b13e565598.otf", @@ -9,12 +9,12 @@ "static/media/RockfordSans-Regular.otf": "/static/media/RockfordSans-Regular.652654f28f1c111914b9.otf", "static/media/RockfordSans-Light.otf": "/static/media/RockfordSans-Light.b4a12e8abb38f7d105c4.otf", "index.html": "/index.html", - "main.56b69c4b.css.map": "/static/css/main.56b69c4b.css.map", - "main.a15735af.js.map": "/static/js/main.a15735af.js.map", + "main.5e35567c.css.map": "/static/css/main.5e35567c.css.map", + "main.de307e91.js.map": "/static/js/main.de307e91.js.map", "787.26bf0a29.chunk.js.map": "/static/js/787.26bf0a29.chunk.js.map" }, "entrypoints": [ - "static/css/main.56b69c4b.css", - "static/js/main.a15735af.js" + "static/css/main.5e35567c.css", + "static/js/main.de307e91.js" ] } \ No newline at end of file diff --git a/webui/musicplayer/build/index.html b/webui/musicplayer/build/index.html index ae15dc3f..7e6a2b57 100644 --- a/webui/musicplayer/build/index.html +++ b/webui/musicplayer/build/index.html @@ -1 +1 @@ -Music Player
\ No newline at end of file +Music Player
\ No newline at end of file diff --git a/webui/musicplayer/build/static/css/main.56b69c4b.css.map b/webui/musicplayer/build/static/css/main.56b69c4b.css.map deleted file mode 100644 index 35178f02..00000000 --- a/webui/musicplayer/build/static/css/main.56b69c4b.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"static/css/main.56b69c4b.css","mappings":"AACA,WACE,6BAAgC,CAChC,gHACF,CAEA,WACE,+BAAkC,CAClC,6GACF,CAEA,WACE,8BAAiC,CACjC,4GACF,CAEA,WACE,4BAA+B,CAC7B,eAAgB,CAChB,0GACJ,CAEA,WACE,iCAAoC,CAClC,eAAgB,CAChB,+GACJ,CAEA,KAKE,kCAAmC,CACnC,iCAAkC,CAJlC,uJAEY,CAHZ,QAAS,CAMT,iBACF,CAEA,KACE,uEAEF,CAEA,qBACE,SACF,CAEA,OAAS,sBAAuB,CAEhC,MACE,YACF,CAEA,eACE,YAAa,CAEb,SAAU,CADV,iBAEF,CAOA,qDACE,cAAe,CACf,aACF,CAGA,6BACC,YACD,CAEA,6BACE,UACD,CAED,uBACE,UACD,CAEA,cACC,eAAgB,CAChB,sBACD,CAEA,EACG,UAAc,CAAd,aAAc,CACd,oBAAwB,CAAxB,uBACH,CAEA,QACC,yBACD,CAEA,uBACC,iBACD,CAEA,sCACC,YAAa,CAEb,SAAU,CADV,iBAAkB,CAElB,QACD,CAEA,4CACC,aACD,CAEA,iCACC,UACD,CAEA,yBACC,wBAA0B,CAC1B,yBACD","sources":["index.css"],"sourcesContent":["\n@font-face {\n font-family: 'RockfordSansLight';\n src: local('RockfordSansLight'), url(./Assets/fonts/RockfordSans-Light.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansRegular';\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Regular.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansMedium';\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Medium.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansBold';\n font-weight: 900;\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Bold.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansExtraBold';\n font-weight: 900;\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-ExtraBold.otf) format('opentype');\n}\n\nbody {\n margin: 0;\n font-family: RockfordSansRegular, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n overflow-y: hidden;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n\n[data-baseweb=\"modal\"] {\n z-index: 1;\n}\n\n*:focus {outline:none !important}\n\n.play {\n display: none;\n}\n\n.floating-play {\n display: none;\n position: absolute;\n left: 40px;\n}\n\ntr:hover td div .play {\n cursor: pointer;\n display: block;\n}\n\ntr:hover td div .floating-play {\n cursor: pointer;\n display: block;\n}\n\n\ntr:hover td div .tracknumber {\n display: none;\n}\n\ntr:hover td div .album-cover {\n opacity: 0.4;\n }\n\ntr:hover td div button {\n color: #000;\n }\n\n tr td div div {\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n a {\n color: initial;\n text-decoration: initial;\n }\n\n a:hover {\n text-decoration: underline;\n }\n\n .album-cover-container {\n position: relative;\n }\n\n .album-cover-container .floating-play {\n display: none;\n position: absolute;\n left: 19px;\n top: 12px;\n }\n\n .album-cover-container:hover .floating-play {\n display: block;\n }\n\n .album-cover-container:hover img {\n opacity: 0.4;\n }\n\n [data-baseweb=\"tab-panel\"] {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/webui/musicplayer/build/static/css/main.56b69c4b.css b/webui/musicplayer/build/static/css/main.5e35567c.css similarity index 93% rename from webui/musicplayer/build/static/css/main.56b69c4b.css rename to webui/musicplayer/build/static/css/main.5e35567c.css index e052ae5b..362f71d6 100644 --- a/webui/musicplayer/build/static/css/main.56b69c4b.css +++ b/webui/musicplayer/build/static/css/main.5e35567c.css @@ -1,2 +1,2 @@ -@font-face{font-family:RockfordSansLight;src:local("RockfordSansLight"),url(/static/media/RockfordSans-Light.b4a12e8abb38f7d105c4.otf) format("opentype")}@font-face{font-family:RockfordSansRegular;src:local("RockfordSans"),url(/static/media/RockfordSans-Regular.652654f28f1c111914b9.otf) format("opentype")}@font-face{font-family:RockfordSansMedium;src:local("RockfordSans"),url(/static/media/RockfordSans-Medium.e10344a796535b513215.otf) format("opentype")}@font-face{font-family:RockfordSansBold;font-weight:900;src:local("RockfordSans"),url(/static/media/RockfordSans-Bold.c9f599ae01b13e565598.otf) format("opentype")}@font-face{font-family:RockfordSansExtraBold;font-weight:900;src:local("RockfordSans"),url(/static/media/RockfordSans-ExtraBold.1513e8fd97078bfb7708.otf) format("opentype")}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:RockfordSansRegular,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0;overflow-y:hidden}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}[data-baseweb=modal]{z-index:1}:focus{outline:none!important}.play{display:none}.floating-play{display:none;left:40px;position:absolute}tr:hover td div .floating-play,tr:hover td div .play{cursor:pointer;display:block}tr:hover td div .tracknumber{display:none}tr:hover td div .album-cover{opacity:.4}tr:hover td div button{color:#000}tr td div div{overflow:hidden;text-overflow:ellipsis}a{color:#000;color:initial;text-decoration:none;text-decoration:initial}a:hover{text-decoration:underline}.album-cover-container{position:relative}.album-cover-container .floating-play{display:none;left:19px;position:absolute;top:12px}.album-cover-container:hover .floating-play{display:block}.album-cover-container:hover img{opacity:.4}[data-baseweb=tab-panel]{padding-left:0!important;padding-right:0!important} -/*# sourceMappingURL=main.56b69c4b.css.map*/ \ No newline at end of file +@font-face{font-family:RockfordSansLight;src:local("RockfordSansLight"),url(/static/media/RockfordSans-Light.b4a12e8abb38f7d105c4.otf) format("opentype")}@font-face{font-family:RockfordSansRegular;src:local("RockfordSans"),url(/static/media/RockfordSans-Regular.652654f28f1c111914b9.otf) format("opentype")}@font-face{font-family:RockfordSansMedium;src:local("RockfordSans"),url(/static/media/RockfordSans-Medium.e10344a796535b513215.otf) format("opentype")}@font-face{font-family:RockfordSansBold;font-weight:900;src:local("RockfordSans"),url(/static/media/RockfordSans-Bold.c9f599ae01b13e565598.otf) format("opentype")}@font-face{font-family:RockfordSansExtraBold;font-weight:900;src:local("RockfordSans"),url(/static/media/RockfordSans-ExtraBold.1513e8fd97078bfb7708.otf) format("opentype")}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:RockfordSansRegular,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0;overflow-y:hidden}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}[data-baseweb=modal]{z-index:1}:focus{outline:none!important}.play{display:none}.floating-play{display:none;left:40px;position:absolute}tr:hover td div .floating-play,tr:hover td div .play{cursor:pointer;display:block}tr:hover td div .tracknumber{display:none}tr:hover td div .album-cover{opacity:.4}tr:hover td div button{color:#000}tr td div div{overflow:hidden;text-overflow:ellipsis}a{color:#000;color:initial;text-decoration:none;text-decoration:initial}a:hover{text-decoration:underline}.album-cover-container{position:relative}.album-cover-container .floating-play{display:none;left:19px;position:absolute;top:12px}.album-cover-container:hover .floating-play{display:block}.album-cover-container:hover img{opacity:.4}[data-baseweb=tab-panel]{padding-left:0!important;padding-right:0!important}.connect-list li:hover{background-color:#f3f3f386;border-radius:5px;cursor:pointer} +/*# sourceMappingURL=main.5e35567c.css.map*/ \ No newline at end of file diff --git a/webui/musicplayer/build/static/css/main.5e35567c.css.map b/webui/musicplayer/build/static/css/main.5e35567c.css.map new file mode 100644 index 00000000..43992e9b --- /dev/null +++ b/webui/musicplayer/build/static/css/main.5e35567c.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/main.5e35567c.css","mappings":"AACA,WACE,6BAAgC,CAChC,gHACF,CAEA,WACE,+BAAkC,CAClC,6GACF,CAEA,WACE,8BAAiC,CACjC,4GACF,CAEA,WACE,4BAA+B,CAC7B,eAAgB,CAChB,0GACJ,CAEA,WACE,iCAAoC,CAClC,eAAgB,CAChB,+GACJ,CAEA,KAKE,kCAAmC,CACnC,iCAAkC,CAJlC,uJAEY,CAHZ,QAAS,CAMT,iBACF,CAEA,KACE,uEAEF,CAEA,qBACE,SACF,CAEA,OAAS,sBAAuB,CAEhC,MACE,YACF,CAEA,eACE,YAAa,CAEb,SAAU,CADV,iBAEF,CAOA,qDACE,cAAe,CACf,aACF,CAGA,6BACC,YACD,CAEA,6BACE,UACD,CAED,uBACE,UACD,CAEA,cACC,eAAgB,CAChB,sBACD,CAEA,EACG,UAAc,CAAd,aAAc,CACd,oBAAwB,CAAxB,uBACH,CAEA,QACC,yBACD,CAEA,uBACC,iBACD,CAEA,sCACC,YAAa,CAEb,SAAU,CADV,iBAAkB,CAElB,QACD,CAEA,4CACC,aACD,CAEA,iCACC,UACD,CAEA,yBACC,wBAA0B,CAC1B,yBACD,CAEA,uBACC,0BAA2B,CAC3B,iBAAkB,CAClB,cACD","sources":["index.css"],"sourcesContent":["\n@font-face {\n font-family: 'RockfordSansLight';\n src: local('RockfordSansLight'), url(./Assets/fonts/RockfordSans-Light.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansRegular';\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Regular.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansMedium';\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Medium.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansBold';\n font-weight: 900;\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Bold.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansExtraBold';\n font-weight: 900;\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-ExtraBold.otf) format('opentype');\n}\n\nbody {\n margin: 0;\n font-family: RockfordSansRegular, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n overflow-y: hidden;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n\n[data-baseweb=\"modal\"] {\n z-index: 1;\n}\n\n*:focus {outline:none !important}\n\n.play {\n display: none;\n}\n\n.floating-play {\n display: none;\n position: absolute;\n left: 40px;\n}\n\ntr:hover td div .play {\n cursor: pointer;\n display: block;\n}\n\ntr:hover td div .floating-play {\n cursor: pointer;\n display: block;\n}\n\n\ntr:hover td div .tracknumber {\n display: none;\n}\n\ntr:hover td div .album-cover {\n opacity: 0.4;\n }\n\ntr:hover td div button {\n color: #000;\n }\n\n tr td div div {\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n a {\n color: initial;\n text-decoration: initial;\n }\n\n a:hover {\n text-decoration: underline;\n }\n\n .album-cover-container {\n position: relative;\n }\n\n .album-cover-container .floating-play {\n display: none;\n position: absolute;\n left: 19px;\n top: 12px;\n }\n\n .album-cover-container:hover .floating-play {\n display: block;\n }\n\n .album-cover-container:hover img {\n opacity: 0.4;\n }\n\n [data-baseweb=\"tab-panel\"] {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n\n .connect-list li:hover {\n background-color: #f3f3f386;\n border-radius: 5px;\n cursor: pointer;\n }"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/webui/musicplayer/build/static/js/main.a15735af.js b/webui/musicplayer/build/static/js/main.a15735af.js deleted file mode 100644 index 38552dbe..00000000 --- a/webui/musicplayer/build/static/js/main.a15735af.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! For license information please see main.a15735af.js.LICENSE.txt */ -!function(){var e={77:function(e){function t(e){e=e||{},this.ms=e.min||100,this.max=e.max||1e4,this.factor=e.factor||2,this.jitter=e.jitter>0&&e.jitter<=1?e.jitter:0,this.attempts=0}e.exports=t,t.prototype.duration=function(){var e=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var t=Math.random(),n=Math.floor(t*this.jitter*e);e=0==(1&Math.floor(10*t))?e-n:e+n}return 0|Math.min(e,this.max)},t.prototype.reset=function(){this.attempts=0},t.prototype.setMin=function(e){this.ms=e},t.prototype.setMax=function(e){this.max=e},t.prototype.setJitter=function(e){this.jitter=e}},546:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return(0,i.default)(e)};var r,o=n(630),i=(r=o)&&r.__esModule?r:{default:r};e.exports=t.default},216:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return"string"===typeof e&&n.test(e)};var n=/-webkit-|-moz-|-ms-/;e.exports=t.default},143:function(e){"use strict";var t=Object.prototype.hasOwnProperty,n="~";function r(){}function o(e,t,n){this.fn=e,this.context=t,this.once=n||!1}function i(e,t,r,i,a){if("function"!==typeof r)throw new TypeError("The listener must be a function");var l=new o(r,i||e,a),u=n?n+t:t;return e._events[u]?e._events[u].fn?e._events[u]=[e._events[u],l]:e._events[u].push(l):(e._events[u]=l,e._eventsCount++),e}function a(e,t){0===--e._eventsCount?e._events=new r:delete e._events[t]}function l(){this._events=new r,this._eventsCount=0}Object.create&&(r.prototype=Object.create(null),(new r).__proto__||(n=!1)),l.prototype.eventNames=function(){var e,r,o=[];if(0===this._eventsCount)return o;for(r in e=this._events)t.call(e,r)&&o.push(n?r.slice(1):r);return Object.getOwnPropertySymbols?o.concat(Object.getOwnPropertySymbols(e)):o},l.prototype.listeners=function(e){var t=n?n+e:e,r=this._events[t];if(!r)return[];if(r.fn)return[r.fn];for(var o=0,i=r.length,a=new Array(i);o"']/g,Y=RegExp(K.source),X=RegExp(G.source),J=/<%-([\s\S]+?)%>/g,Z=/<%([\s\S]+?)%>/g,ee=/<%=([\s\S]+?)%>/g,te=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,ne=/^\w*$/,re=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,oe=/[\\^$.*+?()[\]{}|]/g,ie=RegExp(oe.source),ae=/^\s+/,le=/\s/,ue=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,se=/\{\n\/\* \[wrapped with (.+)\] \*/,ce=/,? & /,fe=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,de=/[()=,{}\[\]\/\s]/,pe=/\\(\\)?/g,he=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,ye=/\w*$/,ve=/^[-+]0x[0-9a-f]+$/i,me=/^0b[01]+$/i,ge=/^\[object .+?Constructor\]$/,be=/^0o[0-7]+$/i,we=/^(?:0|[1-9]\d*)$/,xe=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,ke=/($^)/,Oe=/['\n\r\u2028\u2029\\]/g,Se="\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff",Ee="\\u2700-\\u27bf",_e="a-z\\xdf-\\xf6\\xf8-\\xff",Pe="A-Z\\xc0-\\xd6\\xd8-\\xde",Ce="\\ufe0e\\ufe0f",je="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",Te="['\u2019]",Re="[\\ud800-\\udfff]",Ae="["+je+"]",Fe="["+Se+"]",Ie="\\d+",De="[\\u2700-\\u27bf]",Ne="["+_e+"]",Le="[^\\ud800-\\udfff"+je+Ie+Ee+_e+Pe+"]",Me="\\ud83c[\\udffb-\\udfff]",Be="[^\\ud800-\\udfff]",ze="(?:\\ud83c[\\udde6-\\uddff]){2}",$e="[\\ud800-\\udbff][\\udc00-\\udfff]",Ve="["+Pe+"]",He="(?:"+Ne+"|"+Le+")",We="(?:"+Ve+"|"+Le+")",Ue="(?:['\u2019](?:d|ll|m|re|s|t|ve))?",qe="(?:['\u2019](?:D|LL|M|RE|S|T|VE))?",Qe="(?:"+Fe+"|"+Me+")"+"?",Ke="[\\ufe0e\\ufe0f]?",Ge=Ke+Qe+("(?:\\u200d(?:"+[Be,ze,$e].join("|")+")"+Ke+Qe+")*"),Ye="(?:"+[De,ze,$e].join("|")+")"+Ge,Xe="(?:"+[Be+Fe+"?",Fe,ze,$e,Re].join("|")+")",Je=RegExp(Te,"g"),Ze=RegExp(Fe,"g"),et=RegExp(Me+"(?="+Me+")|"+Xe+Ge,"g"),tt=RegExp([Ve+"?"+Ne+"+"+Ue+"(?="+[Ae,Ve,"$"].join("|")+")",We+"+"+qe+"(?="+[Ae,Ve+He,"$"].join("|")+")",Ve+"?"+He+"+"+Ue,Ve+"+"+qe,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Ie,Ye].join("|"),"g"),nt=RegExp("[\\u200d\\ud800-\\udfff"+Se+Ce+"]"),rt=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,ot=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],it=-1,at={};at[N]=at[L]=at[M]=at[B]=at[z]=at[$]=at[V]=at[H]=at[W]=!0,at[g]=at[b]=at[I]=at[w]=at[D]=at[x]=at[k]=at[O]=at[E]=at[_]=at[P]=at[j]=at[T]=at[R]=at[F]=!1;var lt={};lt[g]=lt[b]=lt[I]=lt[D]=lt[w]=lt[x]=lt[N]=lt[L]=lt[M]=lt[B]=lt[z]=lt[E]=lt[_]=lt[P]=lt[j]=lt[T]=lt[R]=lt[A]=lt[$]=lt[V]=lt[H]=lt[W]=!0,lt[k]=lt[O]=lt[F]=!1;var ut={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},st=parseFloat,ct=parseInt,ft="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g,dt="object"==typeof self&&self&&self.Object===Object&&self,pt=ft||dt||Function("return this")(),ht=t&&!t.nodeType&&t,yt=ht&&e&&!e.nodeType&&e,vt=yt&&yt.exports===ht,mt=vt&&ft.process,gt=function(){try{var e=yt&&yt.require&&yt.require("util").types;return e||mt&&mt.binding&&mt.binding("util")}catch(t){}}(),bt=gt&>.isArrayBuffer,wt=gt&>.isDate,xt=gt&>.isMap,kt=gt&>.isRegExp,Ot=gt&>.isSet,St=gt&>.isTypedArray;function Et(e,t,n){switch(n.length){case 0:return e.call(t);case 1:return e.call(t,n[0]);case 2:return e.call(t,n[0],n[1]);case 3:return e.call(t,n[0],n[1],n[2])}return e.apply(t,n)}function _t(e,t,n,r){for(var o=-1,i=null==e?0:e.length;++o-1}function At(e,t,n){for(var r=-1,o=null==e?0:e.length;++r-1;);return n}function tn(e,t){for(var n=e.length;n--&&$t(t,e[n],0)>-1;);return n}function nn(e,t){for(var n=e.length,r=0;n--;)e[n]===t&&++r;return r}var rn=qt({"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g","\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O","\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w","\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"}),on=qt({"&":"&","<":"<",">":">",'"':""","'":"'"});function an(e){return"\\"+ut[e]}function ln(e){return nt.test(e)}function un(e){var t=-1,n=Array(e.size);return e.forEach((function(e,r){n[++t]=[r,e]})),n}function sn(e,t){return function(n){return e(t(n))}}function cn(e,t){for(var n=-1,r=e.length,o=0,i=[];++n",""":'"',"'":"'"});var mn=function e(t){var n=(t=null==t?pt:mn.defaults(pt.Object(),t,mn.pick(pt,ot))).Array,r=t.Date,le=t.Error,Se=t.Function,Ee=t.Math,_e=t.Object,Pe=t.RegExp,Ce=t.String,je=t.TypeError,Te=n.prototype,Re=Se.prototype,Ae=_e.prototype,Fe=t["__core-js_shared__"],Ie=Re.toString,De=Ae.hasOwnProperty,Ne=0,Le=function(){var e=/[^.]+$/.exec(Fe&&Fe.keys&&Fe.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""}(),Me=Ae.toString,Be=Ie.call(_e),ze=pt._,$e=Pe("^"+Ie.call(De).replace(oe,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),Ve=vt?t.Buffer:o,He=t.Symbol,We=t.Uint8Array,Ue=Ve?Ve.allocUnsafe:o,qe=sn(_e.getPrototypeOf,_e),Qe=_e.create,Ke=Ae.propertyIsEnumerable,Ge=Te.splice,Ye=He?He.isConcatSpreadable:o,Xe=He?He.iterator:o,et=He?He.toStringTag:o,nt=function(){try{var e=pi(_e,"defineProperty");return e({},"",{}),e}catch(t){}}(),ut=t.clearTimeout!==pt.clearTimeout&&t.clearTimeout,ft=r&&r.now!==pt.Date.now&&r.now,dt=t.setTimeout!==pt.setTimeout&&t.setTimeout,ht=Ee.ceil,yt=Ee.floor,mt=_e.getOwnPropertySymbols,gt=Ve?Ve.isBuffer:o,Mt=t.isFinite,qt=Te.join,gn=sn(_e.keys,_e),bn=Ee.max,wn=Ee.min,xn=r.now,kn=t.parseInt,On=Ee.random,Sn=Te.reverse,En=pi(t,"DataView"),_n=pi(t,"Map"),Pn=pi(t,"Promise"),Cn=pi(t,"Set"),jn=pi(t,"WeakMap"),Tn=pi(_e,"create"),Rn=jn&&new jn,An={},Fn=zi(En),In=zi(_n),Dn=zi(Pn),Nn=zi(Cn),Ln=zi(jn),Mn=He?He.prototype:o,Bn=Mn?Mn.valueOf:o,zn=Mn?Mn.toString:o;function $n(e){if(rl(e)&&!qa(e)&&!(e instanceof Un)){if(e instanceof Wn)return e;if(De.call(e,"__wrapped__"))return $i(e)}return new Wn(e)}var Vn=function(){function e(){}return function(t){if(!nl(t))return{};if(Qe)return Qe(t);e.prototype=t;var n=new e;return e.prototype=o,n}}();function Hn(){}function Wn(e,t){this.__wrapped__=e,this.__actions__=[],this.__chain__=!!t,this.__index__=0,this.__values__=o}function Un(e){this.__wrapped__=e,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=v,this.__views__=[]}function qn(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t=t?e:t)),e}function sr(e,t,n,r,i,a){var l,u=1&t,s=2&t,c=4&t;if(n&&(l=i?n(e,r,i,a):n(e)),l!==o)return l;if(!nl(e))return e;var f=qa(e);if(f){if(l=function(e){var t=e.length,n=new e.constructor(t);t&&"string"==typeof e[0]&&De.call(e,"index")&&(n.index=e.index,n.input=e.input);return n}(e),!u)return Ro(e,l)}else{var d=vi(e),p=d==O||d==S;if(Ya(e))return Eo(e,u);if(d==P||d==g||p&&!i){if(l=s||p?{}:gi(e),!u)return s?function(e,t){return Ao(e,yi(e),t)}(e,function(e,t){return e&&Ao(t,Il(t),e)}(l,e)):function(e,t){return Ao(e,hi(e),t)}(e,ir(l,e))}else{if(!lt[d])return i?e:{};l=function(e,t,n){var r=e.constructor;switch(t){case I:return _o(e);case w:case x:return new r(+e);case D:return function(e,t){var n=t?_o(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.byteLength)}(e,n);case N:case L:case M:case B:case z:case $:case V:case H:case W:return Po(e,n);case E:return new r;case _:case R:return new r(e);case j:return function(e){var t=new e.constructor(e.source,ye.exec(e));return t.lastIndex=e.lastIndex,t}(e);case T:return new r;case A:return o=e,Bn?_e(Bn.call(o)):{}}var o}(e,d,u)}}a||(a=new Yn);var h=a.get(e);if(h)return h;a.set(e,l),ul(e)?e.forEach((function(r){l.add(sr(r,t,n,r,e,a))})):ol(e)&&e.forEach((function(r,o){l.set(o,sr(r,t,n,o,e,a))}));var y=f?o:(c?s?ai:ii:s?Il:Fl)(e);return Pt(y||e,(function(r,o){y&&(r=e[o=r]),nr(l,o,sr(r,t,n,o,e,a))})),l}function cr(e,t,n){var r=n.length;if(null==e)return!r;for(e=_e(e);r--;){var i=n[r],a=t[i],l=e[i];if(l===o&&!(i in e)||!a(l))return!1}return!0}function fr(e,t,n){if("function"!=typeof e)throw new je(i);return Fi((function(){e.apply(o,n)}),t)}function dr(e,t,n,r){var o=-1,i=Rt,a=!0,l=e.length,u=[],s=t.length;if(!l)return u;n&&(t=Ft(t,Xt(n))),r?(i=At,a=!1):t.length>=200&&(i=Zt,a=!1,t=new Gn(t));e:for(;++o-1},Qn.prototype.set=function(e,t){var n=this.__data__,r=rr(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this},Kn.prototype.clear=function(){this.size=0,this.__data__={hash:new qn,map:new(_n||Qn),string:new qn}},Kn.prototype.delete=function(e){var t=fi(this,e).delete(e);return this.size-=t?1:0,t},Kn.prototype.get=function(e){return fi(this,e).get(e)},Kn.prototype.has=function(e){return fi(this,e).has(e)},Kn.prototype.set=function(e,t){var n=fi(this,e),r=n.size;return n.set(e,t),this.size+=n.size==r?0:1,this},Gn.prototype.add=Gn.prototype.push=function(e){return this.__data__.set(e,a),this},Gn.prototype.has=function(e){return this.__data__.has(e)},Yn.prototype.clear=function(){this.__data__=new Qn,this.size=0},Yn.prototype.delete=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n},Yn.prototype.get=function(e){return this.__data__.get(e)},Yn.prototype.has=function(e){return this.__data__.has(e)},Yn.prototype.set=function(e,t){var n=this.__data__;if(n instanceof Qn){var r=n.__data__;if(!_n||r.length<199)return r.push([e,t]),this.size=++n.size,this;n=this.__data__=new Kn(r)}return n.set(e,t),this.size=n.size,this};var pr=Do(xr),hr=Do(kr,!0);function yr(e,t){var n=!0;return pr(e,(function(e,r,o){return n=!!t(e,r,o)})),n}function vr(e,t,n){for(var r=-1,i=e.length;++r0&&n(l)?t>1?gr(l,t-1,n,r,o):It(o,l):r||(o[o.length]=l)}return o}var br=No(),wr=No(!0);function xr(e,t){return e&&br(e,t,Fl)}function kr(e,t){return e&&wr(e,t,Fl)}function Or(e,t){return Tt(t,(function(t){return Za(e[t])}))}function Sr(e,t){for(var n=0,r=(t=xo(t,e)).length;null!=e&&nt}function Cr(e,t){return null!=e&&De.call(e,t)}function jr(e,t){return null!=e&&t in _e(e)}function Tr(e,t,r){for(var i=r?At:Rt,a=e[0].length,l=e.length,u=l,s=n(l),c=1/0,f=[];u--;){var d=e[u];u&&t&&(d=Ft(d,Xt(t))),c=wn(d.length,c),s[u]=!r&&(t||a>=120&&d.length>=120)?new Gn(u&&d):o}d=e[0];var p=-1,h=s[0];e:for(;++p=l?u:u*("desc"==n[r]?-1:1)}return e.index-t.index}(e,t,n)}))}function qr(e,t,n){for(var r=-1,o=t.length,i={};++r-1;)l!==e&&Ge.call(l,u,1),Ge.call(e,u,1);return e}function Kr(e,t){for(var n=e?t.length:0,r=n-1;n--;){var o=t[n];if(n==r||o!==i){var i=o;wi(o)?Ge.call(e,o,1):po(e,o)}}return e}function Gr(e,t){return e+yt(On()*(t-e+1))}function Yr(e,t){var n="";if(!e||t<1||t>h)return n;do{t%2&&(n+=e),(t=yt(t/2))&&(e+=e)}while(t);return n}function Xr(e,t){return Ii(Ci(e,t,iu),e+"")}function Jr(e){return Jn(Vl(e))}function Zr(e,t){var n=Vl(e);return Li(n,ur(t,0,n.length))}function eo(e,t,n,r){if(!nl(e))return e;for(var i=-1,a=(t=xo(t,e)).length,l=a-1,u=e;null!=u&&++ii?0:i+t),(r=r>i?i:r)<0&&(r+=i),i=t>r?0:r-t>>>0,t>>>=0;for(var a=n(i);++o>>1,a=e[i];null!==a&&!cl(a)&&(n?a<=t:a=200){var s=t?null:Xo(e);if(s)return fn(s);a=!1,o=Zt,u=new Gn}else u=t?[]:l;e:for(;++r=r?e:oo(e,t,n)}var So=ut||function(e){return pt.clearTimeout(e)};function Eo(e,t){if(t)return e.slice();var n=e.length,r=Ue?Ue(n):new e.constructor(n);return e.copy(r),r}function _o(e){var t=new e.constructor(e.byteLength);return new We(t).set(new We(e)),t}function Po(e,t){var n=t?_o(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.length)}function Co(e,t){if(e!==t){var n=e!==o,r=null===e,i=e===e,a=cl(e),l=t!==o,u=null===t,s=t===t,c=cl(t);if(!u&&!c&&!a&&e>t||a&&l&&s&&!u&&!c||r&&l&&s||!n&&s||!i)return 1;if(!r&&!a&&!c&&e1?n[i-1]:o,l=i>2?n[2]:o;for(a=e.length>3&&"function"==typeof a?(i--,a):o,l&&xi(n[0],n[1],l)&&(a=i<3?o:a,i=1),t=_e(t);++r-1?i[a?t[l]:l]:o}}function $o(e){return oi((function(t){var n=t.length,r=n,a=Wn.prototype.thru;for(e&&t.reverse();r--;){var l=t[r];if("function"!=typeof l)throw new je(i);if(a&&!u&&"wrapper"==ui(l))var u=new Wn([],!0)}for(r=u?r:n;++r1&&b.reverse(),p&&cu))return!1;var c=a.get(e),f=a.get(t);if(c&&f)return c==t&&f==e;var d=-1,p=!0,h=2&n?new Gn:o;for(a.set(e,t),a.set(t,e);++d-1&&e%1==0&&e1?"& ":"")+t[r],t=t.join(n>2?", ":" "),e.replace(ue,"{\n/* [wrapped with "+t+"] */\n")}(r,function(e,t){return Pt(m,(function(n){var r="_."+n[0];t&n[1]&&!Rt(e,r)&&e.push(r)})),e.sort()}(function(e){var t=e.match(se);return t?t[1].split(ce):[]}(r),n)))}function Ni(e){var t=0,n=0;return function(){var r=xn(),i=16-(r-n);if(n=r,i>0){if(++t>=800)return arguments[0]}else t=0;return e.apply(o,arguments)}}function Li(e,t){var n=-1,r=e.length,i=r-1;for(t=t===o?r:t;++n1?e[t-1]:o;return n="function"==typeof n?(e.pop(),n):o,la(e,n)}));function ha(e){var t=$n(e);return t.__chain__=!0,t}function ya(e,t){return t(e)}var va=oi((function(e){var t=e.length,n=t?e[0]:0,r=this.__wrapped__,i=function(t){return lr(t,e)};return!(t>1||this.__actions__.length)&&r instanceof Un&&wi(n)?((r=r.slice(n,+n+(t?1:0))).__actions__.push({func:ya,args:[i],thisArg:o}),new Wn(r,this.__chain__).thru((function(e){return t&&!e.length&&e.push(o),e}))):this.thru(i)}));var ma=Fo((function(e,t,n){De.call(e,n)?++e[n]:ar(e,n,1)}));var ga=zo(Ui),ba=zo(qi);function wa(e,t){return(qa(e)?Pt:pr)(e,ci(t,3))}function xa(e,t){return(qa(e)?Ct:hr)(e,ci(t,3))}var ka=Fo((function(e,t,n){De.call(e,n)?e[n].push(t):ar(e,n,[t])}));var Oa=Xr((function(e,t,r){var o=-1,i="function"==typeof t,a=Ka(e)?n(e.length):[];return pr(e,(function(e){a[++o]=i?Et(t,e,r):Rr(e,t,r)})),a})),Sa=Fo((function(e,t,n){ar(e,n,t)}));function Ea(e,t){return(qa(e)?Ft:zr)(e,ci(t,3))}var _a=Fo((function(e,t,n){e[n?0:1].push(t)}),(function(){return[[],[]]}));var Pa=Xr((function(e,t){if(null==e)return[];var n=t.length;return n>1&&xi(e,t[0],t[1])?t=[]:n>2&&xi(t[0],t[1],t[2])&&(t=[t[0]]),Ur(e,gr(t,1),[])})),Ca=ft||function(){return pt.Date.now()};function ja(e,t,n){return t=n?o:t,t=e&&null==t?e.length:t,Zo(e,f,o,o,o,o,t)}function Ta(e,t){var n;if("function"!=typeof t)throw new je(i);return e=vl(e),function(){return--e>0&&(n=t.apply(this,arguments)),e<=1&&(t=o),n}}var Ra=Xr((function(e,t,n){var r=1;if(n.length){var o=cn(n,si(Ra));r|=s}return Zo(e,r,t,n,o)})),Aa=Xr((function(e,t,n){var r=3;if(n.length){var o=cn(n,si(Aa));r|=s}return Zo(t,r,e,n,o)}));function Fa(e,t,n){var r,a,l,u,s,c,f=0,d=!1,p=!1,h=!0;if("function"!=typeof e)throw new je(i);function y(t){var n=r,i=a;return r=a=o,f=t,u=e.apply(i,n)}function v(e){return f=e,s=Fi(g,t),d?y(e):u}function m(e){var n=e-c;return c===o||n>=t||n<0||p&&e-f>=l}function g(){var e=Ca();if(m(e))return b(e);s=Fi(g,function(e){var n=t-(e-c);return p?wn(n,l-(e-f)):n}(e))}function b(e){return s=o,h&&r?y(e):(r=a=o,u)}function w(){var e=Ca(),n=m(e);if(r=arguments,a=this,c=e,n){if(s===o)return v(c);if(p)return So(s),s=Fi(g,t),y(c)}return s===o&&(s=Fi(g,t)),u}return t=gl(t)||0,nl(n)&&(d=!!n.leading,l=(p="maxWait"in n)?bn(gl(n.maxWait)||0,t):l,h="trailing"in n?!!n.trailing:h),w.cancel=function(){s!==o&&So(s),f=0,r=c=a=s=o},w.flush=function(){return s===o?u:b(Ca())},w}var Ia=Xr((function(e,t){return fr(e,1,t)})),Da=Xr((function(e,t,n){return fr(e,gl(t)||0,n)}));function Na(e,t){if("function"!=typeof e||null!=t&&"function"!=typeof t)throw new je(i);var n=function n(){var r=arguments,o=t?t.apply(this,r):r[0],i=n.cache;if(i.has(o))return i.get(o);var a=e.apply(this,r);return n.cache=i.set(o,a)||i,a};return n.cache=new(Na.Cache||Kn),n}function La(e){if("function"!=typeof e)throw new je(i);return function(){var t=arguments;switch(t.length){case 0:return!e.call(this);case 1:return!e.call(this,t[0]);case 2:return!e.call(this,t[0],t[1]);case 3:return!e.call(this,t[0],t[1],t[2])}return!e.apply(this,t)}}Na.Cache=Kn;var Ma=ko((function(e,t){var n=(t=1==t.length&&qa(t[0])?Ft(t[0],Xt(ci())):Ft(gr(t,1),Xt(ci()))).length;return Xr((function(r){for(var o=-1,i=wn(r.length,n);++o=t})),Ua=Ar(function(){return arguments}())?Ar:function(e){return rl(e)&&De.call(e,"callee")&&!Ke.call(e,"callee")},qa=n.isArray,Qa=bt?Xt(bt):function(e){return rl(e)&&_r(e)==I};function Ka(e){return null!=e&&tl(e.length)&&!Za(e)}function Ga(e){return rl(e)&&Ka(e)}var Ya=gt||gu,Xa=wt?Xt(wt):function(e){return rl(e)&&_r(e)==x};function Ja(e){if(!rl(e))return!1;var t=_r(e);return t==k||"[object DOMException]"==t||"string"==typeof e.message&&"string"==typeof e.name&&!al(e)}function Za(e){if(!nl(e))return!1;var t=_r(e);return t==O||t==S||"[object AsyncFunction]"==t||"[object Proxy]"==t}function el(e){return"number"==typeof e&&e==vl(e)}function tl(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=h}function nl(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function rl(e){return null!=e&&"object"==typeof e}var ol=xt?Xt(xt):function(e){return rl(e)&&vi(e)==E};function il(e){return"number"==typeof e||rl(e)&&_r(e)==_}function al(e){if(!rl(e)||_r(e)!=P)return!1;var t=qe(e);if(null===t)return!0;var n=De.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&Ie.call(n)==Be}var ll=kt?Xt(kt):function(e){return rl(e)&&_r(e)==j};var ul=Ot?Xt(Ot):function(e){return rl(e)&&vi(e)==T};function sl(e){return"string"==typeof e||!qa(e)&&rl(e)&&_r(e)==R}function cl(e){return"symbol"==typeof e||rl(e)&&_r(e)==A}var fl=St?Xt(St):function(e){return rl(e)&&tl(e.length)&&!!at[_r(e)]};var dl=Ko(Br),pl=Ko((function(e,t){return e<=t}));function hl(e){if(!e)return[];if(Ka(e))return sl(e)?hn(e):Ro(e);if(Xe&&e[Xe])return function(e){for(var t,n=[];!(t=e.next()).done;)n.push(t.value);return n}(e[Xe]());var t=vi(e);return(t==E?un:t==T?fn:Vl)(e)}function yl(e){return e?(e=gl(e))===p||e===-1/0?17976931348623157e292*(e<0?-1:1):e===e?e:0:0===e?e:0}function vl(e){var t=yl(e),n=t%1;return t===t?n?t-n:t:0}function ml(e){return e?ur(vl(e),0,v):0}function gl(e){if("number"==typeof e)return e;if(cl(e))return y;if(nl(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=nl(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=Yt(e);var n=me.test(e);return n||be.test(e)?ct(e.slice(2),n?2:8):ve.test(e)?y:+e}function bl(e){return Ao(e,Il(e))}function wl(e){return null==e?"":co(e)}var xl=Io((function(e,t){if(Ei(t)||Ka(t))Ao(t,Fl(t),e);else for(var n in t)De.call(t,n)&&nr(e,n,t[n])})),kl=Io((function(e,t){Ao(t,Il(t),e)})),Ol=Io((function(e,t,n,r){Ao(t,Il(t),e,r)})),Sl=Io((function(e,t,n,r){Ao(t,Fl(t),e,r)})),El=oi(lr);var _l=Xr((function(e,t){e=_e(e);var n=-1,r=t.length,i=r>2?t[2]:o;for(i&&xi(t[0],t[1],i)&&(r=1);++n1),t})),Ao(e,ai(e),n),r&&(n=sr(n,7,ni));for(var o=t.length;o--;)po(n,t[o]);return n}));var Ml=oi((function(e,t){return null==e?{}:function(e,t){return qr(e,t,(function(t,n){return jl(e,n)}))}(e,t)}));function Bl(e,t){if(null==e)return{};var n=Ft(ai(e),(function(e){return[e]}));return t=ci(t),qr(e,n,(function(e,n){return t(e,n[0])}))}var zl=Jo(Fl),$l=Jo(Il);function Vl(e){return null==e?[]:Jt(e,Fl(e))}var Hl=Mo((function(e,t,n){return t=t.toLowerCase(),e+(n?Wl(t):t)}));function Wl(e){return Jl(wl(e).toLowerCase())}function Ul(e){return(e=wl(e))&&e.replace(xe,rn).replace(Ze,"")}var ql=Mo((function(e,t,n){return e+(n?"-":"")+t.toLowerCase()})),Ql=Mo((function(e,t,n){return e+(n?" ":"")+t.toLowerCase()})),Kl=Lo("toLowerCase");var Gl=Mo((function(e,t,n){return e+(n?"_":"")+t.toLowerCase()}));var Yl=Mo((function(e,t,n){return e+(n?" ":"")+Jl(t)}));var Xl=Mo((function(e,t,n){return e+(n?" ":"")+t.toUpperCase()})),Jl=Lo("toUpperCase");function Zl(e,t,n){return e=wl(e),(t=n?o:t)===o?function(e){return rt.test(e)}(e)?function(e){return e.match(tt)||[]}(e):function(e){return e.match(fe)||[]}(e):e.match(t)||[]}var eu=Xr((function(e,t){try{return Et(e,o,t)}catch(n){return Ja(n)?n:new le(n)}})),tu=oi((function(e,t){return Pt(t,(function(t){t=Bi(t),ar(e,t,Ra(e[t],e))})),e}));function nu(e){return function(){return e}}var ru=$o(),ou=$o(!0);function iu(e){return e}function au(e){return Nr("function"==typeof e?e:sr(e,1))}var lu=Xr((function(e,t){return function(n){return Rr(n,e,t)}})),uu=Xr((function(e,t){return function(n){return Rr(e,n,t)}}));function su(e,t,n){var r=Fl(t),o=Or(t,r);null!=n||nl(t)&&(o.length||!r.length)||(n=t,t=e,e=this,o=Or(t,Fl(t)));var i=!(nl(n)&&"chain"in n)||!!n.chain,a=Za(e);return Pt(o,(function(n){var r=t[n];e[n]=r,a&&(e.prototype[n]=function(){var t=this.__chain__;if(i||t){var n=e(this.__wrapped__),o=n.__actions__=Ro(this.__actions__);return o.push({func:r,args:arguments,thisArg:e}),n.__chain__=t,n}return r.apply(e,It([this.value()],arguments))})})),e}function cu(){}var fu=Uo(Ft),du=Uo(jt),pu=Uo(Lt);function hu(e){return ki(e)?Ut(Bi(e)):function(e){return function(t){return Sr(t,e)}}(e)}var yu=Qo(),vu=Qo(!0);function mu(){return[]}function gu(){return!1}var bu=Wo((function(e,t){return e+t}),0),wu=Yo("ceil"),xu=Wo((function(e,t){return e/t}),1),ku=Yo("floor");var Ou=Wo((function(e,t){return e*t}),1),Su=Yo("round"),Eu=Wo((function(e,t){return e-t}),0);return $n.after=function(e,t){if("function"!=typeof t)throw new je(i);return e=vl(e),function(){if(--e<1)return t.apply(this,arguments)}},$n.ary=ja,$n.assign=xl,$n.assignIn=kl,$n.assignInWith=Ol,$n.assignWith=Sl,$n.at=El,$n.before=Ta,$n.bind=Ra,$n.bindAll=tu,$n.bindKey=Aa,$n.castArray=function(){if(!arguments.length)return[];var e=arguments[0];return qa(e)?e:[e]},$n.chain=ha,$n.chunk=function(e,t,r){t=(r?xi(e,t,r):t===o)?1:bn(vl(t),0);var i=null==e?0:e.length;if(!i||t<1)return[];for(var a=0,l=0,u=n(ht(i/t));ai?0:i+n),(r=r===o||r>i?i:vl(r))<0&&(r+=i),r=n>r?0:ml(r);n>>0)?(e=wl(e))&&("string"==typeof t||null!=t&&!ll(t))&&!(t=co(t))&&ln(e)?Oo(hn(e),0,n):e.split(t,n):[]},$n.spread=function(e,t){if("function"!=typeof e)throw new je(i);return t=null==t?0:bn(vl(t),0),Xr((function(n){var r=n[t],o=Oo(n,0,t);return r&&It(o,r),Et(e,this,o)}))},$n.tail=function(e){var t=null==e?0:e.length;return t?oo(e,1,t):[]},$n.take=function(e,t,n){return e&&e.length?oo(e,0,(t=n||t===o?1:vl(t))<0?0:t):[]},$n.takeRight=function(e,t,n){var r=null==e?0:e.length;return r?oo(e,(t=r-(t=n||t===o?1:vl(t)))<0?0:t,r):[]},$n.takeRightWhile=function(e,t){return e&&e.length?yo(e,ci(t,3),!1,!0):[]},$n.takeWhile=function(e,t){return e&&e.length?yo(e,ci(t,3)):[]},$n.tap=function(e,t){return t(e),e},$n.throttle=function(e,t,n){var r=!0,o=!0;if("function"!=typeof e)throw new je(i);return nl(n)&&(r="leading"in n?!!n.leading:r,o="trailing"in n?!!n.trailing:o),Fa(e,t,{leading:r,maxWait:t,trailing:o})},$n.thru=ya,$n.toArray=hl,$n.toPairs=zl,$n.toPairsIn=$l,$n.toPath=function(e){return qa(e)?Ft(e,Bi):cl(e)?[e]:Ro(Mi(wl(e)))},$n.toPlainObject=bl,$n.transform=function(e,t,n){var r=qa(e),o=r||Ya(e)||fl(e);if(t=ci(t,4),null==n){var i=e&&e.constructor;n=o?r?new i:[]:nl(e)&&Za(i)?Vn(qe(e)):{}}return(o?Pt:xr)(e,(function(e,r,o){return t(n,e,r,o)})),n},$n.unary=function(e){return ja(e,1)},$n.union=ra,$n.unionBy=oa,$n.unionWith=ia,$n.uniq=function(e){return e&&e.length?fo(e):[]},$n.uniqBy=function(e,t){return e&&e.length?fo(e,ci(t,2)):[]},$n.uniqWith=function(e,t){return t="function"==typeof t?t:o,e&&e.length?fo(e,o,t):[]},$n.unset=function(e,t){return null==e||po(e,t)},$n.unzip=aa,$n.unzipWith=la,$n.update=function(e,t,n){return null==e?e:ho(e,t,wo(n))},$n.updateWith=function(e,t,n,r){return r="function"==typeof r?r:o,null==e?e:ho(e,t,wo(n),r)},$n.values=Vl,$n.valuesIn=function(e){return null==e?[]:Jt(e,Il(e))},$n.without=ua,$n.words=Zl,$n.wrap=function(e,t){return Ba(wo(t),e)},$n.xor=sa,$n.xorBy=ca,$n.xorWith=fa,$n.zip=da,$n.zipObject=function(e,t){return go(e||[],t||[],nr)},$n.zipObjectDeep=function(e,t){return go(e||[],t||[],eo)},$n.zipWith=pa,$n.entries=zl,$n.entriesIn=$l,$n.extend=kl,$n.extendWith=Ol,su($n,$n),$n.add=bu,$n.attempt=eu,$n.camelCase=Hl,$n.capitalize=Wl,$n.ceil=wu,$n.clamp=function(e,t,n){return n===o&&(n=t,t=o),n!==o&&(n=(n=gl(n))===n?n:0),t!==o&&(t=(t=gl(t))===t?t:0),ur(gl(e),t,n)},$n.clone=function(e){return sr(e,4)},$n.cloneDeep=function(e){return sr(e,5)},$n.cloneDeepWith=function(e,t){return sr(e,5,t="function"==typeof t?t:o)},$n.cloneWith=function(e,t){return sr(e,4,t="function"==typeof t?t:o)},$n.conformsTo=function(e,t){return null==t||cr(e,t,Fl(t))},$n.deburr=Ul,$n.defaultTo=function(e,t){return null==e||e!==e?t:e},$n.divide=xu,$n.endsWith=function(e,t,n){e=wl(e),t=co(t);var r=e.length,i=n=n===o?r:ur(vl(n),0,r);return(n-=t.length)>=0&&e.slice(n,i)==t},$n.eq=Va,$n.escape=function(e){return(e=wl(e))&&X.test(e)?e.replace(G,on):e},$n.escapeRegExp=function(e){return(e=wl(e))&&ie.test(e)?e.replace(oe,"\\$&"):e},$n.every=function(e,t,n){var r=qa(e)?jt:yr;return n&&xi(e,t,n)&&(t=o),r(e,ci(t,3))},$n.find=ga,$n.findIndex=Ui,$n.findKey=function(e,t){return Bt(e,ci(t,3),xr)},$n.findLast=ba,$n.findLastIndex=qi,$n.findLastKey=function(e,t){return Bt(e,ci(t,3),kr)},$n.floor=ku,$n.forEach=wa,$n.forEachRight=xa,$n.forIn=function(e,t){return null==e?e:br(e,ci(t,3),Il)},$n.forInRight=function(e,t){return null==e?e:wr(e,ci(t,3),Il)},$n.forOwn=function(e,t){return e&&xr(e,ci(t,3))},$n.forOwnRight=function(e,t){return e&&kr(e,ci(t,3))},$n.get=Cl,$n.gt=Ha,$n.gte=Wa,$n.has=function(e,t){return null!=e&&mi(e,t,Cr)},$n.hasIn=jl,$n.head=Ki,$n.identity=iu,$n.includes=function(e,t,n,r){e=Ka(e)?e:Vl(e),n=n&&!r?vl(n):0;var o=e.length;return n<0&&(n=bn(o+n,0)),sl(e)?n<=o&&e.indexOf(t,n)>-1:!!o&&$t(e,t,n)>-1},$n.indexOf=function(e,t,n){var r=null==e?0:e.length;if(!r)return-1;var o=null==n?0:vl(n);return o<0&&(o=bn(r+o,0)),$t(e,t,o)},$n.inRange=function(e,t,n){return t=yl(t),n===o?(n=t,t=0):n=yl(n),function(e,t,n){return e>=wn(t,n)&&e=-9007199254740991&&e<=h},$n.isSet=ul,$n.isString=sl,$n.isSymbol=cl,$n.isTypedArray=fl,$n.isUndefined=function(e){return e===o},$n.isWeakMap=function(e){return rl(e)&&vi(e)==F},$n.isWeakSet=function(e){return rl(e)&&"[object WeakSet]"==_r(e)},$n.join=function(e,t){return null==e?"":qt.call(e,t)},$n.kebabCase=ql,$n.last=Ji,$n.lastIndexOf=function(e,t,n){var r=null==e?0:e.length;if(!r)return-1;var i=r;return n!==o&&(i=(i=vl(n))<0?bn(r+i,0):wn(i,r-1)),t===t?function(e,t,n){for(var r=n+1;r--;)if(e[r]===t)return r;return r}(e,t,i):zt(e,Ht,i,!0)},$n.lowerCase=Ql,$n.lowerFirst=Kl,$n.lt=dl,$n.lte=pl,$n.max=function(e){return e&&e.length?vr(e,iu,Pr):o},$n.maxBy=function(e,t){return e&&e.length?vr(e,ci(t,2),Pr):o},$n.mean=function(e){return Wt(e,iu)},$n.meanBy=function(e,t){return Wt(e,ci(t,2))},$n.min=function(e){return e&&e.length?vr(e,iu,Br):o},$n.minBy=function(e,t){return e&&e.length?vr(e,ci(t,2),Br):o},$n.stubArray=mu,$n.stubFalse=gu,$n.stubObject=function(){return{}},$n.stubString=function(){return""},$n.stubTrue=function(){return!0},$n.multiply=Ou,$n.nth=function(e,t){return e&&e.length?Wr(e,vl(t)):o},$n.noConflict=function(){return pt._===this&&(pt._=ze),this},$n.noop=cu,$n.now=Ca,$n.pad=function(e,t,n){e=wl(e);var r=(t=vl(t))?pn(e):0;if(!t||r>=t)return e;var o=(t-r)/2;return qo(yt(o),n)+e+qo(ht(o),n)},$n.padEnd=function(e,t,n){e=wl(e);var r=(t=vl(t))?pn(e):0;return t&&rt){var r=e;e=t,t=r}if(n||e%1||t%1){var i=On();return wn(e+i*(t-e+st("1e-"+((i+"").length-1))),t)}return Gr(e,t)},$n.reduce=function(e,t,n){var r=qa(e)?Dt:Qt,o=arguments.length<3;return r(e,ci(t,4),n,o,pr)},$n.reduceRight=function(e,t,n){var r=qa(e)?Nt:Qt,o=arguments.length<3;return r(e,ci(t,4),n,o,hr)},$n.repeat=function(e,t,n){return t=(n?xi(e,t,n):t===o)?1:vl(t),Yr(wl(e),t)},$n.replace=function(){var e=arguments,t=wl(e[0]);return e.length<3?t:t.replace(e[1],e[2])},$n.result=function(e,t,n){var r=-1,i=(t=xo(t,e)).length;for(i||(i=1,e=o);++rh)return[];var n=v,r=wn(e,v);t=ci(t),e-=v;for(var o=Gt(r,t);++n=a)return e;var u=n-pn(r);if(u<1)return r;var s=l?Oo(l,0,u).join(""):e.slice(0,u);if(i===o)return s+r;if(l&&(u+=s.length-u),ll(i)){if(e.slice(u).search(i)){var c,f=s;for(i.global||(i=Pe(i.source,wl(ye.exec(i))+"g")),i.lastIndex=0;c=i.exec(f);)var d=c.index;s=s.slice(0,d===o?u:d)}}else if(e.indexOf(co(i),u)!=u){var p=s.lastIndexOf(i);p>-1&&(s=s.slice(0,p))}return s+r},$n.unescape=function(e){return(e=wl(e))&&Y.test(e)?e.replace(K,vn):e},$n.uniqueId=function(e){var t=++Ne;return wl(e)+t},$n.upperCase=Xl,$n.upperFirst=Jl,$n.each=wa,$n.eachRight=xa,$n.first=Ki,su($n,function(){var e={};return xr($n,(function(t,n){De.call($n.prototype,n)||(e[n]=t)})),e}(),{chain:!1}),$n.VERSION="4.17.21",Pt(["bind","bindKey","curry","curryRight","partial","partialRight"],(function(e){$n[e].placeholder=$n})),Pt(["drop","take"],(function(e,t){Un.prototype[e]=function(n){n=n===o?1:bn(vl(n),0);var r=this.__filtered__&&!t?new Un(this):this.clone();return r.__filtered__?r.__takeCount__=wn(n,r.__takeCount__):r.__views__.push({size:wn(n,v),type:e+(r.__dir__<0?"Right":"")}),r},Un.prototype[e+"Right"]=function(t){return this.reverse()[e](t).reverse()}})),Pt(["filter","map","takeWhile"],(function(e,t){var n=t+1,r=1==n||3==n;Un.prototype[e]=function(e){var t=this.clone();return t.__iteratees__.push({iteratee:ci(e,3),type:n}),t.__filtered__=t.__filtered__||r,t}})),Pt(["head","last"],(function(e,t){var n="take"+(t?"Right":"");Un.prototype[e]=function(){return this[n](1).value()[0]}})),Pt(["initial","tail"],(function(e,t){var n="drop"+(t?"":"Right");Un.prototype[e]=function(){return this.__filtered__?new Un(this):this[n](1)}})),Un.prototype.compact=function(){return this.filter(iu)},Un.prototype.find=function(e){return this.filter(e).head()},Un.prototype.findLast=function(e){return this.reverse().find(e)},Un.prototype.invokeMap=Xr((function(e,t){return"function"==typeof e?new Un(this):this.map((function(n){return Rr(n,e,t)}))})),Un.prototype.reject=function(e){return this.filter(La(ci(e)))},Un.prototype.slice=function(e,t){e=vl(e);var n=this;return n.__filtered__&&(e>0||t<0)?new Un(n):(e<0?n=n.takeRight(-e):e&&(n=n.drop(e)),t!==o&&(n=(t=vl(t))<0?n.dropRight(-t):n.take(t-e)),n)},Un.prototype.takeRightWhile=function(e){return this.reverse().takeWhile(e).reverse()},Un.prototype.toArray=function(){return this.take(v)},xr(Un.prototype,(function(e,t){var n=/^(?:filter|find|map|reject)|While$/.test(t),r=/^(?:head|last)$/.test(t),i=$n[r?"take"+("last"==t?"Right":""):t],a=r||/^find/.test(t);i&&($n.prototype[t]=function(){var t=this.__wrapped__,l=r?[1]:arguments,u=t instanceof Un,s=l[0],c=u||qa(t),f=function(e){var t=i.apply($n,It([e],l));return r&&d?t[0]:t};c&&n&&"function"==typeof s&&1!=s.length&&(u=c=!1);var d=this.__chain__,p=!!this.__actions__.length,h=a&&!d,y=u&&!p;if(!a&&c){t=y?t:new Un(this);var v=e.apply(t,l);return v.__actions__.push({func:ya,args:[f],thisArg:o}),new Wn(v,d)}return h&&y?e.apply(this,l):(v=this.thru(f),h?r?v.value()[0]:v.value():v)})})),Pt(["pop","push","shift","sort","splice","unshift"],(function(e){var t=Te[e],n=/^(?:push|sort|unshift)$/.test(e)?"tap":"thru",r=/^(?:pop|shift)$/.test(e);$n.prototype[e]=function(){var e=arguments;if(r&&!this.__chain__){var o=this.value();return t.apply(qa(o)?o:[],e)}return this[n]((function(n){return t.apply(qa(n)?n:[],e)}))}})),xr(Un.prototype,(function(e,t){var n=$n[t];if(n){var r=n.name+"";De.call(An,r)||(An[r]=[]),An[r].push({name:t,func:n})}})),An[Vo(o,2).name]=[{name:"wrapper",func:o}],Un.prototype.clone=function(){var e=new Un(this.__wrapped__);return e.__actions__=Ro(this.__actions__),e.__dir__=this.__dir__,e.__filtered__=this.__filtered__,e.__iteratees__=Ro(this.__iteratees__),e.__takeCount__=this.__takeCount__,e.__views__=Ro(this.__views__),e},Un.prototype.reverse=function(){if(this.__filtered__){var e=new Un(this);e.__dir__=-1,e.__filtered__=!0}else(e=this.clone()).__dir__*=-1;return e},Un.prototype.value=function(){var e=this.__wrapped__.value(),t=this.__dir__,n=qa(e),r=t<0,o=n?e.length:0,i=function(e,t,n){var r=-1,o=n.length;for(;++r=this.__values__.length;return{done:e,value:e?o:this.__values__[this.__index__++]}},$n.prototype.plant=function(e){for(var t,n=this;n instanceof Hn;){var r=$i(n);r.__index__=0,r.__values__=o,t?i.__wrapped__=r:t=r;var i=r;n=n.__wrapped__}return i.__wrapped__=e,t},$n.prototype.reverse=function(){var e=this.__wrapped__;if(e instanceof Un){var t=e;return this.__actions__.length&&(t=new Un(this)),(t=t.reverse()).__actions__.push({func:ya,args:[na],thisArg:o}),new Wn(t,this.__chain__)}return this.thru(na)},$n.prototype.toJSON=$n.prototype.valueOf=$n.prototype.value=function(){return vo(this.__wrapped__,this.__actions__)},$n.prototype.first=$n.prototype.head,Xe&&($n.prototype[Xe]=function(){return this}),$n}();pt._=mn,(r=function(){return mn}.call(t,n,t,e))===o||(e.exports=r)}.call(this)},725:function(e){"use strict";var t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;function o(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(o){return!1}}()?Object.assign:function(e,i){for(var a,l,u=o(e),s=1;s