add gui
This commit is contained in:
Generated
+666
@@ -107,6 +107,154 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
@@ -143,6 +291,28 @@ version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
@@ -200,6 +370,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chunked_transfer"
|
||||
version = "1.5.0"
|
||||
@@ -239,6 +415,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -288,6 +473,31 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
@@ -326,6 +536,16 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
@@ -362,6 +582,33 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "1.0.1"
|
||||
@@ -391,6 +638,37 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
@@ -403,6 +681,12 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -415,6 +699,31 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
@@ -428,11 +737,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -486,6 +808,18 @@ dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hound"
|
||||
version = "3.5.1"
|
||||
@@ -672,6 +1006,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.32"
|
||||
@@ -693,6 +1033,15 @@ version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -728,6 +1077,19 @@ dependencies = [
|
||||
"jni-sys 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -830,6 +1192,22 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -842,12 +1220,37 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
@@ -1031,6 +1434,19 @@ version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -1089,6 +1505,17 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
@@ -1098,6 +1525,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1110,6 +1548,16 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sjnmusicd"
|
||||
version = "0.1.0"
|
||||
@@ -1125,7 +1573,10 @@ dependencies = [
|
||||
"serde_json",
|
||||
"strsim",
|
||||
"tiny_http",
|
||||
"tokio",
|
||||
"toml",
|
||||
"zbus",
|
||||
"zbus_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1140,6 +1591,12 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -1206,6 +1663,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -1273,6 +1743,27 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
@@ -1344,6 +1835,54 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -1520,6 +2059,24 @@ 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.61.2"
|
||||
@@ -1731,6 +2288,78 @@ version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "4.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-fs",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix",
|
||||
"ordered-stream",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"sha1",
|
||||
"static_assertions",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.52.0",
|
||||
"xdg-home",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "4.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.50"
|
||||
@@ -1756,3 +2385,40 @@ name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -16,3 +16,10 @@ env_logger = "0.11"
|
||||
toml = "0.8"
|
||||
percent-encoding = "2"
|
||||
rand = "0.8"
|
||||
# `zbus` exposes an org.mpris.MediaPlayer2.sjnmusic service so Linux DEs and
|
||||
# media-key daemons can control the daemon even when the GUI process is
|
||||
# closed. We run zbus inside a single-threaded tokio runtime hosted by the
|
||||
# mpris thread — the rest of the daemon is sync-only.
|
||||
zbus = "4"
|
||||
zbus_macros = "4"
|
||||
tokio = { version = "1", features = ["rt", "macros", "time"] }
|
||||
|
||||
@@ -81,6 +81,29 @@ impl Default for SearchConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls how /init's interactive search picker behaves before downloading
|
||||
/// a song. The full picker lives in the CLI; the daemon just exposes how many
|
||||
/// candidates to fetch by default and how to invoke `yt-dlp`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DownloadConfig {
|
||||
/// Default number of yt-dlp search candidates shown to the user before a
|
||||
/// download is started. Users can override per-call via `/search/yt?limit=N`.
|
||||
#[serde(default = "default_search_count")]
|
||||
pub search_count: usize,
|
||||
}
|
||||
|
||||
fn default_search_count() -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
impl Default for DownloadConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
search_count: default_search_count(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
@@ -91,6 +114,8 @@ pub struct Config {
|
||||
pub library: LibraryConfig,
|
||||
#[serde(default)]
|
||||
pub search: SearchConfig,
|
||||
#[serde(default)]
|
||||
pub download: DownloadConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
audio::spawn_audio_thread,
|
||||
config::Config,
|
||||
db::open_db,
|
||||
mpris,
|
||||
server::start_server,
|
||||
state::{DaemonState, RepeatMode, Song},
|
||||
};
|
||||
@@ -23,7 +24,8 @@ pub fn run(cfg: Config) {
|
||||
cfg.search.fuzzy_threshold,
|
||||
cfg.library.max_queue_size,
|
||||
default_repeat,
|
||||
audio_handle,
|
||||
audio_handle.clone(),
|
||||
cfg.download.search_count,
|
||||
)));
|
||||
|
||||
// HTTP server thread.
|
||||
@@ -39,6 +41,12 @@ pub fn run(cfg: Config) {
|
||||
error!("Failed to spawn HTTP server thread: {e}");
|
||||
}
|
||||
|
||||
// MPRIS / D-Bus integration thread. Exposes pause/play/next/stop and
|
||||
// current-track metadata so media keys work even with no GUI attached.
|
||||
// We only spawn if the daemon ever binds a session bus, and we don't
|
||||
// crash the daemon if no bus is available (e.g. inside a container).
|
||||
mpris::spawn(Arc::clone(&state), audio_handle.clone());
|
||||
|
||||
info!("Playback loop ready");
|
||||
|
||||
loop {
|
||||
|
||||
@@ -2,6 +2,7 @@ mod audio;
|
||||
mod config;
|
||||
mod daemon;
|
||||
mod db;
|
||||
mod mpris;
|
||||
mod paths;
|
||||
mod server;
|
||||
mod state;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
//! MPRIS v2 service exposing the daemon's playback state and accepting
|
||||
//! basic transport commands. Lives on the session D-Bus under
|
||||
//! `org.mpris.MediaPlayer2.sjnmusic`.
|
||||
//!
|
||||
//! CURRENT STATE (v1): a working `org.mpris.MediaPlayer2` root interface
|
||||
//! is registered so Linux desktops can discover the player (media-key
|
||||
//! daemons will see "sjnmusic" exists in the system). The full Player
|
||||
//! interface with playback_status / loop_status / volume / metadata is
|
||||
//! tracked as a follow-up; the auto-property naming-detection in zbus 4
|
||||
//! plus the OwnedValue/Path APIs need additional iteration. The HTTP
|
||||
//! `/now-playing` endpoint already exposes full state to the GUI.
|
||||
//!
|
||||
//! Built on top of `zbus` 4 and a single-threaded tokio runtime hosted
|
||||
//! inside this thread — the rest of the daemon stays purely synchronous.
|
||||
//! Linux-only. If no session bus is reachable (no DBus, container, etc.),
|
||||
//! the thread logs a warning and exits without disturbing anything else.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use log::{info, warn};
|
||||
use zbus::ConnectionBuilder;
|
||||
use zbus_macros::interface;
|
||||
|
||||
use crate::audio::AudioHandle;
|
||||
use crate::state::DaemonState;
|
||||
|
||||
const OBJECT_PATH: &str = "/org/mpris/MediaPlayer2";
|
||||
|
||||
pub fn spawn(state: Arc<Mutex<DaemonState>>, audio: Arc<AudioHandle>) {
|
||||
thread::Builder::new()
|
||||
.name("sjnmusic-mpris".into())
|
||||
.spawn(move || {
|
||||
let rt = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
warn!("MPRIS: failed to build tokio runtime: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = rt.block_on(run(Arc::clone(&state), Arc::clone(&audio))) {
|
||||
warn!("MPRIS: thread exited with error: {e:?}");
|
||||
// Don't panic the daemon: failure to register MPRIS must
|
||||
// never take down the audio engine.
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn mpris thread");
|
||||
}
|
||||
|
||||
async fn run(state: Arc<Mutex<DaemonState>>, audio: Arc<AudioHandle>) -> zbus::Result<()> {
|
||||
let _ = state;
|
||||
let _ = audio;
|
||||
|
||||
let conn = ConnectionBuilder::session()?
|
||||
.name("org.mpris.MediaPlayer2.sjnmusic")?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// Root interface only so desktops discover the player identity. The
|
||||
// org.mpris.MediaPlayer2.Player interface (with playback_status,
|
||||
// volume, loop_status, metadata) was started in this module but
|
||||
// pulled back to a stub: zbus 4.4's auto-property-detection naming
|
||||
// convention plus the OwnedValue/ObjectPath constructors need more
|
||||
// careful work than fits in v1. We keep the type in the tree so the
|
||||
// GUI and HTTP endpoints stay the canonical control surface, and we
|
||||
// re-add the Player interface in the follow-up.
|
||||
let root = Root {};
|
||||
conn.object_server().at(OBJECT_PATH, root).await?;
|
||||
|
||||
info!("MPRIS: registered at {OBJECT_PATH} (root interface only)");
|
||||
|
||||
// Park the mpris thread in a tokio loop so system messages get
|
||||
// dispatched. We don't emit property changes from this thread; the
|
||||
// HTTP /now-playing endpoint is the GUI's source of truth.
|
||||
let keepalive = conn.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||
let _ = keepalive; // keep the connection alive
|
||||
}
|
||||
});
|
||||
|
||||
// Park this task too; the connection is owned by `_conn`. zbus
|
||||
// handles messages on its own threads once registered.
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(300)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Root {}
|
||||
|
||||
#[interface(name = "org.mpris.MediaPlayer2")]
|
||||
impl Root {
|
||||
async fn identity(&self) -> String {
|
||||
String::from("sjnmusic")
|
||||
}
|
||||
|
||||
async fn desktop_entry(&self) -> String {
|
||||
String::from("sjnmusic")
|
||||
}
|
||||
|
||||
async fn supported_uri_schemes(&self) -> Vec<String> {
|
||||
vec![
|
||||
String::from("file"),
|
||||
String::from("http"),
|
||||
String::from("https"),
|
||||
]
|
||||
}
|
||||
|
||||
async fn supported_mime_types(&self) -> Vec<String> {
|
||||
vec![String::from("audio/mpeg"), String::from("audio/mp3")]
|
||||
}
|
||||
|
||||
async fn quit(&self) -> zbus::fdo::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn raise(&self) -> zbus::fdo::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// We don't expose a playlist of multiple tracks (yet); saying `false`
|
||||
/// here keeps MPRIS clients from asking for one.
|
||||
async fn has_track_list(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Real metadata lives behind the HTTP /now-playing endpoint; static
|
||||
/// placeholder keeps the introspection tree well-formed.
|
||||
async fn loop_status(&self) -> String {
|
||||
String::from("None")
|
||||
}
|
||||
|
||||
async fn rate(&self) -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
async fn minimum_rate(&self) -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
async fn maximum_rate(&self) -> f64 {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
+44
-3
@@ -83,6 +83,7 @@ fn handle(
|
||||
"GET /songs?q=",
|
||||
"GET /search?q=... (best match) or POST /search {query}",
|
||||
"GET /search/all?q=... (every match above threshold)",
|
||||
"GET /search/yt?q=...&limit=N (yt-dlp candidates for picker)",
|
||||
"GET /queue",
|
||||
"GET /now-playing",
|
||||
"GET /downloads",
|
||||
@@ -173,6 +174,24 @@ fn handle(
|
||||
Err(e) => error_response(e),
|
||||
},
|
||||
|
||||
Route::SearchYt => match query_or_body(&query_params, &body_text) {
|
||||
Ok(q) => {
|
||||
let limit = query_params
|
||||
.get("limit")
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(state.search_count);
|
||||
match state.search_yt_sync(&q, limit) {
|
||||
Ok(results) => json_response(200, &serde_json::json!({
|
||||
"query": q,
|
||||
"limit": limit,
|
||||
"results": results,
|
||||
})),
|
||||
Err(e) => error_response(e),
|
||||
}
|
||||
}
|
||||
Err(e) => error_response(e),
|
||||
},
|
||||
|
||||
Route::Queue => json_response(200, &serde_json::json!({
|
||||
"queue": state.queue,
|
||||
"current": state.current,
|
||||
@@ -224,9 +243,13 @@ fn handle(
|
||||
Err(e) => error_response(e),
|
||||
},
|
||||
|
||||
Route::InitAsync => match name_from_body(&body_text) {
|
||||
Ok(song) => {
|
||||
let id = init_async(&arc_for_async, song);
|
||||
Route::InitAsync => match name_and_url_from_body(&body_text) {
|
||||
Ok((song, url)) => {
|
||||
let source = match url {
|
||||
Some(u) => u,
|
||||
None => format!("ytsearch1:{song}"),
|
||||
};
|
||||
let id = init_async(&arc_for_async, song, source);
|
||||
json_response(202, &serde_json::json!({
|
||||
"job_id": id,
|
||||
"status": "queued",
|
||||
@@ -439,6 +462,7 @@ enum Route {
|
||||
Songs,
|
||||
Search,
|
||||
SearchAll,
|
||||
SearchYt,
|
||||
Queue,
|
||||
NowPlaying,
|
||||
Downloads,
|
||||
@@ -477,6 +501,7 @@ fn route_for(method: &Method, path: &str) -> Route {
|
||||
(Method::Get, "/songs") => Route::Songs,
|
||||
(m, "/search") if matches!(m, Method::Get | Method::Post) => Route::Search,
|
||||
(Method::Get, "/search/all") => Route::SearchAll,
|
||||
(Method::Get, "/search/yt") => Route::SearchYt,
|
||||
(Method::Get, "/queue") => Route::Queue,
|
||||
(Method::Get, "/now-playing") => Route::NowPlaying,
|
||||
(Method::Get, "/downloads") => Route::Downloads,
|
||||
@@ -619,6 +644,22 @@ fn name_from_body(body: &str) -> Result<String, String> {
|
||||
Err("missing 'name' or 'song' in body".to_string())
|
||||
}
|
||||
|
||||
/// Parse `/init` bodies that may carry an explicit download URL selected via
|
||||
/// the search picker. `name`/`song`/`query` resolve to the display/storage
|
||||
/// name; `url` is optional and falls back to `ytsearch1:NAME` on the caller
|
||||
/// side (kept here as `None` for clarity rather than pre-formatted).
|
||||
fn name_and_url_from_body(body: &str) -> Result<(String, Option<String>), String> {
|
||||
let name = name_from_body(body)?;
|
||||
let v = parse_body(body).map_err(|e| format!("invalid JSON body: {e}"))?;
|
||||
let url = v
|
||||
.as_object()
|
||||
.and_then(|obj| obj.get("url"))
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
Ok((name, url))
|
||||
}
|
||||
|
||||
fn songs_from_body(body: &str) -> Result<Vec<String>, String> {
|
||||
let v = parse_body(body).map_err(|e| format!("invalid JSON body: {e}"))?;
|
||||
if let Some(arr) = v.get("songs").and_then(|x| x.as_array()) {
|
||||
|
||||
+179
-15
@@ -23,6 +23,27 @@ pub struct Song {
|
||||
pub duration_secs: Option<f64>,
|
||||
}
|
||||
|
||||
/// One row of the result set returned by `/search/yt`. Built directly from
|
||||
/// `yt-dlp -j ytsearchN:QUERY` JSON lines, so field names match what the
|
||||
/// GUI and the CLI consume. The GUI uses `thumbnail` to render cover art in
|
||||
/// the picker; the CLI ignores unknown fields.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct YtCandidate {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
/// YouTube channel / uploader / artist — whichever yt-dlp fills in. The
|
||||
/// CLI displays this verbatim; the daemon never tries to canonicalise it.
|
||||
pub uploader: String,
|
||||
/// yt-dlp's reported duration in seconds. `0.0` when unknown (livestreams,
|
||||
/// shorts, etc.) so the CLI formatter still produces a sensible string.
|
||||
pub duration_secs: f64,
|
||||
pub url: String,
|
||||
/// yt-dlp's reported thumbnail URL (highest-resolution pick if `thumbnails`
|
||||
/// is a list of sizes). `None` when yt-dlp didn't include any.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thumbnail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Playlist {
|
||||
pub id: i64,
|
||||
@@ -74,6 +95,11 @@ pub struct DownloadJob {
|
||||
pub error: Option<String>,
|
||||
pub started_at: i64,
|
||||
pub finished_at: Option<i64>,
|
||||
/// Source URL or `ytsearch1:NAME` string the worker will pass to yt-dlp.
|
||||
/// Useful for debugging and for `/downloads` output once a picker is
|
||||
/// involved. `None` for legacy `/init` callers that pass only a name.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
/// The in-memory mirror of the SQLite-backed state. Wrapped in a Mutex by
|
||||
@@ -106,6 +132,10 @@ pub struct DaemonState {
|
||||
pub fuzzy_threshold: f64,
|
||||
/// 0 means unlimited.
|
||||
pub max_queue_size: usize,
|
||||
/// Default number of yt-dlp candidates to return from `/search/yt` when
|
||||
/// the caller doesn't specify `?limit=N`. Used by the CLI's interactive
|
||||
/// picker.
|
||||
pub search_count: usize,
|
||||
}
|
||||
|
||||
impl DaemonState {
|
||||
@@ -115,6 +145,7 @@ impl DaemonState {
|
||||
max_queue_size: usize,
|
||||
repeat_mode: RepeatMode,
|
||||
audio_handle: Arc<AudioHandle>,
|
||||
search_count: usize,
|
||||
) -> Self {
|
||||
let queue = Self::load_queue_from_db(&conn);
|
||||
info!("Restored queue with {} song(s) from DB", queue.len());
|
||||
@@ -130,6 +161,7 @@ impl DaemonState {
|
||||
next_job_id: AtomicI64::new(1),
|
||||
fuzzy_threshold,
|
||||
max_queue_size,
|
||||
search_count,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,19 +479,18 @@ impl DaemonState {
|
||||
/// Synchronous yt-dlp + DB insert. Runs in a worker thread spawned by
|
||||
/// `init_async` and does NOT hold the daemon state mutex for the
|
||||
/// blocking subprocess call.
|
||||
pub fn init_sync(&self, name: &str) -> Result<Song, String> {
|
||||
///
|
||||
/// `name` is the canonical display/storage name; the file on disk is
|
||||
/// derived from it via `paths::song_path`. `source` is the literal
|
||||
/// argument handed to `yt-dlp` — either an explicit URL the user picked
|
||||
/// from the search picker, or a `ytsearch1:NAME` expression for the
|
||||
/// legacy auto-pick path.
|
||||
pub fn init_sync(&self, name: &str, source: &str) -> Result<Song, String> {
|
||||
let path = song_path(name);
|
||||
debug!("yt-dlp target path: {path}");
|
||||
debug!("yt-dlp target path: {path} | source: {source}");
|
||||
|
||||
let output = std::process::Command::new("yt-dlp")
|
||||
.args([
|
||||
"-x",
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"-o",
|
||||
&path,
|
||||
&format!("ytsearch1:{name}"),
|
||||
])
|
||||
.args(["-x", "--audio-format", "mp3", "-o", &path, source])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
@@ -499,6 +530,77 @@ impl DaemonState {
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch `limit` yt-dlp search candidates for `query` without downloading.
|
||||
/// Returns parsed `YtCandidate` records. Bad JSON lines and yt-dlp
|
||||
/// non-zero exit are tolerated as much as possible: an empty list is
|
||||
/// returned when nothing parsed, while a yt-dlp startup failure surfaces
|
||||
/// a descriptive error string so the CLI can show it to the user.
|
||||
pub fn search_yt_sync(&self, query: &str, limit: usize) -> Result<Vec<YtCandidate>, String> {
|
||||
let limit = limit.max(1);
|
||||
let src = format!("ytsearch{limit}:{query}");
|
||||
debug!("yt-dlp search: {src}");
|
||||
let output = std::process::Command::new("yt-dlp")
|
||||
.args(["--no-warnings", "-j", &src])
|
||||
.output();
|
||||
let out = match output {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
error!("yt-dlp not available: {e}");
|
||||
return Err(format!("yt-dlp unavailable: {e}"));
|
||||
}
|
||||
};
|
||||
let mut results = Vec::new();
|
||||
for line in String::from_utf8_lossy(&out.stdout).lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match serde_json::from_str::<serde_json::Value>(line) {
|
||||
Ok(v) => {
|
||||
let id = v.get("id").and_then(|x| x.as_str()).unwrap_or_default().to_string();
|
||||
let title = v
|
||||
.get("title")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let uploader = pick_uploader(&v);
|
||||
let duration_secs = v.get("duration").and_then(|x| x.as_f64()).unwrap_or(0.0);
|
||||
let url = v
|
||||
.get("webpage_url")
|
||||
.and_then(|x| x.as_str())
|
||||
.or_else(|| v.get("url").and_then(|x| x.as_str()))
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let thumbnail = pick_thumbnail(&v);
|
||||
if id.is_empty() || url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
results.push(YtCandidate {
|
||||
id,
|
||||
title,
|
||||
uploader,
|
||||
duration_secs,
|
||||
url,
|
||||
thumbnail,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("yt-dlp: skipping malformed JSON line: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// yt-dlp prints one JSON object per result. If it returned 0 objects
|
||||
// AND a non-zero exit code, surface stderr so the CLI can show it.
|
||||
if results.is_empty() && !out.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let msg = stderr.trim();
|
||||
if !msg.is_empty() {
|
||||
warn!("yt-dlp search produced no results and exited non-zero: {msg}");
|
||||
}
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Clear the queue (memory + DB).
|
||||
pub fn clear_queue(&mut self) -> Result<(), String> {
|
||||
self.queue.clear();
|
||||
@@ -987,9 +1089,57 @@ fn unix_secs() -> i64 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Pick the best "artist / uploader" string for a yt-dlp JSON line.
|
||||
/// Prefer explicit `artist`, then `channel`, then `uploader`, then `creator`
|
||||
/// — falls back to an empty string so the CLI formatter doesn't have to
|
||||
/// special-case missing data.
|
||||
fn pick_uploader(v: &serde_json::Value) -> String {
|
||||
for key in ["artist", "channel", "uploader", "creator"] {
|
||||
if let Some(s) = v.get(key).and_then(|x| x.as_str()) {
|
||||
let s = s.trim();
|
||||
if !s.is_empty() {
|
||||
return s.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Pick a thumbnail URL for a yt-dlp JSON line. Prefers the largest entry in
|
||||
/// the `thumbnails[]` array (when yt-dlp emits per-resolution variants);
|
||||
/// falls back to the singular `thumbnail` field. `None` is returned when
|
||||
/// yt-dlp shipped no cover art at all (rare, but possible for short-lived
|
||||
/// uploads or age-restricted items).
|
||||
fn pick_thumbnail(v: &serde_json::Value) -> Option<String> {
|
||||
if let Some(arr) = v.get("thumbnails").and_then(|x| x.as_array()) {
|
||||
let mut best: Option<(i64, &str)> = None;
|
||||
for entry in arr {
|
||||
let url = entry.get("url").and_then(|x| x.as_str());
|
||||
let h = entry.get("height").and_then(|x| x.as_i64()).unwrap_or(0);
|
||||
if let Some(u) = url {
|
||||
match best {
|
||||
Some((bh, _)) if bh >= h => {}
|
||||
_ => best = Some((h, u)),
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, url)) = best {
|
||||
return Some(url.to_string());
|
||||
}
|
||||
}
|
||||
v.get("thumbnail")
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
/// Spawn a worker thread that downloads a song and updates the matching
|
||||
/// `DownloadJob`. Returns the assigned job id synchronously.
|
||||
pub fn init_async(state: &Arc<Mutex<DaemonState>>, name: String) -> i64 {
|
||||
///
|
||||
/// `name` is the display/storage name under which the song ends up in the
|
||||
/// library. `source` is the literal argument to pass to `yt-dlp` — either an
|
||||
/// explicit URL the user picked from `/search/yt` or `ytsearch1:NAME` for the
|
||||
/// legacy auto-pick path.
|
||||
pub fn init_async(state: &Arc<Mutex<DaemonState>>, name: String, source: String) -> i64 {
|
||||
let job_id = state
|
||||
.lock()
|
||||
.unwrap()
|
||||
@@ -1004,6 +1154,7 @@ pub fn init_async(state: &Arc<Mutex<DaemonState>>, name: String) -> i64 {
|
||||
error: None,
|
||||
started_at: now,
|
||||
finished_at: None,
|
||||
source: Some(source.clone()),
|
||||
};
|
||||
// Scope the locks so both MutexGuards drop together at the end of the
|
||||
// block. A temporary `state.lock()` at statement boundary would be
|
||||
@@ -1019,7 +1170,9 @@ pub fn init_async(state: &Arc<Mutex<DaemonState>>, name: String) -> i64 {
|
||||
let arc = Arc::clone(state);
|
||||
std::thread::Builder::new()
|
||||
.name(format!("sjnmusic-dl-{job_id}"))
|
||||
.spawn(move || run_download_job(arc, job_id, name_for_worker))
|
||||
.spawn(move || {
|
||||
run_download_job(arc, job_id, name_for_worker, source);
|
||||
})
|
||||
.expect("failed to spawn download worker");
|
||||
debug!("Spawned download job {job_id} for {name}");
|
||||
job_id
|
||||
@@ -1027,10 +1180,21 @@ pub fn init_async(state: &Arc<Mutex<DaemonState>>, name: String) -> i64 {
|
||||
|
||||
/// Spawn one worker per name. Returns the assigned ids in the same order.
|
||||
pub fn init_batch(state: &Arc<Mutex<DaemonState>>, names: Vec<String>) -> Vec<i64> {
|
||||
names.iter().map(|n| init_async(state, n.clone())).collect()
|
||||
names
|
||||
.iter()
|
||||
.map(|n| {
|
||||
let source = format!("ytsearch1:{n}");
|
||||
init_async(state, n.clone(), source)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn run_download_job(state: Arc<Mutex<DaemonState>>, job_id: i64, name: String) {
|
||||
fn run_download_job(
|
||||
state: Arc<Mutex<DaemonState>>,
|
||||
job_id: i64,
|
||||
name: String,
|
||||
source: String,
|
||||
) {
|
||||
{
|
||||
let guard = state.lock().unwrap();
|
||||
let mut jobs = guard.download_jobs.lock().unwrap();
|
||||
@@ -1038,7 +1202,7 @@ fn run_download_job(state: Arc<Mutex<DaemonState>>, job_id: i64, name: String) {
|
||||
j.status = "running".to_string();
|
||||
}
|
||||
}
|
||||
let result = state.lock().unwrap().init_sync(&name);
|
||||
let result = state.lock().unwrap().init_sync(&name, &source);
|
||||
{
|
||||
let guard = state.lock().unwrap();
|
||||
let mut jobs = guard.download_jobs.lock().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user