diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8f2d74b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2905 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "asio-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb111d0b927c4b860b42a8bd869b94f5f973b0395ed5892f1ab2be2dc6831ea8" +dependencies = [ + "bindgen 0.69.5", + "cc", + "num-derive", + "num-traits", + "parse_cfg", + "walkdir", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bindgen" +version = "0.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +dependencies = [ + "bindgen 0.72.0", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "asio-sys", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-traits", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.61.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jellyfin-asio-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "cpal", + "crossterm", + "dirs", + "once_cell", + "ratatui", + "reqwest", + "rpassword", + "serde", + "serde_json", + "symphonia", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.2", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "parse_cfg" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905787a434a2c721408e7c9a252e85f3d93ca0f118a5283022636c0e05a7ea49" +dependencies = [ + "nom", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "ratatui" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.12.1", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rpassword" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.59.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-alac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-caf", + "symphonia-format-isomp4", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-caf" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio 1.0.4", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result 0.3.4", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bcb6266 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "jellyfin-asio-client" +version = "0.1.0" +edition = "2024" + +[dependencies] +reqwest = { version = "0.11", features = ["json"] } +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +symphonia = { version = "0.5", features = ["all"] } +cpal = { version = "0.15", features = ["asio"] } +ratatui = "0.26" +crossterm = "0.27" +anyhow = "1.0" +rpassword = "7.0" +tracing = "0.1" +tracing-subscriber = "0.3" +dirs = "5.0" +chrono = "0.4" +once_cell = "1.19" \ No newline at end of file diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..4a8b84a --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,624 @@ +use anyhow::Result; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{Device, Stream, StreamConfig, SampleFormat, SampleRate}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use symphonia::core::audio::{AudioBuffer as SymphoniaAudioBuffer, Signal}; +use symphonia::core::codecs::{Decoder, DecoderOptions}; +use symphonia::core::formats::{FormatOptions, FormatReader}; +use symphonia::core::io::MediaSourceStream; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; +use tracing::{info, debug, error, warn}; + +pub struct AsioPlayer { + device: Device, + stream: Option, + forced_sample_rate: Option, + stream_buffer: Option>>, + duration_ms: Option, + start_time: Option, + paused_position: Option, + volume: f32, +} + +// Shared audio buffer for streaming +struct StreamBuffer { + samples: Vec, + position: usize, + sample_rate: u32, + channels: u16, + volume: f32, + paused: bool, +} + +impl AsioPlayer { + pub fn new() -> Result { + let host = cpal::host_from_id(cpal::HostId::Asio)?; + let device = host.default_output_device() + .ok_or_else(|| anyhow::anyhow!("No ASIO output device found"))?; + + info!("Using ASIO device: {}", device.name()?); + + Ok(Self { + device, + stream: None, + forced_sample_rate: None, + stream_buffer: None, + duration_ms: None, + start_time: None, + paused_position: None, + volume: 1.0, + }) + } + + pub fn set_sample_rate(&mut self, sample_rate: u32) -> Result<()> { + // Validate that the device supports this sample rate + let supported_configs: Vec<_> = self.device.supported_output_configs()?.collect(); + + let mut supported = false; + for config in &supported_configs { + if config.min_sample_rate().0 <= sample_rate && config.max_sample_rate().0 >= sample_rate { + supported = true; + break; + } + } + + if !supported { + return Err(anyhow::anyhow!( + "Sample rate {} Hz is not supported by this device. Supported ranges: {:?}", + sample_rate, + supported_configs.iter() + .map(|c| format!("{}-{} Hz", c.min_sample_rate().0, c.max_sample_rate().0)) + .collect::>() + )); + } + + self.forced_sample_rate = Some(sample_rate); + info!("Forced sample rate set to {} Hz", sample_rate); + Ok(()) + } + + pub fn clear_sample_rate(&mut self) { + self.forced_sample_rate = None; + info!("Sample rate forcing cleared - will use automatic selection"); + } + + pub async fn play_url(&mut self, url: &str) -> Result<()> { + info!("Starting playback of: {}", url); + + // Stop any existing playback + self.stop()?; + + // Download and decode the audio + let (samples, original_sample_rate, channels, duration_ms) = self.download_and_decode(url).await?; + + // Set up ASIO stream with proper sample rate + self.setup_stream(samples, original_sample_rate, channels)?; + + // Store duration + self.duration_ms = Some(duration_ms); + + // Start playing + if let Some(stream) = &self.stream { + stream.play()?; + self.start_time = Some(Instant::now()); + self.paused_position = None; + info!("Playback started"); + } + + Ok(()) + } + + pub fn pause(&mut self) -> Result<()> { + if let Some(stream) = &self.stream { + stream.pause()?; + + // Calculate current position when pausing + if let Some(start_time) = self.start_time { + let elapsed = start_time.elapsed().as_secs_f32(); + let duration_secs = self.duration_ms.unwrap_or(0) as f32 / 1000.0; + self.paused_position = Some((elapsed / duration_secs).min(1.0)); + } + + // Set paused flag in buffer + if let Some(buffer) = &self.stream_buffer { + let mut buffer = buffer.lock().unwrap(); + buffer.paused = true; + } + + info!("Playback paused"); + Ok(()) + } else { + Err(anyhow::anyhow!("No active stream to pause")) + } + } + + pub fn resume(&mut self) -> Result<()> { + if let Some(stream) = &self.stream { + // Update start time based on paused position + if let Some(paused_pos) = self.paused_position { + let duration_secs = self.duration_ms.unwrap_or(0) as f32 / 1000.0; + let elapsed_secs = paused_pos * duration_secs; + self.start_time = Some(Instant::now() - Duration::from_secs_f32(elapsed_secs)); + } + + // Clear paused flag in buffer + if let Some(buffer) = &self.stream_buffer { + let mut buffer = buffer.lock().unwrap(); + buffer.paused = false; + } + + stream.play()?; + info!("Playback resumed"); + Ok(()) + } else { + Err(anyhow::anyhow!("No active stream to resume")) + } + } + + pub fn stop(&mut self) -> Result<()> { + if let Some(stream) = &self.stream { + stream.pause()?; + self.stream = None; + self.stream_buffer = None; + self.start_time = None; + self.paused_position = None; + info!("Playback stopped"); + Ok(()) + } else { + // Not an error if already stopped + Ok(()) + } + } + + pub fn seek(&mut self, position: f32) -> Result<()> { + // position is 0.0 - 1.0 (percentage of track) + let position = position.max(0.0).min(1.0); + + if let Some(buffer) = &self.stream_buffer { + let mut buffer = buffer.lock().unwrap(); + let total_frames = buffer.samples.len() / buffer.channels as usize; + let new_frame = (total_frames as f32 * position) as usize; + buffer.position = new_frame * buffer.channels as usize; + + // Update start time to reflect new position + if let Some(duration_ms) = self.duration_ms { + let duration_secs = duration_ms as f32 / 1000.0; + let elapsed_secs = position * duration_secs; + self.start_time = Some(Instant::now() - Duration::from_secs_f32(elapsed_secs)); + } + + if let Some(stream) = &self.stream { + if buffer.paused { + self.paused_position = Some(position); + } + } + + info!("Seeked to position: {:.1}%", position * 100.0); + Ok(()) + } else { + Err(anyhow::anyhow!("No active stream to seek")) + } + } + + pub fn set_volume(&mut self, volume: f32) -> Result<()> { + let volume = volume.max(0.0).min(1.0); + self.volume = volume; + + if let Some(buffer) = &self.stream_buffer { + let mut buffer = buffer.lock().unwrap(); + buffer.volume = volume; + info!("Volume set to: {:.0}%", volume * 100.0); + } + + Ok(()) + } + + pub fn get_position(&self) -> Option { + if let (Some(start_time), Some(duration_ms)) = (self.start_time, self.duration_ms) { + if let Some(paused_pos) = self.paused_position { + return Some(paused_pos); + } + + let elapsed = start_time.elapsed().as_millis() as f32; + let duration = duration_ms as f32; + Some((elapsed / duration).min(1.0)) + } else { + None + } + } + + async fn download_and_decode(&self, url: &str) -> Result<(Vec, u32, u16, u64)> { + debug!("Downloading audio from: {}", url); + + let response = reqwest::get(url).await?; + let bytes = response.bytes().await?; + + debug!("Downloaded {} bytes, decoding...", bytes.len()); + + // Create media source + let cursor = std::io::Cursor::new(bytes); + let media_source = Box::new(cursor); + let mss = MediaSourceStream::new(media_source, Default::default()); + + // Probe the format + let mut hint = Hint::new(); + let format_opts = FormatOptions::default(); + let metadata_opts = MetadataOptions::default(); + + let probed = symphonia::default::get_probe() + .format(&hint, mss, &format_opts, &metadata_opts)?; + + let mut format_reader = probed.format; + let track = format_reader.tracks() + .iter() + .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL) + .ok_or_else(|| anyhow::anyhow!("No audio track found"))?; + + let track_id = track.id; + let sample_rate = track.codec_params.sample_rate.unwrap_or(44100); + let channels = track.codec_params.channels.map(|c| c.count()).unwrap_or(2) as u16; + + // Create decoder + let decoder_opts = DecoderOptions::default(); + let mut decoder = symphonia::default::get_codecs() + .make(&track.codec_params, &decoder_opts)?; + + info!("Decoding: {} channels, {} Hz", channels, sample_rate); + + let mut samples = Vec::new(); + + // Decode packets + loop { + match format_reader.next_packet() { + Ok(packet) => { + if packet.track_id() != track_id { + continue; + } + + match decoder.decode(&packet) { + Ok(decoded) => { + // Convert samples based on the actual format + match decoded { + symphonia::core::audio::AudioBufferRef::F32(buf) => { + let spec = *buf.spec(); + if spec.channels.count() == 1 { + // Mono + let chan = buf.chan(0); + samples.extend_from_slice(chan); + } else if spec.channels.count() == 2 { + // Stereo - interleave L/R + let left = buf.chan(0); + let right = buf.chan(1); + + for i in 0..left.len() { + samples.push(left[i]); + samples.push(right[i]); + } + } + } + symphonia::core::audio::AudioBufferRef::S32(buf) => { + let spec = *buf.spec(); + if spec.channels.count() == 1 { + // Mono - convert i32 to f32 + let chan = buf.chan(0); + for &sample in chan { + samples.push(sample as f32 / i32::MAX as f32); + } + } else if spec.channels.count() == 2 { + // Stereo - interleave L/R and convert + let left = buf.chan(0); + let right = buf.chan(1); + + for i in 0..left.len() { + samples.push(left[i] as f32 / i32::MAX as f32); + samples.push(right[i] as f32 / i32::MAX as f32); + } + } + } + symphonia::core::audio::AudioBufferRef::S16(buf) => { + let spec = *buf.spec(); + if spec.channels.count() == 1 { + // Mono - convert i16 to f32 + let chan = buf.chan(0); + for &sample in chan { + samples.push(sample as f32 / i16::MAX as f32); + } + } else if spec.channels.count() == 2 { + // Stereo - interleave L/R and convert + let left = buf.chan(0); + let right = buf.chan(1); + + for i in 0..left.len() { + samples.push(left[i] as f32 / i16::MAX as f32); + samples.push(right[i] as f32 / i16::MAX as f32); + } + } + } + _ => { + debug!("Unsupported audio format"); + } + } + } + Err(e) => { + debug!("Decode error: {}", e); + break; + } + } + } + Err(_) => break, + } + } + + // Calculate duration in milliseconds + let num_frames = samples.len() / channels as usize; + let duration_ms = (num_frames as f64 / sample_rate as f64 * 1000.0) as u64; + + info!("Decoded {} samples, duration: {}ms", samples.len(), duration_ms); + Ok((samples, sample_rate, channels, duration_ms)) + } + + fn setup_stream(&mut self, samples: Vec, source_sample_rate: u32, source_channels: u16) -> Result<()> { + // Get the device's supported configurations + let supported_configs: Vec<_> = self.device.supported_output_configs()?.collect(); + + // Find a configuration that matches our requirements + let mut selected_config = None; + let mut target_sample_rate = self.forced_sample_rate.unwrap_or(source_sample_rate); + + // If we have a forced sample rate, prioritize that + if let Some(forced_rate) = self.forced_sample_rate { + for config in &supported_configs { + if config.min_sample_rate().0 <= forced_rate && + config.max_sample_rate().0 >= forced_rate && + config.channels() == source_channels { + selected_config = Some(config.with_sample_rate(SampleRate(forced_rate))); + target_sample_rate = forced_rate; + info!("Using forced sample rate: {} Hz", forced_rate); + break; + } + } + + // If forced rate doesn't work with desired channels, try with any channel count + if selected_config.is_none() { + for config in &supported_configs { + if config.min_sample_rate().0 <= forced_rate && + config.max_sample_rate().0 >= forced_rate { + selected_config = Some(config.with_sample_rate(SampleRate(forced_rate))); + target_sample_rate = forced_rate; + warn!("Using forced sample rate {} Hz but with {} channels instead of requested {}", + forced_rate, config.channels(), source_channels); + break; + } + } + } + } + + // If no forced rate or forced rate didn't work, try to find exact match for source + if selected_config.is_none() { + for config in &supported_configs { + if config.min_sample_rate().0 <= source_sample_rate && + config.max_sample_rate().0 >= source_sample_rate && + config.channels() == source_channels { + selected_config = Some(config.with_sample_rate(SampleRate(source_sample_rate))); + break; + } + } + + // If no exact match, find a reasonable alternative + if selected_config.is_none() { + for config in &supported_configs { + if config.channels() == source_channels { + // Use the minimum supported sample rate if source is too low + if source_sample_rate < config.min_sample_rate().0 { + target_sample_rate = config.min_sample_rate().0; + selected_config = Some(config.with_sample_rate(SampleRate(target_sample_rate))); + break; + } + // Use the maximum supported sample rate if source is too high + else if source_sample_rate > config.max_sample_rate().0 { + target_sample_rate = config.max_sample_rate().0; + selected_config = Some(config.with_sample_rate(SampleRate(target_sample_rate))); + break; + } + } + } + } + } + + // Fallback to device default if nothing works + let config = if let Some(config) = selected_config { + config + } else { + warn!("No suitable config found, using device default"); + self.device.default_output_config()? + }; + + info!("Selected config: {} channels, {} Hz, format: {:?}", + config.channels(), config.sample_rate().0, config.sample_format()); + + // Resample if necessary + let final_samples = if target_sample_rate != source_sample_rate { + warn!("Resampling from {} Hz to {} Hz", source_sample_rate, target_sample_rate); + self.resample(samples, source_sample_rate, target_sample_rate, source_channels) + } else { + samples + }; + + let audio_buffer = Arc::new(Mutex::new(StreamBuffer { + samples: final_samples, + position: 0, + sample_rate: target_sample_rate, + channels: source_channels, + volume: self.volume, + paused: false, + })); + + self.stream_buffer = Some(audio_buffer.clone()); + + let stream_config = config.config(); + + // Build stream based on the selected format + match config.sample_format() { + SampleFormat::F32 => { + self.build_f32_stream(stream_config, audio_buffer)?; + } + SampleFormat::I32 => { + self.build_i32_stream(stream_config, audio_buffer)?; + } + SampleFormat::I16 => { + self.build_i16_stream(stream_config, audio_buffer)?; + } + _ => { + return Err(anyhow::anyhow!("Unsupported sample format")); + } + } + + Ok(()) + } + + // Simple linear interpolation resampling (not high quality, but functional) + fn resample(&self, input: Vec, input_rate: u32, output_rate: u32, channels: u16) -> Vec { + let ratio = input_rate as f64 / output_rate as f64; + let input_frames = input.len() / channels as usize; + let output_frames = (input_frames as f64 / ratio) as usize; + let mut output = Vec::with_capacity(output_frames * channels as usize); + + for frame in 0..output_frames { + let input_frame = (frame as f64 * ratio) as usize; + + if input_frame < input_frames { + for ch in 0..channels { + let input_idx = input_frame * channels as usize + ch as usize; + if input_idx < input.len() { + output.push(input[input_idx]); + } else { + output.push(0.0); + } + } + } else { + for _ in 0..channels { + output.push(0.0); + } + } + } + + output + } + + fn build_f32_stream(&mut self, config: StreamConfig, audio_buffer: Arc>) -> Result<()> { + let stream = self.device.build_output_stream( + &config, + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let mut buffer = audio_buffer.lock().unwrap(); + + // If paused, just output silence + if buffer.paused { + data.fill(0.0); + return; + } + + let samples_to_copy = data.len().min(buffer.samples.len() - buffer.position); + + if samples_to_copy > 0 { + let end_pos = buffer.position + samples_to_copy; + + // Apply volume + if buffer.volume < 0.999 { + for i in 0..samples_to_copy { + data[i] = buffer.samples[buffer.position + i] * buffer.volume; + } + } else { + data[..samples_to_copy].copy_from_slice(&buffer.samples[buffer.position..end_pos]); + } + + buffer.position = end_pos; + + // Fill remaining with silence if we don't have enough samples + if samples_to_copy < data.len() { + data[samples_to_copy..].fill(0.0); + } + } else { + // No more samples, fill with silence + data.fill(0.0); + } + }, + |err| error!("ASIO stream error: {}", err), + None, + )?; + + self.stream = Some(stream); + Ok(()) + } + + fn build_i32_stream(&mut self, config: StreamConfig, audio_buffer: Arc>) -> Result<()> { + let stream = self.device.build_output_stream( + &config, + move |data: &mut [i32], _: &cpal::OutputCallbackInfo| { + let mut buffer = audio_buffer.lock().unwrap(); + + // If paused, just output silence + if buffer.paused { + data.fill(0); + return; + } + + let samples_to_copy = data.len().min(buffer.samples.len() - buffer.position); + + if samples_to_copy > 0 { + for i in 0..samples_to_copy { + data[i] = (buffer.samples[buffer.position + i] * buffer.volume * i32::MAX as f32) as i32; + } + buffer.position += samples_to_copy; + + if samples_to_copy < data.len() { + data[samples_to_copy..].fill(0); + } + } else { + data.fill(0); + } + }, + |err| error!("ASIO stream error: {}", err), + None, + )?; + + self.stream = Some(stream); + Ok(()) + } + + fn build_i16_stream(&mut self, config: StreamConfig, audio_buffer: Arc>) -> Result<()> { + let stream = self.device.build_output_stream( + &config, + move |data: &mut [i16], _: &cpal::OutputCallbackInfo| { + let mut buffer = audio_buffer.lock().unwrap(); + + // If paused, just output silence + if buffer.paused { + data.fill(0); + return; + } + + let samples_to_copy = data.len().min(buffer.samples.len() - buffer.position); + + if samples_to_copy > 0 { + for i in 0..samples_to_copy { + data[i] = (buffer.samples[buffer.position + i] * buffer.volume * i16::MAX as f32) as i16; + } + buffer.position += samples_to_copy; + + if samples_to_copy < data.len() { + data[samples_to_copy..].fill(0); + } + } else { + data.fill(0); + } + }, + |err| error!("ASIO stream error: {}", err), + None, + )?; + + self.stream = Some(stream); + Ok(()) + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..88e622a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,79 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use tracing::{info, error}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Config { + pub server_url: String, + pub user_id: Option, + pub access_token: Option, + pub username: Option, + pub device_id: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + server_url: String::new(), + user_id: None, + access_token: None, + username: None, + device_id: "jellyfin-asio-001".to_string(), + } + } +} + +impl Config { + pub fn load() -> Result { + let config_path = get_config_path()?; + + if !config_path.exists() { + info!("No config file found, creating default config"); + let default_config = Config::default(); + default_config.save()?; + return Ok(default_config); + } + + let config_str = fs::read_to_string(&config_path)?; + let config: Config = serde_json::from_str(&config_str)?; + + info!("Loaded config from {}", config_path.display()); + Ok(config) + } + + pub fn save(&self) -> Result<()> { + let config_path = get_config_path()?; + + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent)?; + } + + let config_str = serde_json::to_string_pretty(self)?; + fs::write(&config_path, config_str)?; + + info!("Saved config to {}", config_path.display()); + Ok(()) + } + + pub fn is_authenticated(&self) -> bool { + self.user_id.is_some() && self.access_token.is_some() + } + + pub fn clear_auth(&mut self) -> Result<()> { + self.user_id = None; + self.access_token = None; + self.save() + } +} + +fn get_config_path() -> Result { + let mut path = dirs::config_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?; + + path.push("jellyfin-asio-client"); + path.push("config.json"); + + Ok(path) +} \ No newline at end of file diff --git a/src/errorlog.log b/src/errorlog.log new file mode 100644 index 0000000..481b4f4 --- /dev/null +++ b/src/errorlog.log @@ -0,0 +1,55 @@ +error: struct is not supported in `trait`s or `impl`s + --> src\tui.rs:469:5 + | +469 | struct UiData { + | ^^^^^^^^^^^^^ + | + = help: consider moving the struct out to a nearby module scope + +error[E0412]: cannot find type `UiData` in this scope + --> src\tui.rs:485:34 + | +485 | fn prepare_ui_data(&self) -> UiData { + | ^^^^^^ not found in this scope + +error[E0422]: cannot find struct, variant or union type `UiData` in this scope + --> src\tui.rs:486:9 + | +486 | UiData { + | ^^^^^^ not found in this scope + +error[E0412]: cannot find type `UiData` in this scope + --> src\tui.rs:504:48 + | +504 | fn draw_ui_static(f: &mut Frame, ui_data: &UiData, player: &Player) { + | ^^^^^^ not found in this scope + +error[E0412]: cannot find type `UiData` in this scope + --> src\tui.rs:547:58 + | +547 | fn draw_artists(f: &mut Frame, area: Rect, ui_data: &UiData) { + | ^^^^^^ not found in this scope + +error[E0412]: cannot find type `UiData` in this scope + --> src\tui.rs:564:57 + | +564 | fn draw_albums(f: &mut Frame, area: Rect, ui_data: &UiData) { + | ^^^^^^ not found in this scope + +error[E0412]: cannot find type `UiData` in this scope + --> src\tui.rs:588:56 + | +588 | fn draw_songs(f: &mut Frame, area: Rect, ui_data: &UiData) { + | ^^^^^^ not found in this scope + +error[E0412]: cannot find type `UiData` in this scope + --> src\tui.rs:618:56 + | +618 | fn draw_queue(f: &mut Frame, area: Rect, ui_data: &UiData) { + | ^^^^^^ not found in this scope + +error[E0412]: cannot find type `UiData` in this scope + --> src\tui.rs:642:57 + | +642 | fn draw_search(f: &mut Frame, area: Rect, ui_data: &UiData) { + | ^^^^^^ not found in this scope \ No newline at end of file diff --git a/src/jellyfin.rs b/src/jellyfin.rs new file mode 100644 index 0000000..d71ee6e --- /dev/null +++ b/src/jellyfin.rs @@ -0,0 +1,162 @@ +use anyhow::Result; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tracing::{info, debug}; + +use crate::config::Config; + +#[derive(Debug, Clone)] +pub struct JellyfinClient { + client: Client, + config: Config, +} + +#[derive(Debug, Deserialize)] +pub struct AuthResponse { + #[serde(rename = "User")] + pub user: User, + #[serde(rename = "AccessToken")] + pub access_token: String, +} + +#[derive(Debug, Deserialize)] +pub struct User { + #[serde(rename = "Id")] + pub id: String, + #[serde(rename = "Name")] + pub name: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct LibraryItem { + #[serde(rename = "Id")] + pub id: String, + #[serde(rename = "Name")] + pub name: String, + #[serde(rename = "Type")] + pub item_type: String, + #[serde(rename = "Artists")] + pub artists: Option>, + #[serde(rename = "Album")] + pub album: Option, + #[serde(rename = "RunTimeTicks")] + pub runtime_ticks: Option, +} + +#[derive(Debug, Deserialize)] +pub struct ItemsResponse { + #[serde(rename = "Items")] + pub items: Vec, +} + +impl JellyfinClient { + pub fn new(config: Config) -> Self { + let client = Client::builder() + .user_agent("JellyfinASIOClient/0.1.0") + .build() + .expect("Failed to create HTTP client"); + + Self { + client, + config, + } + } + + pub async fn authenticate(&mut self, username: &str, password: &str) -> Result<()> { + let auth_url = format!("{}/Users/authenticatebyname", self.config.server_url); + + let mut auth_data = HashMap::new(); + auth_data.insert("Username", username); + auth_data.insert("Pw", password); + + info!("Authenticating with Jellyfin server..."); + + let response = self.client + .post(&auth_url) + .header("X-Emby-Authorization", format!(r#"MediaBrowser Client="JellyfinASIOClient", Device="Rust Client", DeviceId="{}", Version="0.1.0""#, self.config.device_id)) + .json(&auth_data) + .send() + .await?; + + if !response.status().is_success() { + let status = response.status(); + let text = response.text().await?; + return Err(anyhow::anyhow!("Authentication failed: {} - {}", status, text)); + } + + let auth_response: AuthResponse = response.json().await?; + + self.config.user_id = Some(auth_response.user.id.clone()); + self.config.access_token = Some(auth_response.access_token.clone()); + self.config.username = Some(username.to_string()); + self.config.save()?; + + info!("Authenticated as: {}", auth_response.user.name); + Ok(()) + } + + pub async fn get_music_library(&self) -> Result> { + let user_id = self.config.user_id.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?; + let token = self.config.access_token.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?; + + let url = format!("{}/Users/{}/Items", self.config.server_url, user_id); + + debug!("Fetching music library..."); + + let response = self.client + .get(&url) + .header("X-Emby-Authorization", format!(r#"MediaBrowser Client="JellyfinASIOClient", Device="Rust Client", DeviceId="{}", Version="0.1.0", Token="{}""#, self.config.device_id, token)) + .query(&[ + ("IncludeItemTypes", "Audio,MusicAlbum"), + ("Recursive", "true"), + ("Fields", "Artists,Album,RunTimeTicks"), + ("SortBy", "Artist,Album,SortName"), + ]) + .send() + .await?; + + if !response.status().is_success() { + let status = response.status(); + let text = response.text().await?; + return Err(anyhow::anyhow!("Failed to fetch library: {} - {}", status, text)); + } + + let items_response: ItemsResponse = response.json().await?; + + info!("Found {} music items", items_response.items.len()); + Ok(items_response.items) + } + + pub fn get_stream_url(&self, item_id: &str) -> Result { + let user_id = self.config.user_id.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?; + let token = self.config.access_token.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?; + + let url = format!( + "{}/Audio/{}/stream?UserId={}&api_key={}&Static=true", + self.config.server_url, item_id, user_id, token + ); + + Ok(url) + } + + pub fn is_authenticated(&self) -> bool { + self.config.is_authenticated() + } + + pub fn logout(&mut self) -> Result<()> { + self.config.clear_auth() + } +} + +impl LibraryItem { + pub fn artist_name(&self) -> Option { + self.artists.as_ref() + .and_then(|a| a.first()) + .map(|s| s.to_string()) + } + + pub fn album_name(&self) -> Option { + self.album.clone() + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3425ce2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,160 @@ +use anyhow::Result; +use tracing::info; +use cpal::traits::{HostTrait, DeviceTrait}; +use std::io::Write; + +mod jellyfin; +mod audio; +mod config; +mod player; +mod tui; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + info!("Starting Jellyfin ASIO Client"); + + // Load config + let mut config = config::Config::load()?; + + // Create Jellyfin client + let mut jellyfin = jellyfin::JellyfinClient::new(config.clone()); + + // Check if we need to authenticate + if !jellyfin.is_authenticated() { + println!("No saved session found. Please log in."); + + // Get server URL if not set + if config.server_url.is_empty() { + print!("Enter Jellyfin server URL (e.g., https://jellyfin.example.com): "); + std::io::stdout().flush()?; + let mut url = String::new(); + std::io::stdin().read_line(&mut url)?; + config.server_url = url.trim().to_string(); + config.save()?; + jellyfin = jellyfin::JellyfinClient::new(config.clone()); + } + + print!("Enter your Jellyfin username: "); + std::io::stdout().flush()?; + let mut username = String::new(); + std::io::stdin().read_line(&mut username)?; + username = username.trim().to_string(); + + print!("Enter your Jellyfin password: "); + std::io::stdout().flush()?; + let password = rpassword::read_password()?; + + jellyfin.authenticate(&username, &password).await? + } else { + info!("Using saved session for {}", config.username.unwrap_or_else(|| "Unknown user".to_string())); + } + + // List ASIO devices + list_asio_devices()?; + + // Create player + let player = player::Player::new(jellyfin.clone())?; + + // Create and run TUI + let mut tui = tui::Tui::new(jellyfin, player)?; + tui.run().await?; + + Ok(()) +} + +async fn test_jellyfin_connection() -> Result<()> { + info!("Testing Jellyfin connection..."); + + let config = config::Config { + server_url: "https://jfin.mercurio.moe".to_string(), + ..config::Config::default() + }; + let mut client = jellyfin::JellyfinClient::new(config); + + println!("Enter your Jellyfin username:"); + let mut username = String::new(); + std::io::stdin().read_line(&mut username)?; + username = username.trim().to_string(); + + println!("Enter your Jellyfin password:"); + let password = rpassword::read_password()?; + + client.authenticate(&username, &password).await?; + + let items = client.get_music_library().await?; + + info!("Your music library (first few items):"); + for item in items.iter().take(10) { + let artist = item.artists.as_ref() + .and_then(|a| a.first()) + .map(|s| s.as_str()) + .unwrap_or("Unknown Artist"); + let album = item.album.as_ref() + .map(|s| s.as_str()) + .unwrap_or("Unknown Album"); + + info!(" {} - {} - {}", artist, album, item.name); + + if items.iter().position(|i| i.id == item.id) == Some(0) { + let stream_url = client.get_stream_url(&item.id)?; + info!(" Stream URL: {}", stream_url); + + println!("\nDo you want to play this track? (y/n):"); + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + + if input.trim().to_lowercase() == "y" { + let mut player = audio::AsioPlayer::new()?; + + println!("Do you want to set a specific sample rate? (y/n):"); + let mut rate_input = String::new(); + std::io::stdin().read_line(&mut rate_input)?; + + if rate_input.trim().to_lowercase() == "y" { + println!("Enter sample rate (e.g., 44100, 48000, 96000, 192000):"); + let mut rate_str = String::new(); + std::io::stdin().read_line(&mut rate_str)?; + + if let Ok(sample_rate) = rate_str.trim().parse::() { + match player.set_sample_rate(sample_rate) { + Ok(_) => println!("Sample rate set to {} Hz", sample_rate), + Err(e) => println!("Failed to set sample rate: {}", e), + } + } else { + println!("Invalid sample rate, using automatic selection"); + } + } + + player.play_url(&stream_url).await?; + } + } + } + + Ok(()) +} + +fn list_asio_devices() -> Result<()> { + info!("Enumerating ASIO devices..."); + + let host = cpal::host_from_id(cpal::HostId::Asio)?; + + let devices = host.output_devices()?; + + for (i, device) in devices.enumerate() { + let name = device.name()?; + info!("ASIO Device {}: {}", i, name); + + let supported_configs = device.supported_output_configs()?; + for config in supported_configs { + info!(" Channels: {}, Sample Rate: {}-{} Hz, Format: {:?}", + config.channels(), + config.min_sample_rate().0, + config.max_sample_rate().0, + config.sample_format()); + } + } + + Ok(()) +} \ No newline at end of file diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..336fe7a --- /dev/null +++ b/src/player.rs @@ -0,0 +1,275 @@ +use anyhow::Result; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; +use tracing::{info, debug, error}; + +use crate::audio::AsioPlayer; +use crate::jellyfin::{JellyfinClient, LibraryItem}; + +#[derive(Debug, Clone, PartialEq)] +pub enum PlaybackState { + Stopped, + Playing, + Paused, +} + +#[derive(Debug, Clone)] +pub enum PlayerCommand { + Play(String), + PlayPause, + Stop, + Next, + Previous, + Seek(f32), + SetVolume(f32), +} + +#[derive(Debug, Clone)] +enum AudioCommand { + Play(String), + Pause, + Resume, + Stop, + Seek(f32), + SetVolume(f32), +} + +pub struct PlayerState { + pub current_item: Option, + pub queue: Vec, + pub queue_position: usize, + pub playback_state: PlaybackState, + pub volume: f32, + pub position: f32, // 0.0 - 1.0 +} + +impl Default for PlayerState { + fn default() -> Self { + Self { + current_item: None, + queue: Vec::new(), + queue_position: 0, + playback_state: PlaybackState::Stopped, + volume: 1.0, + position: 0.0, + } + } +} + +pub struct Player { + state: Arc>, + command_tx: mpsc::Sender, + audio_tx: mpsc::Sender, + jellyfin: Arc>, +} + +impl Player { + pub fn new(jellyfin: JellyfinClient) -> Result { + let (command_tx, mut command_rx) = mpsc::channel::(32); + let (audio_tx, mut audio_rx) = mpsc::channel::(32); + let state = Arc::new(Mutex::new(PlayerState::default())); + let jellyfin = Arc::new(Mutex::new(jellyfin)); + + let player_state = state.clone(); + let player_jellyfin = jellyfin.clone(); + let command_tx_clone = command_tx.clone(); + let audio_tx_clone = audio_tx.clone(); + + // Spawn audio handler in a blocking thread + let audio_state = state.clone(); + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let mut asio_player = match AsioPlayer::new() { + Ok(player) => Some(player), + Err(e) => { + error!("Failed to initialize ASIO player: {}", e); + None + } + }; + + while let Some(cmd) = audio_rx.recv().await { + if let Some(ref mut player) = asio_player { + match cmd { + AudioCommand::Play(url) => { + { + let mut state = audio_state.lock().unwrap(); + state.playback_state = PlaybackState::Playing; + } + + match player.play_url(&url).await { + Ok(_) => { + let mut state = audio_state.lock().unwrap(); + state.playback_state = PlaybackState::Stopped; + state.position = 0.0; + }, + Err(e) => error!("Playback error: {}", e) + } + }, + AudioCommand::Pause => { + if player.pause().is_ok() { + let mut state = audio_state.lock().unwrap(); + state.playback_state = PlaybackState::Paused; + } + }, + AudioCommand::Resume => { + if player.resume().is_ok() { + let mut state = audio_state.lock().unwrap(); + state.playback_state = PlaybackState::Playing; + } + }, + AudioCommand::Stop => { + if player.stop().is_ok() { + let mut state = audio_state.lock().unwrap(); + state.playback_state = PlaybackState::Stopped; + state.position = 0.0; + } + }, + AudioCommand::Seek(position) => { + if player.seek(position).is_ok() { + let mut state = audio_state.lock().unwrap(); + state.position = position; + } + }, + AudioCommand::SetVolume(volume) => { + if player.set_volume(volume).is_ok() { + let mut state = audio_state.lock().unwrap(); + state.volume = volume; + } + } + } + } + } + }); + }); + + // Spawn command handler task + tokio::spawn(async move { + while let Some(cmd) = command_rx.recv().await { + match cmd { + PlayerCommand::Play(item_id) => { + // Get the URL + let url = { + let jellyfin_guard = player_jellyfin.lock().unwrap(); + jellyfin_guard.get_stream_url(&item_id) + }; + + match url { + Ok(url) => { + let _ = audio_tx_clone.send(AudioCommand::Play(url)).await; + }, + Err(e) => error!("Failed to get stream URL: {}", e) + } + }, + PlayerCommand::PlayPause => { + // Get the current state + let (current_state, item_id) = { + let state = player_state.lock().unwrap(); + (state.playback_state.clone(), state.current_item.as_ref().map(|item| item.id.clone())) + }; + + match current_state { + PlaybackState::Playing => { + let _ = audio_tx_clone.send(AudioCommand::Pause).await; + }, + PlaybackState::Paused => { + let _ = audio_tx_clone.send(AudioCommand::Resume).await; + }, + PlaybackState::Stopped => { + if let Some(item_id) = item_id { + let _ = command_tx_clone.send(PlayerCommand::Play(item_id)).await; + } + } + } + }, + PlayerCommand::Stop => { + let _ = audio_tx_clone.send(AudioCommand::Stop).await; + }, + PlayerCommand::Next => { + let next_item_id = { + let mut state = player_state.lock().unwrap(); + if !state.queue.is_empty() && state.queue_position < state.queue.len() - 1 { + state.queue_position += 1; + Some(state.queue[state.queue_position].id.clone()) + } else { + None + } + }; + + if let Some(item_id) = next_item_id { + let _ = command_tx_clone.send(PlayerCommand::Play(item_id)).await; + } + }, + PlayerCommand::Previous => { + let prev_item_id = { + let mut state = player_state.lock().unwrap(); + if !state.queue.is_empty() && state.queue_position > 0 { + state.queue_position -= 1; + Some(state.queue[state.queue_position].id.clone()) + } else { + None + } + }; + + if let Some(item_id) = prev_item_id { + let _ = command_tx_clone.send(PlayerCommand::Play(item_id)).await; + } + }, + PlayerCommand::Seek(position) => { + let _ = audio_tx_clone.send(AudioCommand::Seek(position)).await; + }, + PlayerCommand::SetVolume(volume) => { + let _ = audio_tx_clone.send(AudioCommand::SetVolume(volume)).await; + } + } + } + }); + + Ok(Self { + state, + command_tx, + audio_tx, + jellyfin, + }) + } + + pub async fn send_command(&self, command: PlayerCommand) -> Result<()> { + self.command_tx.send(command).await + .map_err(|e| anyhow::anyhow!("Failed to send player command: {}", e)) + } + + pub fn get_state(&self) -> Arc> { + self.state.clone() + } + + pub fn set_queue(&self, items: Vec) -> Result<()> { + let mut state = self.state.lock().unwrap(); + state.queue = items; + state.queue_position = 0; + Ok(()) + } + + pub fn get_jellyfin(&self) -> Arc> { + self.jellyfin.clone() + } + + pub fn get_volume(&self) -> f32 { + let state = self.state.lock().unwrap(); + state.volume + } + + pub fn get_position(&self) -> f32 { + let state = self.state.lock().unwrap(); + state.position + } + + pub fn get_current_item(&self) -> Option { + let state = self.state.lock().unwrap(); + state.current_item.clone() + } + + pub fn get_playback_state(&self) -> PlaybackState { + let state = self.state.lock().unwrap(); + state.playback_state.clone() + } +} \ No newline at end of file diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..a4e27e3 --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,903 @@ +use anyhow::Result; +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; +use crossterm::ExecutableCommand; +use ratatui::backend::{Backend, CrosstermBackend}; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::{Line, Span, Text}; +use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Tabs}; +use ratatui::{Frame, Terminal}; +use std::io::{self, Stdout}; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::time::{Duration, Instant}; +use std::thread; +use tracing::{debug, error, info}; + +use crate::jellyfin::{JellyfinClient, LibraryItem}; +use crate::player::{Player, PlayerCommand, PlaybackState}; + +const TAB_ARTISTS: usize = 0; +const TAB_ALBUMS: usize = 1; +const TAB_SONGS: usize = 2; +const TAB_QUEUE: usize = 3; +const TAB_SEARCH: usize = 4; + +enum UiEvent { + Tick, + Input(KeyEvent), + Quit, +} + +struct UiData { + active_tab: usize, + show_help: bool, + artists: Vec, + artists_state: ListState, + albums: Vec, + albums_state: ListState, + songs: Vec, + songs_state: ListState, + queue: Vec, + queue_state: ListState, + search_query: String, + search_results: Vec, + search_state: ListState, + current_artist_filter: Option, + current_album_filter: Option, +} + +pub struct Tui { + terminal: Terminal>, + events: Receiver, + _event_sender: Sender, + jellyfin: JellyfinClient, + player: Player, + + active_tab: usize, + artists: Vec, + artists_state: ListState, + all_albums: Vec, + albums: Vec, + albums_state: ListState, + all_songs: Vec, + songs: Vec, + songs_state: ListState, + queue: Vec, + queue_state: ListState, + search_query: String, + search_results: Vec, + search_state: ListState, + show_help: bool, + current_artist_filter: Option, + current_album_filter: Option, + last_key_time: Option, + last_key_code: Option, +} + +impl Tui { + pub fn new(jellyfin: JellyfinClient, player: Player) -> Result { + enable_raw_mode()?; + io::stdout().execute(EnterAlternateScreen)?; + let backend = CrosstermBackend::new(io::stdout()); + let terminal = Terminal::new(backend)?; + + let tick_rate = Duration::from_millis(250); + let (tx, rx) = channel(); + let event_tx = tx.clone(); + + thread::spawn(move || { + let tick_rate = tick_rate; + loop { + if event::poll(tick_rate).unwrap() { + if let Event::Key(key) = event::read().unwrap() { + if tx.send(UiEvent::Input(key)).is_err() { + break; + } + } + } else { + if tx.send(UiEvent::Tick).is_err() { + break; + } + } + } + }); + + Ok(Self { + terminal, + events: rx, + _event_sender: event_tx, + jellyfin, + player, + active_tab: 0, + artists: Vec::new(), + artists_state: ListState::default(), + all_albums: Vec::new(), + albums: Vec::new(), + albums_state: ListState::default(), + all_songs: Vec::new(), + songs: Vec::new(), + songs_state: ListState::default(), + queue: Vec::new(), + queue_state: ListState::default(), + search_query: String::new(), + search_results: Vec::new(), + search_state: ListState::default(), + show_help: false, + current_artist_filter: None, + current_album_filter: None, + last_key_time: None, + last_key_code: None, + }) + } + + pub async fn run(&mut self) -> Result<()> { + self.load_library_data().await?; + + if !self.artists.is_empty() { + self.artists_state.select(Some(0)); + } + if !self.albums.is_empty() { + self.albums_state.select(Some(0)); + } + if !self.songs.is_empty() { + self.songs_state.select(Some(0)); + } + self.clear_filters(); + + let debounce_duration = Duration::from_millis(150); + loop { + let ui_data = self.prepare_ui_data(); + self.terminal.draw(|f| { + Self::draw_ui_static(f, &ui_data, &self.player); + })?; + match self.events.recv()? { + UiEvent::Input(event) => { + let now = Instant::now(); + let is_repeat = self.last_key_code == Some(event.code) + && self.last_key_time.map_or(false, |t| now.duration_since(t) < debounce_duration); + if !is_repeat { + self.last_key_time = Some(now); + self.last_key_code = Some(event.code); + if self.handle_input(event).await? { + break; + } + } + } + UiEvent::Tick => { + // Just tick, no action needed + } + UiEvent::Quit => break, + } + } + disable_raw_mode()?; + io::stdout().execute(LeaveAlternateScreen)?; + + Ok(()) + } + + async fn load_library_data(&mut self) -> Result<()> { + let items = self.jellyfin.get_music_library().await?; + + let mut artist_set = std::collections::HashSet::new(); + for item in &items { + if let Some(artist) = item.artist_name() { + artist_set.insert(artist.clone()); + } + } + self.artists = artist_set.into_iter().collect(); + self.artists.sort(); + + self.all_albums = items.iter() + .filter(|item| item.item_type == "MusicAlbum") + .cloned() + .collect(); + self.all_albums.sort_by(|a, b| a.name.cmp(&b.name)); + + self.all_songs = items.iter() + .filter(|item| item.item_type == "Audio") + .cloned() + .collect(); + self.all_songs.sort_by(|a, b| a.name.cmp(&b.name)); + + // Initially show all albums and songs + self.albums = self.all_albums.clone(); + self.songs = self.all_songs.clone(); + + Ok(()) + } + + fn filter_albums_by_artist(&mut self, artist: &str) { + self.albums = self.all_albums.iter() + .filter(|album| { + album.artist_name().map_or(false, |a| a == artist) + }) + .cloned() + .collect(); + + // Reset album selection + self.albums_state.select(if self.albums.is_empty() { None } else { Some(0) }); + + // Clear album filter and update songs + self.current_album_filter = None; + self.filter_songs(); + } + + fn filter_songs_by_album(&mut self, album: &LibraryItem) { + self.current_album_filter = Some(album.name.clone()); + self.filter_songs(); + } + + fn filter_songs(&mut self) { + self.songs = self.all_songs.iter() + .filter(|song| { + // Filter by artist if set + if let Some(ref artist_filter) = self.current_artist_filter { + if song.artist_name().map_or(true, |a| a != *artist_filter) { + return false; + } + } + + // Filter by album if set + if let Some(ref album_filter) = self.current_album_filter { + if song.album_name().map_or(true, |a| a != *album_filter) { + return false; + } + } + + true + }) + .cloned() + .collect(); + + // Reset song selection + self.songs_state.select(if self.songs.is_empty() { None } else { Some(0) }); + } + + fn clear_filters(&mut self) { + self.current_artist_filter = None; + self.current_album_filter = None; + self.albums = self.all_albums.clone(); + self.songs = self.all_songs.clone(); + + // Reset selections + self.albums_state.select(if self.albums.is_empty() { None } else { Some(0) }); + self.songs_state.select(if self.songs.is_empty() { None } else { Some(0) }); + } + + async fn handle_input(&mut self, key: KeyEvent) -> Result { + // Global shortcuts + match (key.code, key.modifiers) { + (KeyCode::Char('q'), _) => return Ok(true), + (KeyCode::Char('?'), _) => { + self.show_help = !self.show_help; + return Ok(false); + }, + (KeyCode::Char('c'), KeyModifiers::CONTROL) => return Ok(true), + (KeyCode::Tab, _) => { + self.next_tab(); + return Ok(false); + }, + (KeyCode::BackTab, _) => { + self.prev_tab(); + return Ok(false); + }, + (KeyCode::Char('1'), _) => { + self.active_tab = TAB_ARTISTS; + return Ok(false); + }, + (KeyCode::Char('2'), _) => { + self.active_tab = TAB_ALBUMS; + return Ok(false); + }, + (KeyCode::Char('3'), _) => { + self.active_tab = TAB_SONGS; + return Ok(false); + } + (KeyCode::Char('4'), _) => { + self.active_tab = TAB_QUEUE; + return Ok(false); + }, + (KeyCode::Char('5'), _) => { + self.active_tab = TAB_SEARCH; + return Ok(false); + }, + (KeyCode::Char('r'), _) => { + // Reset filters + self.clear_filters(); + return Ok(false); + }, + _ => {} + } + + // Player controls (only if not showing help and not in search mode) + if !self.show_help && self.active_tab != TAB_SEARCH { + match key.code { + KeyCode::Char(' ') => { + self.player.send_command(PlayerCommand::PlayPause).await?; + return Ok(false); + }, + KeyCode::Char('s') => { + self.player.send_command(PlayerCommand::Stop).await?; + return Ok(false); + }, + KeyCode::Char('n') => { + self.player.send_command(PlayerCommand::Next).await?; + return Ok(false); + }, + KeyCode::Char('p') => { + self.player.send_command(PlayerCommand::Previous).await?; + return Ok(false); + }, + KeyCode::Char('+') | KeyCode::Char('=') => { + let vol = (self.player.get_volume() + 0.05).min(1.0); + self.player.send_command(PlayerCommand::SetVolume(vol)).await?; + return Ok(false); + }, + KeyCode::Char('-') => { + let vol = (self.player.get_volume() - 0.05).max(0.0); + self.player.send_command(PlayerCommand::SetVolume(vol)).await?; + return Ok(false); + }, + KeyCode::Left => { + let pos = (self.player.get_position() - 0.05).max(0.0); + self.player.send_command(PlayerCommand::Seek(pos)).await?; + return Ok(false); + }, + KeyCode::Right => { + let pos = (self.player.get_position() + 0.05).min(1.0); + self.player.send_command(PlayerCommand::Seek(pos)).await?; + return Ok(false); + }, + _ => {} + } + } + + // Tab-specific input handling + if !self.show_help { + match self.active_tab { + TAB_ARTISTS => self.handle_artists_input(key).await?, + TAB_ALBUMS => self.handle_albums_input(key).await?, + TAB_SONGS => self.handle_songs_input(key).await?, + TAB_QUEUE => self.handle_queue_input(key).await?, + TAB_SEARCH => self.handle_search_input(key).await?, + _ => {} + } + } + + Ok(false) + } + + async fn handle_artists_input(&mut self, key: KeyEvent) -> Result<()> { + match key.code { + KeyCode::Up => { + if let Some(selected) = self.artists_state.selected() { + if selected > 0 { + self.artists_state.select(Some(selected - 1)); + } + } + } + KeyCode::Down => { + if let Some(selected) = self.artists_state.selected() { + if selected < self.artists.len().saturating_sub(1) { + self.artists_state.select(Some(selected + 1)); + } + } + } + KeyCode::Enter => { + if let Some(selected) = self.artists_state.selected() { + if selected < self.artists.len() { + let artist = self.artists[selected].clone(); + self.current_artist_filter = Some(artist.clone()); + self.filter_albums_by_artist(&artist); + self.active_tab = TAB_ALBUMS; + } + } + } + _ => {} + } + Ok(()) + } + + async fn handle_albums_input(&mut self, key: KeyEvent) -> Result<()> { + match key.code { + KeyCode::Up => { + if let Some(selected) = self.albums_state.selected() { + if selected > 0 { + self.albums_state.select(Some(selected - 1)); + } + } + } + KeyCode::Down => { + if let Some(selected) = self.albums_state.selected() { + if selected < self.albums.len().saturating_sub(1) { + self.albums_state.select(Some(selected + 1)); + } + } + } + KeyCode::Enter => { + if let Some(selected) = self.albums_state.selected() { + if selected < self.albums.len() { + let album = self.albums[selected].clone(); + self.filter_songs_by_album(&album); + self.active_tab = TAB_SONGS; + } + } + } + _ => {} + } + Ok(()) + } + + async fn handle_songs_input(&mut self, key: KeyEvent) -> Result<()> { + match key.code { + KeyCode::Up => { + if let Some(selected) = self.songs_state.selected() { + if selected > 0 { + self.songs_state.select(Some(selected - 1)); + } + } + } + KeyCode::Down => { + if let Some(selected) = self.songs_state.selected() { + if selected < self.songs.len().saturating_sub(1) { + self.songs_state.select(Some(selected + 1)); + } + } + } + KeyCode::Enter => { + if let Some(selected) = self.songs_state.selected() { + if selected < self.songs.len() { + let song = &self.songs[selected]; + let url = self.jellyfin.get_stream_url(&song.id)?; + self.player.send_command(PlayerCommand::Play(url)).await?; + + if !self.queue.iter().any(|item| item.id == song.id) { + self.queue.push(song.clone()); + } + } + } + } + KeyCode::Char('a') => { + if let Some(selected) = self.songs_state.selected() { + if selected < self.songs.len() { + let song = &self.songs[selected]; + if !self.queue.iter().any(|item| item.id == song.id) { + self.queue.push(song.clone()); + } + } + } + } + _ => {} + } + Ok(()) + } + + async fn handle_queue_input(&mut self, key: KeyEvent) -> Result<()> { + match key.code { + KeyCode::Up => { + if let Some(selected) = self.queue_state.selected() { + if selected > 0 { + self.queue_state.select(Some(selected - 1)); + } + } + } + KeyCode::Down => { + if let Some(selected) = self.queue_state.selected() { + if selected < self.queue.len().saturating_sub(1) { + self.queue_state.select(Some(selected + 1)); + } + } + } + KeyCode::Enter => { + if let Some(selected) = self.queue_state.selected() { + if selected < self.queue.len() { + let song = &self.queue[selected]; + let url = self.jellyfin.get_stream_url(&song.id)?; + self.player.send_command(PlayerCommand::Play(url)).await?; + } + } + } + KeyCode::Delete | KeyCode::Char('d') => { + if let Some(selected) = self.queue_state.selected() { + if selected < self.queue.len() { + self.queue.remove(selected); + if self.queue.is_empty() { + self.queue_state.select(None); + } else if selected >= self.queue.len() { + self.queue_state.select(Some(self.queue.len() - 1)); + } + } + } + } + KeyCode::Char('c') => { + self.queue.clear(); + self.queue_state.select(None); + } + _ => {} + } + Ok(()) + } + + async fn handle_search_input(&mut self, key: KeyEvent) -> Result<()> { + match key.code { + KeyCode::Char(c) => { + self.search_query.push(c); + self.perform_search().await?; + } + KeyCode::Backspace => { + self.search_query.pop(); + self.perform_search().await?; + } + KeyCode::Up => { + if let Some(selected) = self.search_state.selected() { + if selected > 0 { + self.search_state.select(Some(selected - 1)); + } + } + } + KeyCode::Down => { + if let Some(selected) = self.search_state.selected() { + if selected < self.search_results.len().saturating_sub(1) { + self.search_state.select(Some(selected + 1)); + } + } + } + KeyCode::Enter => { + if let Some(selected) = self.search_state.selected() { + if selected < self.search_results.len() { + let song = &self.search_results[selected]; + if song.item_type == "Audio" { + let url = self.jellyfin.get_stream_url(&song.id)?; + self.player.send_command(PlayerCommand::Play(url)).await?; + + if !self.queue.iter().any(|item| item.id == song.id) { + self.queue.push(song.clone()); + } + } + } + } + } + _ => {} + } + Ok(()) + } + + async fn perform_search(&mut self) -> Result<()> { + if self.search_query.is_empty() { + self.search_results.clear(); + self.search_state.select(None); + return Ok(()); + } + + let query = self.search_query.to_lowercase(); + let mut results = Vec::new(); + + // Search in all songs + for item in &self.all_songs { + if item.name.to_lowercase().contains(&query) || + item.artist_name().map_or(false, |a| a.to_lowercase().contains(&query)) || + item.album_name().map_or(false, |a| a.to_lowercase().contains(&query)) { + results.push(item.clone()); + } + } + + // Search in all albums + for item in &self.all_albums { + if item.name.to_lowercase().contains(&query) || + item.artist_name().map_or(false, |a| a.to_lowercase().contains(&query)) { + results.push(item.clone()); + } + } + + self.search_results = results; + + if !self.search_results.is_empty() { + self.search_state.select(Some(0)); + } else { + self.search_state.select(None); + } + + Ok(()) + } + + fn next_tab(&mut self) { + self.active_tab = (self.active_tab + 1) % 5; + } + + fn prev_tab(&mut self) { + self.active_tab = (self.active_tab + 4) % 5; + } + + fn prepare_ui_data(&self) -> UiData { + UiData { + active_tab: self.active_tab, + show_help: self.show_help, + artists: self.artists.clone(), + artists_state: self.artists_state.clone(), + albums: self.albums.clone(), + albums_state: self.albums_state.clone(), + songs: self.songs.clone(), + songs_state: self.songs_state.clone(), + queue: self.queue.clone(), + queue_state: self.queue_state.clone(), + search_query: self.search_query.clone(), + search_results: self.search_results.clone(), + search_state: self.search_state.clone(), + current_artist_filter: self.current_artist_filter.clone(), + current_album_filter: self.current_album_filter.clone(), + } + } + + fn draw_ui_static(f: &mut Frame, ui_data: &UiData, player: &Player) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), // Tabs + Constraint::Min(0), // Content + Constraint::Length(3), // Status bar + ]) + .split(f.size()); + + // Draw tabs + let tab_titles: Vec = vec!["Artists", "Albums", "Songs", "Queue", "Search"] + .iter() + .map(|t| Span::styled(*t, Style::default().fg(Color::White))) + .collect(); + + let mut title = "Jellyfin ASIO Client".to_string(); + if let Some(ref artist) = ui_data.current_artist_filter { + title.push_str(&format!(" - Artist: {}", artist)); + } + if let Some(ref album) = ui_data.current_album_filter { + title.push_str(&format!(" - Album: {}", album)); + } + + let tabs = Tabs::new(tab_titles) + .block(Block::default().borders(Borders::ALL).title(title)) + .highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) + .select(ui_data.active_tab); + + f.render_widget(tabs, chunks[0]); + + if ui_data.show_help { + Self::draw_help(f, chunks[1]); + } else { + match ui_data.active_tab { + TAB_ARTISTS => Self::draw_artists(f, chunks[1], ui_data), + TAB_ALBUMS => Self::draw_albums(f, chunks[1], ui_data), + TAB_SONGS => Self::draw_songs(f, chunks[1], ui_data), + TAB_QUEUE => Self::draw_queue(f, chunks[1], ui_data), + TAB_SEARCH => Self::draw_search(f, chunks[1], ui_data), + _ => {} + } + } + + Self::draw_status_bar(f, chunks[2], player); + } + + fn draw_artists(f: &mut Frame, area: Rect, ui_data: &UiData) { + let items: Vec = ui_data.artists + .iter() + .map(|artist| { + ListItem::new(Line::from(vec![Span::raw(artist.clone())])) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("Artists")) + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)); + + let mut state = ui_data.artists_state.clone(); + f.render_stateful_widget(list, area, &mut state); + } + + fn draw_albums(f: &mut Frame, area: Rect, ui_data: &UiData) { + let items: Vec = ui_data.albums + .iter() + .map(|album| { + let artist = album.artist_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| "Unknown Artist".to_string()); + ListItem::new(Line::from(vec![ + Span::raw(album.name.clone()), + Span::raw(" - ".to_string()), + Span::raw(artist), + ])) + }) + .collect(); + + let mut title = "Albums".to_string(); + if let Some(ref artist) = ui_data.current_artist_filter { + title.push_str(&format!(" ({})", artist)); + } + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title(title)) + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)); + + let mut state = ui_data.albums_state.clone(); + f.render_stateful_widget(list, area, &mut state); + } + + fn draw_songs(f: &mut Frame, area: Rect, ui_data: &UiData) { + let items: Vec = ui_data.songs + .iter() + .map(|song| { + let artist = song.artist_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| "Unknown Artist".to_string()); + let album = song.album_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| "Unknown Album".to_string()); + ListItem::new(Line::from(vec![ + Span::raw(song.name.clone()), + Span::raw(" - ".to_string()), + Span::raw(artist), + Span::raw(" (Album: ".to_string()), + Span::raw(album), + Span::raw(")".to_string()), + ])) + }) + .collect(); + + let mut title = "Songs".to_string(); + if let Some(ref album) = ui_data.current_album_filter { + title.push_str(&format!(" ({})", album)); + } else if let Some(ref artist) = ui_data.current_artist_filter { + title.push_str(&format!(" ({})", artist)); + } + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title(title)) + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)); + + let mut state = ui_data.songs_state.clone(); + f.render_stateful_widget(list, area, &mut state); + } + + fn draw_queue(f: &mut Frame, area: Rect, ui_data: &UiData) { + let items: Vec = ui_data.queue + .iter() + .map(|song| { + let artist = song.artist_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| "Unknown Artist".to_string()); + ListItem::new(Line::from(vec![ + Span::raw(song.name.clone()), + Span::raw(" - ".to_string()), + Span::raw(artist), + ])) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("Queue")) + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)); + + let mut state = ui_data.queue_state.clone(); + f.render_stateful_widget(list, area, &mut state); + } + + fn draw_search(f: &mut Frame, area: Rect, ui_data: &UiData) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Search input + Constraint::Min(0), // Search results + ]) + .split(area); + + // Search input + let search_text = Paragraph::new(ui_data.search_query.clone()) + .block(Block::default().borders(Borders::ALL).title("Search")); + f.render_widget(search_text, chunks[0]); + + // Search results + let items: Vec = ui_data.search_results + .iter() + .map(|item| { + // Fixed: Clone the artist string to avoid temporary value issues + let artist = item.artist_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| "Unknown Artist".to_string()); + let type_str = match item.item_type.as_str() { + "Audio" => "Song", + "MusicAlbum" => "Album", + _ => &item.item_type, + }; + + // Fixed: use Line::from instead of Span::from with Vec + ListItem::new(Line::from(vec![ + Span::raw(format!("[{}] ", type_str)), + Span::raw(item.name.clone()), + Span::raw(" - ".to_string()), + Span::raw(artist), + ])) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("Results")) + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)); + + let mut state = ui_data.search_state.clone(); + f.render_stateful_widget(list, chunks[1], &mut state); + } + + fn draw_status_bar(f: &mut Frame, area: Rect, player: &Player) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(70), // Now playing + Constraint::Percentage(30), // Controls help + ]) + .split(area); + + // Now playing info + let now_playing = match player.get_current_item() { + Some(item) => { + // Fixed: Clone the artist string to avoid temporary value issues + let artist = item.artist_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| "Unknown Artist".to_string()); + let state = match player.get_playback_state() { + PlaybackState::Playing => "▶", + PlaybackState::Paused => "⏸", + PlaybackState::Stopped => "⏹", + }; + + let position = player.get_position(); + let position_str = format!("{:.0}%", position * 100.0); + + format!("{} {} - {} ({})", state, item.name, artist, position_str) + }, + None => "Not playing".to_string(), + }; + + let playing_text = Paragraph::new(now_playing) + .block(Block::default().borders(Borders::ALL).title("Now Playing")); + f.render_widget(playing_text, chunks[0]); + + // Controls help + let controls = "Space: Play/Pause | s: Stop | n/p: Next/Prev"; + let controls_text = Paragraph::new(controls) + .block(Block::default().borders(Borders::ALL).title("Controls")); + f.render_widget(controls_text, chunks[1]); + } + + fn draw_help(f: &mut Frame, area: Rect) { + // Fixed: use Text::from_iter or convert to proper Text format + let help_text = Text::from_iter([ + "Keyboard Controls:", + "", + "Tab/Shift+Tab: Switch tabs", + "1-5: Switch to specific tab", + "Up/Down: Navigate lists", + "Enter: Select/Play item", + "", + "Space: Play/Pause", + "s: Stop playback", + "n: Next track", + "p: Previous track", + "Left/Right: Seek backward/forward", + "+/-: Adjust volume", + "", + "In Songs tab:", + " a: Add to queue", + "", + "In Queue tab:", + " d: Delete selected item", + " c: Clear queue", + "", + "?: Toggle help", + "q: Quit", + ]); + + let help = Paragraph::new(help_text) + .block(Block::default().borders(Borders::ALL).title("Help")); + + f.render_widget(help, area); + } +} \ No newline at end of file