Compare commits

...

18 Commits

21 changed files with 1263 additions and 436 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
/target /target
perf.data*
heaptrack*

390
Cargo.lock generated
View File

@@ -68,9 +68,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.93" version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
@@ -89,6 +89,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@@ -189,9 +195,9 @@ dependencies = [
"serde_json", "serde_json",
"serde_path_to_error", "serde_path_to_error",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper",
"tokio", "tokio",
"tower 0.5.1", "tower 0.5.2",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -212,7 +218,7 @@ dependencies = [
"mime", "mime",
"pin-project-lite", "pin-project-lite",
"rustversion", "rustversion",
"sync_wrapper 1.0.2", "sync_wrapper",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -282,12 +288,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
[[package]] [[package]]
name = "block-buffer" name = "blake3"
version = "0.10.4" version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e"
dependencies = [ dependencies = [
"generic-array", "arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
] ]
[[package]] [[package]]
@@ -322,15 +332,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.1" version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@@ -401,6 +411,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@@ -450,15 +466,6 @@ dependencies = [
"windows", "windows",
] ]
[[package]]
name = "cpufeatures"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@@ -470,9 +477,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [ dependencies = [
"crossbeam-epoch", "crossbeam-epoch",
"crossbeam-utils", "crossbeam-utils",
@@ -489,9 +496,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.20" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "crunchy" name = "crunchy"
@@ -499,16 +506,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]] [[package]]
name = "dasp_sample" name = "dasp_sample"
version = "0.11.0" version = "0.11.0"
@@ -516,13 +513,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]] [[package]]
name = "digest" name = "deadpool"
version = "0.10.7" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed"
dependencies = [ dependencies = [
"block-buffer", "deadpool-runtime",
"crypto-common", "num_cpus",
"tokio",
]
[[package]]
name = "deadpool-runtime"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
dependencies = [
"tokio",
]
[[package]]
name = "deadpool-sqlite"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "656f14fc1ab819c65f332045ea7cb38841bbe551f3b2bc7e3abefb559af4155c"
dependencies = [
"deadpool",
"deadpool-sync",
"rusqlite",
]
[[package]]
name = "deadpool-sync"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04"
dependencies = [
"deadpool-runtime",
] ]
[[package]] [[package]]
@@ -554,12 +581,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.9" version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -597,15 +624,15 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "fdeflate" name = "fdeflate"
version = "0.3.6" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [ dependencies = [
"simd-adler32", "simd-adler32",
] ]
@@ -680,16 +707,6 @@ dependencies = [
"pin-utils", "pin-utils",
] ]
[[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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"
@@ -728,17 +745,17 @@ name = "groove"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"blake3",
"deadpool",
"deadpool-sqlite",
"dotenvy", "dotenvy",
"hex", "hex",
"home", "home",
"image", "image",
"prost", "prost",
"r2d2",
"r2d2_sqlite",
"rayon", "rayon",
"rodio", "rodio",
"rusqlite", "rusqlite",
"sha2",
"symphonia", "symphonia",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@@ -762,7 +779,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"http", "http",
"indexmap 2.6.0", "indexmap 2.7.0",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -796,9 +813,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.1" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]] [[package]]
name = "hashlink" name = "hashlink"
@@ -844,9 +861,9 @@ checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
[[package]] [[package]]
name = "http" name = "http"
version = "1.1.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -878,9 +895,9 @@ dependencies = [
[[package]] [[package]]
name = "http-range-header" name = "http-range-header"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]] [[package]]
name = "httparse" name = "httparse"
@@ -998,12 +1015,12 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.6.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.1", "hashbrown 0.15.2",
] ]
[[package]] [[package]]
@@ -1037,9 +1054,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.13" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]] [[package]]
name = "jni" name = "jni"
@@ -1080,10 +1097,11 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.72" version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [ dependencies = [
"once_cell",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -1112,9 +1130,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.164" version = "0.2.168"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
@@ -1128,9 +1146,9 @@ dependencies = [
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.52.6",
@@ -1253,11 +1271,10 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [ dependencies = [
"hermit-abi",
"libc", "libc",
"wasi", "wasi",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -1370,6 +1387,16 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]] [[package]]
name = "num_enum" name = "num_enum"
version = "0.7.3" version = "0.7.3"
@@ -1480,7 +1507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [ dependencies = [
"fixedbitset", "fixedbitset",
"indexmap 2.6.0", "indexmap 2.7.0",
] ]
[[package]] [[package]]
@@ -1523,9 +1550,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]] [[package]]
name = "png" name = "png"
version = "0.17.14" version = "0.17.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" checksum = "b67582bd5b65bdff614270e2ea89a1cf15bef71245cc1e5f7ea126977144211d"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"crc32fast", "crc32fast",
@@ -1592,9 +1619,9 @@ dependencies = [
[[package]] [[package]]
name = "prost" name = "prost"
version = "0.13.3" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
dependencies = [ dependencies = [
"bytes", "bytes",
"prost-derive", "prost-derive",
@@ -1602,11 +1629,10 @@ dependencies = [
[[package]] [[package]]
name = "prost-build" name = "prost-build"
version = "0.13.3" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b"
dependencies = [ dependencies = [
"bytes",
"heck", "heck",
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
@@ -1623,9 +1649,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-derive" name = "prost-derive"
version = "0.13.3" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.13.0", "itertools 0.13.0",
@@ -1636,9 +1662,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-types" name = "prost-types"
version = "0.13.3" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc"
dependencies = [ dependencies = [
"prost", "prost",
] ]
@@ -1667,28 +1693,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r2d2"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
dependencies = [
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]]
name = "r2d2_sqlite"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb14dba8247a6a15b7fdbc7d389e2e6f03ee9f184f87117706d509c092dfe846"
dependencies = [
"r2d2",
"rusqlite",
"uuid",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@@ -1791,9 +1795,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.7" version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
] ]
@@ -1874,15 +1878,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.41" version = "0.38.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -1906,15 +1910,6 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "scheduled-thread-pool"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
dependencies = [
"parking_lot",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -1923,18 +1918,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.215" version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.215" version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1984,17 +1979,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -2042,9 +2026,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.7" version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -2198,21 +2182,15 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.89" version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]] [[package]]
name = "sync_wrapper" name = "sync_wrapper"
version = "1.0.2" version = "1.0.2"
@@ -2248,7 +2226,7 @@ dependencies = [
"fastrand", "fastrand",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -2299,9 +2277,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.41.1" version = "1.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -2328,9 +2306,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.16" version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
@@ -2339,9 +2317,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.12" version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
@@ -2377,7 +2355,7 @@ version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [ dependencies = [
"indexmap 2.6.0", "indexmap 2.7.0",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
@@ -2470,14 +2448,14 @@ dependencies = [
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.1" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"pin-project-lite", "pin-project-lite",
"sync_wrapper 0.1.2", "sync_wrapper",
"tokio", "tokio",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
@@ -2539,9 +2517,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.40" version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"log", "log",
"pin-project-lite", "pin-project-lite",
@@ -2551,9 +2529,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.27" version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2562,9 +2540,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.32" version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
] ]
@@ -2575,20 +2553,11 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.7.0" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
@@ -2596,16 +2565,6 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "uuid"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"rand",
]
[[package]] [[package]]
name = "v_frame" name = "v_frame"
version = "0.3.8" version = "0.3.8"
@@ -2662,9 +2621,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.95" version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -2673,13 +2632,12 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.95" version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@@ -2688,21 +2646,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.45" version = "0.4.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"once_cell",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
] ]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.95" version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -2710,9 +2669,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.95" version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2723,15 +2682,15 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.95" version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.72" version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -2759,7 +2718,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -2809,6 +2768,15 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.42.2" version = "0.42.2"
@@ -2977,9 +2945,9 @@ dependencies = [
[[package]] [[package]]
name = "zune-jpeg" name = "zune-jpeg"
version = "0.4.13" version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
dependencies = [ dependencies = [
"zune-core", "zune-core",
] ]

View File

@@ -5,17 +5,17 @@ edition = "2021"
[dependencies] [dependencies]
axum = "0.7.9" axum = "0.7.9"
blake3 = "1.5.4"
deadpool = "0.12.1"
deadpool-sqlite = "0.9.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
hex = "0.4.3" hex = "0.4.3"
home = "0.5.9" home = "0.5.9"
image = "0.25.5" image = "0.25.5"
prost = "0.13.3" prost = "0.13.3"
r2d2 = "0.8.10"
r2d2_sqlite = { version = "0.25.0", features = ["bundled"] }
rayon = "1.10.0" rayon = "1.10.0"
rodio = "0.20.1" rodio = "0.20.1"
rusqlite = { version = "0.32.1", features = ["bundled"] } rusqlite = { version = "0.32.1", features = ["bundled"] }
sha2 = "0.10.8"
symphonia = { version = "0.5.4", features = ["mp3"] } symphonia = { version = "0.5.4", features = ["mp3"] }
tokio = { version = "1.41.1", features = ["full"] } tokio = { version = "1.41.1", features = ["full"] }
tokio-stream = "0.1.16" tokio-stream = "0.1.16"

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# groove
An audio player for local files.
> [!NOTE]
> This is merely the backend, for the frontend see [groove-web](https://github.com/4-0-9/groove-web).

View File

@@ -6,6 +6,12 @@ package library;
service Library { service Library {
rpc ListTracks(google.protobuf.Empty) returns (TrackList); rpc ListTracks(google.protobuf.Empty) returns (TrackList);
rpc ListPlaylists(google.protobuf.Empty) returns (ListPlaylistsResponse);
rpc CreatePlaylist(CreatePlaylistRequest) returns (CreatePlaylistResponse);
rpc DeletePlaylist(DeletePlaylistRequest) returns (google.protobuf.Empty);
rpc AddTrackToPlaylist(AddTrackToPlaylistRequest) returns (TrackList);
rpc RemoveTrackFromPlaylist(RemoveTrackFromPlaylistRequest) returns (TrackList);
rpc SwapTracks(SwapTracksRequest) returns (TrackList);
} }
message TrackList { message TrackList {
@@ -19,3 +25,41 @@ message Track {
uint64 artist_id = 4; uint64 artist_id = 4;
uint64 duration = 5; uint64 duration = 5;
} }
message Playlist {
uint32 id = 1;
string name = 2;
repeated Track tracks = 3;
}
message ListPlaylistsResponse {
repeated Playlist playlists = 1;
}
message CreatePlaylistRequest {
string name = 1;
}
message CreatePlaylistResponse {
Playlist playlist = 1;
}
message DeletePlaylistRequest {
uint32 id = 1;
}
message AddTrackToPlaylistRequest {
uint32 playlist_id = 1;
string track_hash = 2;
}
message RemoveTrackFromPlaylistRequest {
uint32 playlist_id = 1;
uint32 track_rank = 2;
}
message SwapTracksRequest {
uint32 playlist_id = 1;
uint32 a_rank = 2;
uint32 b_rank = 3;
}

View File

@@ -6,16 +6,23 @@ import 'library.proto';
package player; package player;
service Player { service Player {
rpc PlayTrack(PlayTrackRequest) returns (PlayTrackResponse); rpc PlayTrack(TrackRequest) returns (PlayTrackResponse);
rpc ResumeTrack(google.protobuf.Empty) returns (PauseState); rpc ResumeTrack(google.protobuf.Empty) returns (PauseState);
rpc PauseTrack(google.protobuf.Empty) returns (PauseState); rpc PauseTrack(google.protobuf.Empty) returns (PauseState);
rpc TogglePause(google.protobuf.Empty) returns (PauseState); rpc TogglePause(google.protobuf.Empty) returns (PauseState);
rpc GetStatus(google.protobuf.Empty) returns (stream PlayerStatus); rpc GetStatus(google.protobuf.Empty) returns (stream PlayerStatus);
rpc SeekPosition(SeekPositionRequest) returns (SeekPositionResponse); rpc SeekPosition(SeekPositionRequest) returns (SeekPositionResponse);
rpc SetVolume(SetVolumeRequest) returns (SetVolumeResponse); rpc SetVolume(SetVolumeRequest) returns (SetVolumeResponse);
rpc PlayTrackNext(TrackRequest) returns (Queue);
rpc AddTrackToQueue(TrackRequest) returns (Queue);
rpc AddTracksToQueue(TracksRequest) returns (Queue);
rpc PlayPlaylist(PlayPlaylistRequest) returns (PlayerStatus);
rpc SwapQueueIndices(SwapQueueIndicesRequest) returns (Queue);
rpc SkipTrack(google.protobuf.Empty) returns (PlayerStatus);
rpc SkipToQueueIndex(SkipToQueueIndexRequest) returns (PlayerStatus);
} }
message PlayTrackRequest { message TrackRequest {
string hash = 1; string hash = 1;
} }
@@ -29,6 +36,11 @@ message PlayerStatus {
bool is_paused = 2; bool is_paused = 2;
float volume = 3; float volume = 3;
uint64 progress = 4; uint64 progress = 4;
repeated library.Track queue = 5;
}
message Queue {
repeated library.Track tracks = 1;
} }
message PauseState { message PauseState {
@@ -50,3 +62,21 @@ message SetVolumeRequest {
message SetVolumeResponse { message SetVolumeResponse {
float volume = 1; float volume = 1;
} }
message SkipToQueueIndexRequest {
uint32 index = 1;
}
message SwapQueueIndicesRequest {
uint32 a = 1;
uint32 b = 2;
}
message TracksRequest {
repeated string tracks = 1;
}
message PlayPlaylistRequest {
uint32 id = 1;
optional uint32 starting_rank = 2;
}

View File

@@ -1,11 +1,7 @@
use sha2::{self, Digest, Sha256}; use blake3::hash;
pub fn generate_hash(content: impl AsRef<[u8]>) -> String { pub fn generate_hash(content: &[u8]) -> String {
let mut hasher = Sha256::new(); let hash = hash(content);
hasher.update(content);
let result = hasher.finalize();
let hex = hex::encode(result); hash.to_string()
hex
} }

View File

@@ -1,6 +1,6 @@
use std::{fs, path::Path, time::Instant};
use axum::Router; use axum::Router;
use image::EncodableLayout;
use std::{fs, path::Path};
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use webp::Encoder; use webp::Encoder;
@@ -40,9 +40,11 @@ pub fn get_all_cover_hashes() -> Vec<String> {
hashes hashes
} }
pub fn write_cover(hash: String, cover: CoverData) -> Result<(), Box<dyn std::error::Error>> { pub fn write_cover(
let now = Instant::now(); hash: &str,
cover: &CoverData,
base_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if cover.mime_type != "image/jpeg" if cover.mime_type != "image/jpeg"
&& cover.mime_type != "image/png" && cover.mime_type != "image/png"
&& cover.mime_type != "image/webp" && cover.mime_type != "image/webp"
@@ -50,34 +52,21 @@ pub fn write_cover(hash: String, cover: CoverData) -> Result<(), Box<dyn std::er
return Err(format!("Invalid cover MIME type: {}", cover.mime_type).into()); return Err(format!("Invalid cover MIME type: {}", cover.mime_type).into());
} }
let base_path = get_cover_base_path(); let dynamic_image = image::load_from_memory(&cover.bytes)?;
let path = Path::new(&base_path).join(format!("{hash}.webp"));
let dynamic_image = image::load_from_memory_with_format(
&cover.bytes,
match cover.mime_type.as_str() {
"image/png" => image::ImageFormat::Png,
"image/jpeg" => image::ImageFormat::Jpeg,
"image/webp" => image::ImageFormat::WebP,
_ => panic!("Invalid cover MIME type (this should never happen)"),
},
)?;
let resized_image = if dynamic_image.width() > 640 || dynamic_image.height() > 640 { let resized_image = if dynamic_image.width() > 640 || dynamic_image.height() > 640 {
dynamic_image.resize_to_fill(640, 640, image::imageops::FilterType::Lanczos3) let a = dynamic_image.resize_to_fill(640, 640, image::imageops::FilterType::Lanczos3);
drop(dynamic_image);
a
} else { } else {
dynamic_image dynamic_image
}; };
let webp_encoder = Encoder::from_image(&resized_image)?; let webp = Encoder::from_image(&resized_image)?.encode_lossless();
let encoded_image = webp_encoder.encode_lossless().to_vec();
fs::write(path, encoded_image)?; let path = Path::new(&base_path).join(format!("{hash}.webp"));
let elapsed = now.elapsed(); fs::write(path, webp.as_bytes())?;
println!("Writing '{}' cover took {:.?}", hash, elapsed);
Ok(()) Ok(())
} }

View File

@@ -1,5 +1,4 @@
use r2d2::PooledConnection; use deadpool_sqlite::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::Row; use rusqlite::Row;
#[derive(Debug)] #[derive(Debug)]
@@ -15,9 +14,10 @@ fn map_artist(row: &Row) -> Result<Artist, rusqlite::Error> {
}) })
} }
pub fn get_artists( pub async fn get_artists(pool: &Pool) -> Result<Vec<Artist>, rusqlite::Error> {
connection: &PooledConnection<SqliteConnectionManager>, let manager = pool.get().await.unwrap();
) -> Result<Vec<Artist>, rusqlite::Error> { let connection = manager.lock().unwrap();
let mut statement = connection.prepare("SELECT id, name FROM artists")?; let mut statement = connection.prepare("SELECT id, name FROM artists")?;
let rows = statement.query_map([], map_artist)?; let rows = statement.query_map([], map_artist)?;

View File

@@ -1,25 +1,27 @@
use deadpool_sqlite::{Config, Pool};
pub mod artists; pub mod artists;
pub mod paths; pub mod paths;
pub mod tracks; pub mod tracks;
use r2d2::{Pool, PooledConnection}; pub fn establish_connection() -> Pool {
use r2d2_sqlite::SqliteConnectionManager;
pub fn establish_connection() -> Pool<SqliteConnectionManager> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let manager = SqliteConnectionManager::file(database_url); let cfg = Config::new(database_url);
let pool = Pool::new(manager).expect("Error creating SQLite pool"); let pool = cfg
.create_pool(deadpool::Runtime::Tokio1)
.expect("Error creating SQLite pool");
pool pool
} }
pub fn initialize_database( pub async fn initialize_database(pool: &Pool) -> Result<(), rusqlite::Error> {
connection: &PooledConnection<SqliteConnectionManager>, let manager = pool.get().await.unwrap();
) -> Result<(), r2d2_sqlite::rusqlite::Error> { let conn = manager.lock().unwrap();
connection.execute(
conn.execute(
" "
CREATE TABLE IF NOT EXISTS library_paths ( CREATE TABLE IF NOT EXISTS library_paths (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
@@ -29,7 +31,7 @@ pub fn initialize_database(
[], [],
)?; )?;
connection.execute( conn.execute(
"CREATE TABLE IF NOT EXISTS artists ( "CREATE TABLE IF NOT EXISTS artists (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL name TEXT NOT NULL
@@ -37,7 +39,17 @@ pub fn initialize_database(
[], [],
)?; )?;
connection.execute( conn.execute(
"CREATE TABLE IF NOT EXISTS albums (
name TEXT NOT NULL,
artist_id INTEGER NOT NULL,
PRIMARY KEY (name, artist_id),
FOREIGN KEY (artist_id) REFERENCES artists (id) ON DELETE CASCADE
)",
[],
)?;
conn.execute(
" "
CREATE TABLE IF NOT EXISTS tracks ( CREATE TABLE IF NOT EXISTS tracks (
hash TEXT PRIMARY KEY NOT NULL, hash TEXT PRIMARY KEY NOT NULL,
@@ -53,5 +65,29 @@ pub fn initialize_database(
[], [],
)?; )?;
conn.execute(
"
CREATE TABLE IF NOT EXISTS playlists (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL
);
",
[],
)?;
conn.execute(
"
CREATE TABLE IF NOT EXISTS playlist_tracks (
playlist_id INTEGER NOT NULL,
track_hash TEXT NOT NULL,
rank INTEGER NOT NULL,
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE,
FOREIGN KEY (track_hash) REFERENCES tracks (hash) ON DELETE CASCADE,
UNIQUE (playlist_id, rank) ON CONFLICT FAIL
);
",
[],
)?;
Ok(()) Ok(())
} }

View File

@@ -1,5 +1,4 @@
use r2d2::PooledConnection; use deadpool_sqlite::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::Row; use rusqlite::Row;
#[derive(Debug)] #[derive(Debug)]
@@ -17,9 +16,10 @@ fn map_library_path(row: &Row) -> Result<LibraryPath, rusqlite::Error> {
}) })
} }
pub fn get_library_paths( pub async fn get_library_paths(pool: &Pool) -> Result<Vec<LibraryPath>, rusqlite::Error> {
connection: &PooledConnection<SqliteConnectionManager>, let manager = pool.get().await.unwrap();
) -> Result<Vec<LibraryPath>, rusqlite::Error> { let connection = manager.lock().unwrap();
let mut statement = connection.prepare("SELECT id, path FROM library_paths")?; let mut statement = connection.prepare("SELECT id, path FROM library_paths")?;
let rows = statement.query_map([], map_library_path)?; let rows = statement.query_map([], map_library_path)?;
@@ -34,10 +34,10 @@ pub fn get_library_paths(
Ok(paths) Ok(paths)
} }
pub fn get_library_path( pub async fn get_library_path(pool: &Pool, id: u64) -> Result<LibraryPath, rusqlite::Error> {
connection: &PooledConnection<SqliteConnectionManager>, let manager = pool.get().await.unwrap();
id: u64, let connection = manager.lock().unwrap();
) -> Result<LibraryPath, rusqlite::Error> {
Ok(connection.query_row( Ok(connection.query_row(
"SELECT id, path FROM library_paths WHERE id = ?1", "SELECT id, path FROM library_paths WHERE id = ?1",
[id], [id],
@@ -45,19 +45,22 @@ pub fn get_library_path(
)?) )?)
} }
pub fn insert_library_path( pub async fn insert_library_path(
connection: &PooledConnection<SqliteConnectionManager>, pool: &Pool,
path: LibraryPathInsertData, path: LibraryPathInsertData,
) -> Result<bool, rusqlite::Error> { ) -> Result<bool, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = manager.lock().unwrap();
let result = connection.execute("INSERT INTO library_paths (path) VALUES (?1)", [path])?; let result = connection.execute("INSERT INTO library_paths (path) VALUES (?1)", [path])?;
Ok(result > 0) Ok(result > 0)
} }
pub fn delete_library_path( pub async fn delete_library_path(pool: &Pool, path_id: u64) -> Result<bool, rusqlite::Error> {
connection: &PooledConnection<SqliteConnectionManager>, let manager = pool.get().await.unwrap();
path_id: u64, let connection = manager.lock().unwrap();
) -> Result<bool, rusqlite::Error> {
let result = connection.execute("DELETE FROM library_paths WHERE id = ?1", [path_id])?; let result = connection.execute("DELETE FROM library_paths WHERE id = ?1", [path_id])?;
Ok(result > 0) Ok(result > 0)

View File

@@ -1,17 +1,15 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
thread::JoinHandle,
}; };
use r2d2::PooledConnection; use deadpool_sqlite::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{params, Row}; use rusqlite::{params, Row};
use crate::{ use crate::{
covers::{get_all_cover_hashes, write_cover}, covers::{get_all_cover_hashes, get_cover_base_path, write_cover},
music::metadata::TrackMetadata, music::metadata::TrackMetadata,
proto, proto::{self, library::Playlist},
}; };
use super::artists::get_artists; use super::artists::get_artists;
@@ -59,9 +57,20 @@ fn map_track(row: &Row) -> Result<Track, rusqlite::Error> {
}) })
} }
pub fn get_tracks( /// Creates a playlist from a database row (tracks are empty and need to be filled afterwards)
connection: &PooledConnection<SqliteConnectionManager>, /// * `row`: The database row
) -> Result<Vec<Track>, rusqlite::Error> { fn map_playlist(row: &Row) -> Result<Playlist, rusqlite::Error> {
Ok(Playlist {
id: row.get(0)?,
name: row.get(1)?,
tracks: Vec::new(),
})
}
pub async fn get_tracks(pool: &Pool) -> Result<Vec<Track>, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = manager.lock().unwrap();
let mut statement = connection.prepare("SELECT t.hash, t.name, t.duration, t.artist_id, a.name AS artist_name FROM tracks t INNER JOIN artists a ON a.id = t.artist_id")?; let mut statement = connection.prepare("SELECT t.hash, t.name, t.duration, t.artist_id, a.name AS artist_name FROM tracks t INNER JOIN artists a ON a.id = t.artist_id")?;
let rows = statement.query_map([], map_track)?; let rows = statement.query_map([], map_track)?;
@@ -76,17 +85,34 @@ pub fn get_tracks(
Ok(tracks) Ok(tracks)
} }
pub fn get_track( pub async fn get_track(pool: &Pool, hash: &str) -> Result<Track, rusqlite::Error> {
connection: &PooledConnection<SqliteConnectionManager>, let manager = pool.get().await.unwrap();
hash: &str, let connection = &manager.lock().unwrap();
) -> Result<Track, rusqlite::Error> {
connection.query_row("SELECT t.hash, t.name, t.duration, t.artist_id, a.name AS artist_name FROM tracks t INNER JOIN artists a ON a.id = t.artist_id WHERE t.hash = ?1", [hash], map_track) connection.query_row("SELECT t.hash, t.name, t.duration, t.artist_id, a.name AS artist_name FROM tracks t INNER JOIN artists a ON a.id = t.artist_id WHERE t.hash = ?1", [hash], map_track)
} }
pub fn get_track_full_path( pub async fn get_specific_tracks(
connection: &PooledConnection<SqliteConnectionManager>, pool: &Pool,
hash: &str, hashes: &Vec<String>,
) -> Result<PathBuf, rusqlite::Error> { ) -> Result<Vec<Track>, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = &manager.lock().unwrap();
let mut statement = connection.prepare("SELECT t.hash, t.name, t.duration, t.artist_id, a.name AS artist_name FROM tracks t INNER JOIN artists a ON a.id = t.artist_id WHERE t.hash = ?1")?;
let mut tracks: Vec<Track> = Vec::new();
for hash in hashes {
tracks.push(statement.query_row([hash], map_track)?);
}
Ok(tracks)
}
pub async fn get_track_full_path(pool: &Pool, hash: &str) -> Result<PathBuf, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = &manager.lock().unwrap();
let (relative_path, library_path): (String, String) = connection.query_row( let (relative_path, library_path): (String, String) = connection.query_row(
"SELECT t.path, l.path AS library_root FROM tracks t INNER JOIN library_paths l ON t.library_path_id = l.id WHERE t.hash = ?1", "SELECT t.path, l.path AS library_root FROM tracks t INNER JOIN library_paths l ON t.library_path_id = l.id WHERE t.hash = ?1",
[hash], [hash],
@@ -99,14 +125,43 @@ pub fn get_track_full_path(
Ok(path) Ok(path)
} }
pub fn insert_tracks( pub async fn get_specific_tracks_full_path(
mut connection: PooledConnection<SqliteConnectionManager>, pool: &Pool,
hashes: &Vec<String>,
) -> Result<Vec<PathBuf>, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = &manager.lock().unwrap();
let mut paths: Vec<PathBuf> = Vec::new();
let home = home::home_dir().unwrap();
let home_dir = home.to_str().unwrap();
let mut statement = connection.prepare(
"SELECT t.path, l.path AS library_root FROM tracks t INNER JOIN library_paths l ON t.library_path_id = l.id WHERE t.hash = ?1",
)?;
for hash in hashes {
let (relative_path, library_path): (String, String) =
statement.query_row([hash], |r| Ok((r.get(0)?, r.get(1)?)))?;
let library_root = library_path.replace("~", home_dir);
let path = Path::new(&library_root).join(&relative_path);
paths.push(path);
}
Ok(paths)
}
pub async fn insert_tracks(
pool: &Pool,
tracks: HashMap<String, TrackMetadata>, tracks: HashMap<String, TrackMetadata>,
library_path_id: u64, library_path_id: u64,
) -> Result<(), rusqlite::Error> { ) -> Result<(), rusqlite::Error> {
let existing_covers = get_all_cover_hashes(); let existing_covers = get_all_cover_hashes();
let artists = get_artists(&connection)?; let artists = get_artists(pool).await?;
let mut artist_names_to_id: HashMap<String, u64> = HashMap::new(); let mut artist_names_to_id: HashMap<String, u64> = HashMap::new();
for artist in artists { for artist in artists {
@@ -125,6 +180,9 @@ pub fn insert_tracks(
new_artists.push(meta.artist_name.clone()); new_artists.push(meta.artist_name.clone());
} }
let manager = pool.get().await.unwrap();
let mut connection = manager.lock().unwrap();
// BEGIN TRANSACTION // BEGIN TRANSACTION
let tx = connection.transaction()?; let tx = connection.transaction()?;
@@ -145,13 +203,36 @@ pub fn insert_tracks(
// BEGIN TRANSACTION // BEGIN TRANSACTION
let tx = connection.transaction()?; let tx = connection.transaction()?;
let mut cover_handles: Vec<JoinHandle<()>> = Vec::new();
{ {
let mut statement = let base_path = get_cover_base_path();
tx.prepare("INSERT OR REPLACE INTO tracks (hash, library_path_id, name, artist_id, path, duration) VALUES (?1, ?2, ?3, ?4, ?5, ?6)")?;
for (hash, meta) in tracks { std::thread::scope(|s| {
for (hash, meta) in tracks.iter() {
if let Some(ref cover) = &meta.cover {
if !existing_covers.contains(&hash) {
s.spawn(|| {
let _ = write_cover(hash, cover, &base_path);
});
}
}
}
});
let mut statement =
tx.prepare("
INSERT INTO tracks
(hash, library_path_id, name, artist_id, path, duration)
VALUES
(?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(hash) DO UPDATE SET
library_path_id = ?2,
name = ?3,
artist_id = ?4,
path = ?5,
duration = ?6
")?;
for (hash, meta) in tracks.iter() {
statement.execute(params![ statement.execute(params![
&hash, &hash,
library_path_id, library_path_id,
@@ -160,23 +241,161 @@ pub fn insert_tracks(
meta.path, meta.path,
meta.total_seconds * 1000 meta.total_seconds * 1000
])?; ])?;
if let Some(cover) = meta.cover {
if !existing_covers.contains(&hash) {
cover_handles.push(std::thread::spawn(|| {
let _ = write_cover(hash, cover);
}));
}
}
} }
} }
// COMMIT // COMMIT
tx.commit()?; tx.commit()?;
for handle in cover_handles {
let _ = handle.join();
}
Ok(()) Ok(())
} }
pub async fn get_playlists(pool: &Pool) -> Result<Vec<Playlist>, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = manager.lock().unwrap();
let mut statement = connection.prepare("SELECT id, name FROM playlists")?;
let rows = statement.query_map([], map_playlist)?;
let mut playlists: Vec<Playlist> = Vec::new();
let mut tracks_statement = connection.prepare("
SELECT t.hash, t.name, t.duration, t.artist_id, a.name FROM playlist_tracks pt INNER JOIN tracks t ON pt.track_hash = t.hash INNER JOIN artists a ON t.artist_id = a.id WHERE pt.playlist_id = ?1
")?;
for row in rows {
if let Ok(mut row) = row {
let track_rows: Vec<proto::library::Track> = tracks_statement
.query_map([row.id], map_track)?
.filter_map(|t| t.ok().map(|t| t.into()))
.collect();
row.tracks = track_rows;
playlists.push(row);
}
}
Ok(playlists)
}
pub async fn get_playlist(pool: &Pool, id: u32) -> Result<Playlist, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = manager.lock().unwrap();
let mut playlist = connection.query_row(
"SELECT id, name FROM playlists WHERE id = ?1",
[id],
map_playlist,
)?;
let mut tracks_statement = connection.prepare("
SELECT t.hash, t.name, t.duration, t.artist_id, a.name FROM playlist_tracks pt INNER JOIN tracks t ON pt.track_hash = t.hash INNER JOIN artists a ON t.artist_id = a.id WHERE pt.playlist_id = ?1
")?;
let track_rows: Vec<proto::library::Track> = tracks_statement
.query_map([id], map_track)?
.filter_map(|t| t.ok().map(|t| t.into()))
.collect();
playlist.tracks = track_rows;
Ok(playlist)
}
pub async fn create_playlist(pool: &Pool, name: &str) -> Result<Playlist, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = manager.lock().unwrap();
let mut statement =
connection.prepare("INSERT INTO playlists (name) VALUES (?1) RETURNING id, name")?;
let playlist = statement.query_row([name], map_playlist)?;
Ok(playlist)
}
pub async fn delete_playlist(pool: &Pool, id: u32) -> Result<bool, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = manager.lock().unwrap();
let mut statement = connection.prepare("DELETE FROM playlists WHERE id = ?1")?;
let len = statement.execute([id])?;
Ok(len == 1)
}
pub async fn add_track_to_playlist(
pool: &Pool,
playlist_id: u32,
track_hash: &str,
) -> Result<bool, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let connection = manager.lock().unwrap();
let mut statement = connection
.prepare("INSERT INTO playlist_tracks (playlist_id, track_hash, rank) VALUES (?1, ?2, (SELECT COALESCE(MAX(rank) + 1, 0) FROM playlist_tracks WHERE playlist_id = ?1))")?;
let len = statement.execute(params![playlist_id, track_hash])?;
Ok(len == 1)
}
pub async fn remove_track_from_playlist(
pool: &Pool,
playlist_id: u32,
track_rank: u32,
) -> Result<bool, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let mut connection = manager.lock().unwrap();
let tx = connection.transaction()?;
let len = tx.execute(
"DELETE FROM playlist_tracks WHERE playlist_id = ?1 AND rank = ?2",
params![playlist_id, track_rank],
)?;
tx.execute(
"UPDATE playlist_tracks SET rank = - rank WHERE playlist_id = ?1 AND rank > ?2",
[playlist_id, track_rank],
)?;
tx.execute(
"UPDATE playlist_tracks SET rank = - rank - 1 WHERE playlist_id = ?1 AND rank < 0",
[playlist_id],
)?;
if len != 1 {
tx.rollback()?;
return Ok(false);
}
tx.commit()?;
Ok(true)
}
pub async fn swap_playlist_tracks(
pool: &Pool,
playlist_id: u32,
a_rank: u32,
b_rank: u32,
) -> Result<bool, rusqlite::Error> {
let manager = pool.get().await.unwrap();
let mut connection = manager.lock().unwrap();
let tx = connection.transaction()?;
tx.execute(
"UPDATE playlist_tracks SET rank = CASE WHEN rank = ?2 THEN - ?3 - 1 ELSE - ?2 - 1 END WHERE playlist_id = ?1 AND rank IN (?2, ?3)",
[playlist_id, a_rank, b_rank],
)?;
let len = tx.execute("UPDATE playlist_tracks SET rank = - rank - 1 WHERE playlist_id = ?1 AND rank IN (- ?2 - 1, - ?3 - 1)", [playlist_id, a_rank, b_rank])?;
if len != 2 {
tx.rollback()?;
return Ok(false);
}
tx.commit()?;
Ok(true)
}

View File

@@ -1,47 +1,47 @@
use rayon::prelude::*; use deadpool_sqlite::Pool;
use std::{collections::HashMap, fs, path::PathBuf, time::Instant}; use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Instant};
use tokio::{sync::Mutex, task::JoinSet};
use r2d2::{Pool, PooledConnection}; use tonic::{Request, Response, Status};
use r2d2_sqlite::SqliteConnectionManager;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
use crate::{ use crate::{
checksum::generate_hash, checksum::generate_hash,
database::tracks::{get_tracks, insert_tracks}, database::tracks::{
add_track_to_playlist, create_playlist, delete_playlist, get_playlist, get_playlists,
get_tracks, insert_tracks, remove_track_from_playlist, swap_playlist_tracks,
},
music::metadata::{extract_track_data, TrackMetadata}, music::metadata::{extract_track_data, TrackMetadata},
proto::library::{library_server::Library, Track, TrackList}, proto::library::{
library_server::Library, AddTrackToPlaylistRequest, CreatePlaylistRequest,
CreatePlaylistResponse, DeletePlaylistRequest, ListPlaylistsResponse,
RemoveTrackFromPlaylistRequest, SwapTracksRequest, Track, TrackList,
},
state::GrooveState, state::GrooveState,
}; };
pub struct LibraryService { pub struct LibraryService {
#[allow(dead_code)] #[allow(dead_code)]
state: GrooveState, state: GrooveState,
pool: Pool<SqliteConnectionManager>, pool: Pool,
} }
impl LibraryService { impl LibraryService {
pub fn new(state: GrooveState, pool: Pool<SqliteConnectionManager>) -> Self { pub fn new(state: GrooveState, pool: Pool) -> Self {
Self { state, pool } Self { state, pool }
} }
} }
#[tonic::async_trait] #[tonic::async_trait]
impl Library for LibraryService { impl Library for LibraryService {
async fn list_tracks( async fn list_tracks(&self, _request: Request<()>) -> Result<Response<TrackList>, Status> {
&self, let Ok(tracks) = get_tracks(&self.pool).await else {
_request: tonic::Request<()>, return Err(Status::internal(""));
) -> Result<tonic::Response<TrackList>, tonic::Status> {
let Ok(db) = self.pool.get() else {
return Err(tonic::Status::internal(""));
};
let Ok(tracks) = get_tracks(&db) else {
return Err(tonic::Status::internal(""));
}; };
let response = TrackList { let response = TrackList {
tracks: tracks tracks: tracks
.iter() .into_iter()
.map(|t| Track { .map(|t| Track {
hash: t.hash.clone(), hash: t.hash.clone(),
name: t.name.clone(), name: t.name.clone(),
@@ -52,15 +52,139 @@ impl Library for LibraryService {
.collect::<Vec<Track>>(), .collect::<Vec<Track>>(),
}; };
Ok(tonic::Response::new(response)) Ok(Response::new(response))
}
async fn list_playlists(
&self,
_request: Request<()>,
) -> Result<Response<ListPlaylistsResponse>, Status> {
let Ok(playlists) = get_playlists(&self.pool).await else {
return Err(Status::internal(""));
};
let response = ListPlaylistsResponse { playlists };
Ok(Response::new(response))
}
async fn create_playlist(
&self,
request: Request<CreatePlaylistRequest>,
) -> Result<Response<CreatePlaylistResponse>, Status> {
let input = request.get_ref();
let Ok(playlist) = create_playlist(&self.pool, &input.name).await else {
return Err(Status::internal(""));
};
let response = CreatePlaylistResponse {
playlist: Some(playlist),
};
Ok(Response::new(response))
}
async fn delete_playlist(
&self,
request: Request<DeletePlaylistRequest>,
) -> Result<Response<()>, Status> {
let input = request.get_ref();
let Ok(success) = delete_playlist(&self.pool, input.id).await else {
return Err(Status::internal(""));
};
if !success {
return Err(Status::internal(""));
}
Ok(Response::new(()))
}
async fn add_track_to_playlist(
&self,
request: Request<AddTrackToPlaylistRequest>,
) -> Result<Response<TrackList>, Status> {
let input = request.get_ref();
let Ok(success) =
add_track_to_playlist(&self.pool, input.playlist_id, &input.track_hash).await
else {
return Err(Status::internal(""));
};
if !success {
return Err(Status::internal(""));
}
let Ok(playlist) = get_playlist(&self.pool, input.playlist_id).await else {
return Err(Status::internal(""));
};
let response = TrackList {
tracks: playlist.tracks,
};
Ok(Response::new(response))
}
async fn remove_track_from_playlist(
&self,
request: Request<RemoveTrackFromPlaylistRequest>,
) -> Result<Response<TrackList>, Status> {
let input = request.get_ref();
let Ok(success) =
remove_track_from_playlist(&self.pool, input.playlist_id, input.track_rank).await
else {
return Err(Status::internal(""));
};
if !success {
return Err(Status::internal(""));
}
let Ok(playlist) = get_playlist(&self.pool, input.playlist_id).await else {
return Err(Status::internal(""));
};
let response = TrackList {
tracks: playlist.tracks,
};
Ok(Response::new(response))
}
async fn swap_tracks(
&self,
request: Request<SwapTracksRequest>,
) -> Result<Response<TrackList>, Status> {
let input = request.get_ref();
let Ok(success) =
swap_playlist_tracks(&self.pool, input.playlist_id, input.a_rank, input.b_rank).await
else {
return Err(Status::internal(""));
};
if !success {
return Err(Status::internal(""));
}
let Ok(playlist) = get_playlist(&self.pool, input.playlist_id).await else {
return Err(Status::internal(""));
};
let response = TrackList {
tracks: playlist.tracks,
};
Ok(Response::new(response))
} }
} }
pub fn index_path( pub async fn index_path(path: PathBuf, db: &Pool, path_id: u64) -> Result<(), rusqlite::Error> {
path: PathBuf,
db: PooledConnection<SqliteConnectionManager>,
path_id: u64,
) -> Result<(), rusqlite::Error> {
let home = home::home_dir().unwrap(); let home = home::home_dir().unwrap();
let correct_path = path.to_str().unwrap().replace("~", home.to_str().unwrap()); let correct_path = path.to_str().unwrap().replace("~", home.to_str().unwrap());
@@ -73,45 +197,37 @@ pub fn index_path(
.filter_map(Result::ok) .filter_map(Result::ok)
.collect(); .collect();
let hashmaps: Vec<HashMap<String, TrackMetadata>> = entries let tracks: Arc<Mutex<HashMap<String, TrackMetadata>>> = Arc::new(Mutex::new(HashMap::new()));
.par_iter() let mut set = JoinSet::new();
.fold(
|| HashMap::new(),
|mut acc: HashMap<String, TrackMetadata>, entry| {
if entry.file_type().is_file()
&& entry.path().extension().is_some_and(|ext| ext == "mp3")
{
let file_path = entry.path();
let content = fs::read(file_path).unwrap();
for entry in entries.into_iter() {
if entry.file_type().is_file() && entry.path().extension().is_some_and(|ext| ext == "mp3") {
let tracks = tracks.clone();
set.spawn(async move {
let file_path = entry.path();
if let Ok(content) = std::fs::read(file_path) {
let hash = generate_hash(&content); let hash = generate_hash(&content);
let relative_path = let relative_path =
file_path.to_str().unwrap().to_string()[path_offset..].to_string(); file_path.to_str().unwrap().to_string()[path_offset..].to_string();
if let Some(metadata) = extract_track_data(content, relative_path) { if let Some(metadata) = extract_track_data(content, relative_path) {
acc.insert(hash, metadata); tracks.lock().await.insert(hash, metadata);
} }
} }
});
acc }
},
)
.collect();
let mut tracks = HashMap::<String, TrackMetadata>::new();
for tracks_chunk in hashmaps {
tracks.extend(tracks_chunk);
} }
set.join_all().await;
let elapsed = now.elapsed(); let elapsed = now.elapsed();
println!("indexing took {:.2?}", elapsed); println!("indexing took {:.2?}", elapsed);
let now = Instant::now(); let now = Instant::now();
insert_tracks(db, tracks, path_id)?; insert_tracks(db, Arc::try_unwrap(tracks).unwrap().into_inner(), path_id).await?;
let elapsed = now.elapsed(); let elapsed = now.elapsed();

View File

@@ -8,6 +8,7 @@ use proto::player::player_server::PlayerServer;
use proto::settings::settings_server::SettingsServer; use proto::settings::settings_server::SettingsServer;
use rodio::{OutputStream, Sink}; use rodio::{OutputStream, Sink};
use state::{GrooveState, GrooveStateData}; use state::{GrooveState, GrooveStateData};
use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tonic::transport::Server; use tonic::transport::Server;
@@ -20,6 +21,7 @@ pub mod player;
pub mod settings; pub mod settings;
pub mod state; pub mod state;
use music::player::start_watching;
use settings::SettingsService; use settings::SettingsService;
pub mod proto { pub mod proto {
@@ -38,9 +40,9 @@ pub mod proto {
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pool = &mut establish_connection(); let pool = &mut establish_connection();
let connection = pool.get().unwrap(); initialize_database(&pool)
.await
initialize_database(&connection).expect("Error initializing database"); .expect("Error initializing database");
let address = "[::1]:39993".parse()?; let address = "[::1]:39993".parse()?;
@@ -48,9 +50,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
OutputStream::try_default().expect("Error getting audio output stream"); OutputStream::try_default().expect("Error getting audio output stream");
let sink = Sink::try_new(&stream_handle).expect("Error getting audio sink"); let sink = Sink::try_new(&stream_handle).expect("Error getting audio sink");
let player = AudioPlayer::new(sink); let player = Arc::new(Mutex::new(AudioPlayer::new(sink)));
let c_player = player.clone();
let state = GrooveState::new(Mutex::new(GrooveStateData::new(player))); let (watch_handle, mut rx) = start_watching(player.clone());
let player_rx_handle = tokio::spawn(async move {
while let Some(next) = rx.recv().await {
let mut player = player.lock().await;
match next {
Some(queued_track) => {
let _ = player.play_track(queued_track.track, queued_track.path, false);
}
None => {
player.clear();
}
}
}
});
let state = GrooveState::new(Mutex::new(GrooveStateData::new(c_player)));
let settings = SettingsService::new(state.clone(), pool.clone()); let settings = SettingsService::new(state.clone(), pool.clone());
let library = LibraryService::new(state.clone(), pool.clone()); let library = LibraryService::new(state.clone(), pool.clone());
@@ -72,6 +91,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?; .await?;
let _ = cover_server_handle.await; let _ = cover_server_handle.await;
let _ = watch_handle.await;
let _ = player_rx_handle.await;
Ok(()) Ok(())
} }

View File

@@ -25,6 +25,9 @@ pub struct CoverData {
pub mime_type: String, pub mime_type: String,
} }
const JPEG_SOI_AND_JFIF: &[u8; 5] = &[0xFF, 0xD8, 0xFF, 0xE0, 0x00];
const JPEG_RAW_HEADER_SEGMENT: &[u8; 8] = &[0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01];
pub fn extract_track_data(file: Vec<u8>, path: String) -> Option<TrackMetadata> { pub fn extract_track_data(file: Vec<u8>, path: String) -> Option<TrackMetadata> {
let probe = symphonia::default::get_probe(); let probe = symphonia::default::get_probe();
@@ -66,10 +69,10 @@ pub fn extract_track_data(file: Vec<u8>, path: String) -> Option<TrackMetadata>
for tag in current_metadata.tags() { for tag in current_metadata.tags() {
match tag.key.as_str() { match tag.key.as_str() {
"TIT2" => title = Some(tag.value.to_string()), "TIT2" | "TT2" => title = Some(tag.value.to_string()),
"TPE1" => artist = Some(tag.value.to_string()), "TPE1" | "TP1" => artist = Some(tag.value.to_string()),
"TPE2" => album_artist = Some(tag.value.to_string()), "TPE2" | "TP2" => album_artist = Some(tag.value.to_string()),
"TALB" => album = Some(tag.value.to_string()), "TALB" | "TAL" => album = Some(tag.value.to_string()),
_ => (), _ => (),
} }
} }
@@ -77,14 +80,33 @@ pub fn extract_track_data(file: Vec<u8>, path: String) -> Option<TrackMetadata>
let mut cover: Option<CoverData> = None; let mut cover: Option<CoverData> = None;
for visual in current_metadata.visuals() { for visual in current_metadata.visuals() {
let mime_type = visual.media_type.clone(); let mut mime_type = visual.media_type.clone();
let lower_mime_type = mime_type.to_lowercase();
if lower_mime_type == "png"
|| lower_mime_type == "webp"
|| lower_mime_type == "jpeg"
|| lower_mime_type == "jpg"
{
mime_type = format!("image/{lower_mime_type}");
}
if mime_type != "image/png" && mime_type != "image/jpeg" && mime_type != "image/webp" { if mime_type != "image/png" && mime_type != "image/jpeg" && mime_type != "image/webp" {
continue; continue;
} }
let data = if visual.data.starts_with(JPEG_RAW_HEADER_SEGMENT) {
let mut data = JPEG_SOI_AND_JFIF.to_vec();
data.append(&mut visual.data.to_vec());
data
} else {
visual.data.to_vec()
};
cover = Some(CoverData { cover = Some(CoverData {
bytes: visual.data.to_vec(), bytes: data,
mime_type, mime_type,
}); });
} }

View File

@@ -1,2 +1,3 @@
pub mod metadata; pub mod metadata;
pub mod player; pub mod player;
pub mod queue;

View File

@@ -1,40 +1,63 @@
use std::{ use std::{
collections::VecDeque,
fs, fs,
io::{BufReader, Cursor}, io::{BufReader, Cursor},
path::Path, path::{Path, PathBuf},
sync::Arc,
time::Duration, time::Duration,
}; };
use rodio::{Decoder, Sink}; use rodio::{Decoder, Sink};
use tokio::{
sync::{
mpsc::{self, Receiver},
Mutex,
},
task::JoinHandle,
};
use crate::{database::tracks::Track, proto::player::PlayerStatus}; use crate::{database::tracks::Track, proto::player::PlayerStatus};
use super::queue::QueuedTrack;
pub struct AudioPlayer { pub struct AudioPlayer {
sink: Sink, sink: Sink,
currently_playing: Option<Track>, currently_playing: Option<Track>,
queue: VecDeque<QueuedTrack>,
} }
impl AudioPlayer { impl AudioPlayer {
const WATCH_SLEEP_TIME: Duration = Duration::from_millis(20);
pub fn new(sink: Sink) -> Self { pub fn new(sink: Sink) -> Self {
sink.set_volume(0.5); sink.set_volume(0.5);
Self { Self {
sink, sink,
currently_playing: None, currently_playing: None,
queue: VecDeque::new(),
} }
} }
pub fn play_track<P>(&mut self, track: Track, path: P) -> Result<(), Box<dyn std::error::Error>> pub fn play_track<P>(
&mut self,
track: Track,
path: P,
clear_queue: bool,
) -> Result<(), Box<dyn std::error::Error>>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
self.currently_playing = Some(track);
self.sink.clear(); self.sink.clear();
if clear_queue {
self.queue.clear();
}
let file = BufReader::new(Cursor::new(fs::read(path)?)); let file = BufReader::new(Cursor::new(fs::read(path)?));
let source = Decoder::new(file)?; let source = Decoder::new(file)?;
self.currently_playing = Some(track);
self.sink.append(source); self.sink.append(source);
self.sink.play(); self.sink.play();
@@ -42,12 +65,100 @@ impl AudioPlayer {
Ok(()) Ok(())
} }
pub fn play_track_next(
&mut self,
track: Track,
path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
if self.sink.empty() && self.queue.is_empty() {
return self.play_track(track, path, false);
}
let queued_track = QueuedTrack::new(track, path.to_path_buf());
self.queue.push_front(queued_track);
Ok(())
}
pub async fn add_to_queue(
&mut self,
track: Track,
path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
if self.sink.empty() && self.queue.is_empty() {
return self.play_track(track, path, false);
}
let queued_track = QueuedTrack::new(track, path.to_path_buf());
self.queue.push_back(queued_track);
Ok(())
}
pub async fn add_tracks_to_queue(
&mut self,
tracks: Vec<(Track, PathBuf)>,
clear_queue: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if clear_queue {
self.clear();
}
for (track, path) in tracks {
self.queue.push_back(QueuedTrack { track, path });
}
if !self.sink.empty() || self.queue.is_empty() {
return Ok(());
}
if let Some(queued_track) = self.queue.pop_front() {
self.play_track(queued_track.track, queued_track.path, false)?;
};
Ok(())
}
pub fn skip_track(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let Some(queued_track) = self.queue.pop_front() else {
self.clear();
return Ok(());
};
self.play_track(queued_track.track, queued_track.path, false)
}
pub fn skip_to_queue_index(&mut self, index: usize) -> Result<(), Box<dyn std::error::Error>> {
let Some(queued_track) = self.queue.remove(index) else {
self.clear();
return Ok(());
};
self.play_track(queued_track.track, queued_track.path, false)
}
pub fn swap_queue_indices(&mut self, a: usize, b: usize) -> bool {
let len = self.queue.len();
if a >= len || b >= len {
return false;
}
self.queue.swap(a, b);
true
}
pub fn resume(&self) { pub fn resume(&self) {
self.sink.play(); self.sink.play();
} }
pub fn pause(&self) { pub fn pause(&self) {
self.sink.pause(); self.sink.pause()
} }
/// Toggles the player's pause state and returns the new value /// Toggles the player's pause state and returns the new value
@@ -85,22 +196,63 @@ impl AudioPlayer {
self.currently_playing.clone() self.currently_playing.clone()
} }
pub fn queue(&self) -> VecDeque<QueuedTrack> {
self.queue.clone()
}
pub fn clear(&mut self) {
self.queue.clear();
self.sink.clear();
self.currently_playing = None;
}
pub fn get_snapshot(&self) -> StatusSnapshot { pub fn get_snapshot(&self) -> StatusSnapshot {
StatusSnapshot { StatusSnapshot {
volume: self.volume(), volume: self.volume(),
position: self.position(), position: self.position(),
is_paused: self.is_paused(), is_paused: self.is_paused(),
currently_playing: self.currently_playing(), currently_playing: self.currently_playing(),
queue: self.queue(),
} }
} }
} }
#[derive(Debug, Clone, PartialEq)] pub fn start_watching(
player: Arc<Mutex<AudioPlayer>>,
) -> (JoinHandle<()>, Receiver<Option<QueuedTrack>>) {
let (tx, rx) = mpsc::channel::<Option<QueuedTrack>>(128);
(
tokio::spawn(async move {
loop {
{
let mut player = player.lock().await;
if let Some(current_track) = player.currently_playing() {
if player.sink.empty()
|| current_track.duration as u128 <= player.position()
{
if let Err(_) = tx.send(player.queue.pop_front()).await {
break;
}
}
}
}
tokio::time::sleep(AudioPlayer::WATCH_SLEEP_TIME).await;
}
}),
rx,
)
}
#[derive(Debug, Clone)]
pub struct StatusSnapshot { pub struct StatusSnapshot {
pub volume: f32, pub volume: f32,
pub position: u128, pub position: u128,
pub is_paused: bool, pub is_paused: bool,
pub currently_playing: Option<Track>, pub currently_playing: Option<Track>,
pub queue: VecDeque<QueuedTrack>,
} }
impl Into<PlayerStatus> for StatusSnapshot { impl Into<PlayerStatus> for StatusSnapshot {
@@ -110,6 +262,7 @@ impl Into<PlayerStatus> for StatusSnapshot {
is_paused: self.is_paused, is_paused: self.is_paused,
progress: self.position as u64, progress: self.position as u64,
currently_playing: self.currently_playing.clone().map(|t| t.into()), currently_playing: self.currently_playing.clone().map(|t| t.into()),
queue: Vec::from_iter(self.queue.into_iter().map(|t| t.track.into())),
} }
} }
} }

28
src/music/queue.rs Normal file
View File

@@ -0,0 +1,28 @@
use std::{collections::VecDeque, path::PathBuf};
use crate::{database::tracks::Track, proto};
#[derive(Debug, Clone)]
pub struct QueuedTrack {
pub track: Track,
pub path: PathBuf,
}
impl QueuedTrack {
pub fn new(track: Track, path: PathBuf) -> Self {
Self { track, path }
}
}
impl From<QueuedTrack> for Track {
fn from(value: QueuedTrack) -> Track {
value.track
}
}
pub fn queue_to_track_vec(queue: VecDeque<QueuedTrack>) -> Vec<proto::library::Track> {
queue
.into_iter()
.map(|queued_track| Track::from(queued_track).into())
.collect()
}

View File

@@ -1,16 +1,21 @@
use r2d2::Pool; use deadpool_sqlite::Pool;
use r2d2_sqlite::SqliteConnectionManager; use std::{path::PathBuf, pin::Pin, time::Duration};
use std::{pin::Pin, time::Duration};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_stream::{wrappers::ReceiverStream, Stream}; use tokio_stream::{wrappers::ReceiverStream, Stream};
use crate::{ use crate::{
database::tracks::{get_track, get_track_full_path}, database::tracks::{
get_playlist, get_specific_tracks, get_specific_tracks_full_path, get_track,
get_track_full_path, Track,
},
music::queue::queue_to_track_vec,
proto::{ proto::{
self, self,
player::{ player::{
player_server::Player, PauseState, PlayTrackRequest, PlayTrackResponse, PlayerStatus, player_server::Player, PauseState, PlayPlaylistRequest, PlayTrackResponse,
SeekPositionRequest, SeekPositionResponse, SetVolumeRequest, SetVolumeResponse, PlayerStatus, Queue, SeekPositionRequest, SeekPositionResponse, SetVolumeRequest,
SetVolumeResponse, SkipToQueueIndexRequest, SwapQueueIndicesRequest, TrackRequest,
TracksRequest,
}, },
}, },
state::GrooveState, state::GrooveState,
@@ -18,11 +23,11 @@ use crate::{
pub struct PlayerService { pub struct PlayerService {
state: GrooveState, state: GrooveState,
pool: Pool<SqliteConnectionManager>, pool: Pool,
} }
impl PlayerService { impl PlayerService {
pub fn new(state: GrooveState, pool: Pool<SqliteConnectionManager>) -> Self { pub fn new(state: GrooveState, pool: Pool) -> Self {
Self { state, pool } Self { state, pool }
} }
} }
@@ -33,25 +38,23 @@ impl Player for PlayerService {
async fn play_track( async fn play_track(
&self, &self,
request: tonic::Request<PlayTrackRequest>, request: tonic::Request<TrackRequest>,
) -> Result<tonic::Response<PlayTrackResponse>, tonic::Status> { ) -> Result<tonic::Response<PlayTrackResponse>, tonic::Status> {
let Ok(db) = self.pool.get() else {
return Err(tonic::Status::internal(""));
};
let input = request.get_ref(); let input = request.get_ref();
let Ok(track) = get_track(&db, input.hash.as_str()) else { let Ok(track) = get_track(&self.pool, input.hash.as_str()).await else {
return Err(tonic::Status::not_found("")); return Err(tonic::Status::not_found("Could not get track"));
}; };
let Ok(track_path) = get_track_full_path(&db, input.hash.as_str()) else { let Ok(track_path) = get_track_full_path(&self.pool, input.hash.as_str()).await else {
return Err(tonic::Status::not_found("")); return Err(tonic::Status::not_found("Could not get track file path"));
}; };
let mut state = self.state.lock().await; let state = self.state.lock().await;
let _ = state.player.play_track(track.clone(), track_path); let player = &mut state.player.lock().await;
let _ = player.play_track(track.clone(), track_path, true);
let response = PlayTrackResponse { let response = PlayTrackResponse {
track: Some(proto::library::Track::from(track.into())), track: Some(proto::library::Track::from(track.into())),
@@ -63,11 +66,11 @@ impl Player for PlayerService {
async fn resume_track( async fn resume_track(
&self, &self,
_request: tonic::Request<()>, _: tonic::Request<()>,
) -> Result<tonic::Response<PauseState>, tonic::Status> { ) -> Result<tonic::Response<PauseState>, tonic::Status> {
let state = self.state.lock().await; let state = self.state.lock().await;
state.player.resume(); state.player.lock().await.resume();
let response = PauseState { is_paused: false }; let response = PauseState { is_paused: false };
@@ -76,11 +79,11 @@ impl Player for PlayerService {
async fn pause_track( async fn pause_track(
&self, &self,
_request: tonic::Request<()>, _: tonic::Request<()>,
) -> Result<tonic::Response<PauseState>, tonic::Status> { ) -> Result<tonic::Response<PauseState>, tonic::Status> {
let state = self.state.lock().await; let state = self.state.lock().await;
state.player.pause(); state.player.lock().await.pause();
let response = PauseState { is_paused: true }; let response = PauseState { is_paused: true };
@@ -89,11 +92,11 @@ impl Player for PlayerService {
async fn toggle_pause( async fn toggle_pause(
&self, &self,
_request: tonic::Request<()>, _: tonic::Request<()>,
) -> Result<tonic::Response<PauseState>, tonic::Status> { ) -> Result<tonic::Response<PauseState>, tonic::Status> {
let state = self.state.lock().await; let state = self.state.lock().await;
let is_paused = state.player.toggle_pause(); let is_paused = state.player.lock().await.toggle_pause();
let response = PauseState { is_paused }; let response = PauseState { is_paused };
@@ -107,7 +110,7 @@ impl Player for PlayerService {
let input = request.get_ref(); let input = request.get_ref();
let state = self.state.lock().await; let state = self.state.lock().await;
state.player.set_position(input.position); state.player.lock().await.set_position(input.position);
let response = SeekPositionResponse { let response = SeekPositionResponse {
position: input.position, position: input.position,
@@ -123,7 +126,7 @@ impl Player for PlayerService {
let input = request.get_ref(); let input = request.get_ref();
let state = self.state.lock().await; let state = self.state.lock().await;
state.player.set_volume(input.volume); state.player.lock().await.set_volume(input.volume);
let response = SetVolumeResponse { let response = SetVolumeResponse {
volume: input.volume, volume: input.volume,
@@ -134,7 +137,7 @@ impl Player for PlayerService {
async fn get_status( async fn get_status(
&self, &self,
_request: tonic::Request<()>, _: tonic::Request<()>,
) -> Result<tonic::Response<Self::GetStatusStream>, tonic::Status> { ) -> Result<tonic::Response<Self::GetStatusStream>, tonic::Status> {
let state = self.state.clone(); let state = self.state.clone();
@@ -145,7 +148,14 @@ impl Player for PlayerService {
tokio::spawn(async move { tokio::spawn(async move {
while !tx.is_closed() { while !tx.is_closed() {
if let Err(_) = tx if let Err(_) = tx
.send(Ok(state.lock().await.player.get_snapshot().into())) .send(Ok(state
.lock()
.await
.player
.lock()
.await
.get_snapshot()
.into()))
.await .await
{ {
break; break;
@@ -161,4 +171,205 @@ impl Player for PlayerService {
Ok(tonic::Response::new(response)) Ok(tonic::Response::new(response))
} }
async fn add_track_to_queue(
&self,
request: tonic::Request<TrackRequest>,
) -> Result<tonic::Response<Queue>, tonic::Status> {
let input = request.get_ref();
let Ok(track) = get_track(&self.pool, input.hash.as_str()).await else {
return Err(tonic::Status::not_found(""));
};
let Ok(track_path) = get_track_full_path(&self.pool, input.hash.as_str()).await else {
return Err(tonic::Status::not_found(""));
};
let state = self.state.lock().await;
let mut player = state.player.lock().await;
if let Err(_) = player.add_to_queue(track, &track_path).await {
return Err(tonic::Status::internal(""));
}
let queue = player.queue();
let response = Queue {
tracks: queue_to_track_vec(queue),
};
Ok(tonic::Response::new(response))
}
async fn add_tracks_to_queue(
&self,
request: tonic::Request<TracksRequest>,
) -> Result<tonic::Response<Queue>, tonic::Status> {
let input = request.get_ref();
let Ok(tracks) = get_specific_tracks(&self.pool, &input.tracks).await else {
return Err(tonic::Status::not_found(""));
};
let Ok(track_paths) = get_specific_tracks_full_path(&self.pool, &input.tracks).await else {
return Err(tonic::Status::not_found(""));
};
let state = self.state.lock().await;
let mut player = state.player.lock().await;
let tracks_and_paths: Vec<(Track, PathBuf)> =
tracks.into_iter().zip(track_paths.into_iter()).collect();
if let Err(_) = player.add_tracks_to_queue(tracks_and_paths, false).await {
return Err(tonic::Status::internal(""));
}
let queue = player.queue();
let response = Queue {
tracks: queue_to_track_vec(queue),
};
Ok(tonic::Response::new(response))
}
async fn play_playlist(
&self,
request: tonic::Request<PlayPlaylistRequest>,
) -> Result<tonic::Response<PlayerStatus>, tonic::Status> {
let input = request.get_ref();
let Ok(playlist) = get_playlist(&self.pool, input.id).await else {
return Err(tonic::Status::not_found(""));
};
let Ok(track_paths) = get_specific_tracks_full_path(
&self.pool,
&playlist.tracks.iter().map(|t| t.hash.clone()).collect(),
)
.await
else {
return Err(tonic::Status::not_found(""));
};
let state = self.state.lock().await;
let mut player = state.player.lock().await;
let tracks = playlist.tracks.iter().map(|t| t.clone().into());
let tracks_and_paths: Vec<(Track, PathBuf)> = tracks
.into_iter()
.zip(track_paths.into_iter())
.skip(input.starting_rank.unwrap_or(0) as usize)
.collect();
if let Err(_) = player.add_tracks_to_queue(tracks_and_paths, true).await {
return Err(tonic::Status::internal(""));
}
let response = player.get_snapshot().into();
Ok(tonic::Response::new(response))
}
async fn play_track_next(
&self,
request: tonic::Request<TrackRequest>,
) -> Result<tonic::Response<Queue>, tonic::Status> {
let input = request.get_ref();
let Ok(track) = get_track(&self.pool, input.hash.as_str()).await else {
return Err(tonic::Status::not_found(""));
};
let Ok(track_path) = get_track_full_path(&self.pool, input.hash.as_str()).await else {
return Err(tonic::Status::not_found(""));
};
let state = self.state.lock().await;
let mut player = state.player.lock().await;
if let Err(_) = player.play_track_next(track, &track_path) {
return Err(tonic::Status::internal(""));
}
let queue = player.queue();
let response = Queue {
tracks: queue_to_track_vec(queue),
};
Ok(tonic::Response::new(response))
}
async fn skip_track(
&self,
_: tonic::Request<()>,
) -> Result<tonic::Response<PlayerStatus>, tonic::Status> {
let state = self.state.lock().await;
let mut player = state.player.lock().await;
if let Err(_) = player.skip_track() {
return Err(tonic::Status::internal(""));
};
let response = player.get_snapshot().into();
Ok(tonic::Response::new(response))
}
async fn skip_to_queue_index(
&self,
request: tonic::Request<SkipToQueueIndexRequest>,
) -> Result<tonic::Response<PlayerStatus>, tonic::Status> {
let input = request.get_ref();
let Ok(index) = input.index.try_into() else {
return Err(tonic::Status::internal(""));
};
let state = self.state.lock().await;
let mut player = state.player.lock().await;
if let Err(_) = player.skip_to_queue_index(index) {
return Err(tonic::Status::internal(""));
};
let response = player.get_snapshot().into();
Ok(tonic::Response::new(response))
}
async fn swap_queue_indices(
&self,
request: tonic::Request<SwapQueueIndicesRequest>,
) -> Result<tonic::Response<Queue>, tonic::Status> {
let input = request.get_ref();
let Ok(a) = input.a.try_into() else {
return Err(tonic::Status::internal(""));
};
let Ok(b) = input.b.try_into() else {
return Err(tonic::Status::internal(""));
};
let state = self.state.lock().await;
let mut player = state.player.lock().await;
if !player.swap_queue_indices(a, b) {
return Err(tonic::Status::internal(""));
};
let queue = player.queue();
let response = Queue {
tracks: queue_to_track_vec(queue),
};
Ok(tonic::Response::new(response))
}
} }

View File

@@ -9,18 +9,17 @@ use crate::proto::settings::{
}; };
use crate::state::GrooveState; use crate::state::GrooveState;
use deadpool_sqlite::Pool;
use proto::settings::{settings_server::Settings, LibraryPath, SettingsData}; use proto::settings::{settings_server::Settings, LibraryPath, SettingsData};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
pub struct SettingsService { pub struct SettingsService {
#[allow(dead_code)] #[allow(dead_code)]
state: GrooveState, state: GrooveState,
pool: Pool<SqliteConnectionManager>, pool: Pool,
} }
impl SettingsService { impl SettingsService {
pub fn new(state: GrooveState, pool: Pool<SqliteConnectionManager>) -> Self { pub fn new(state: GrooveState, pool: Pool) -> Self {
Self { state, pool } Self { state, pool }
} }
} }
@@ -31,11 +30,7 @@ impl Settings for SettingsService {
&self, &self,
_request: tonic::Request<()>, _request: tonic::Request<()>,
) -> Result<tonic::Response<SettingsData>, tonic::Status> { ) -> Result<tonic::Response<SettingsData>, tonic::Status> {
let Ok(db) = self.pool.get() else { let Ok(library_paths) = get_library_paths(&self.pool).await else {
return Err(tonic::Status::internal(""));
};
let Ok(library_paths) = get_library_paths(&db) else {
return Err(tonic::Status::internal("")); return Err(tonic::Status::internal(""));
}; };
@@ -58,11 +53,7 @@ impl Settings for SettingsService {
) -> Result<tonic::Response<AddPathResponse>, tonic::Status> { ) -> Result<tonic::Response<AddPathResponse>, tonic::Status> {
let input = request.into_inner(); let input = request.into_inner();
let Ok(db) = self.pool.get() else { let Ok(insert_result) = insert_library_path(&self.pool, input.path).await else {
return Err(tonic::Status::internal(""));
};
let Ok(insert_result) = insert_library_path(&db, input.path) else {
return Err(tonic::Status::internal("")); return Err(tonic::Status::internal(""));
}; };
@@ -81,11 +72,7 @@ impl Settings for SettingsService {
) -> Result<tonic::Response<DeletePathResponse>, tonic::Status> { ) -> Result<tonic::Response<DeletePathResponse>, tonic::Status> {
let input = request.into_inner(); let input = request.into_inner();
let Ok(db) = self.pool.get() else { let Ok(delete_result) = delete_library_path(&self.pool, input.id).await else {
return Err(tonic::Status::internal(""));
};
let Ok(delete_result) = delete_library_path(&db, input.id) else {
return Err(tonic::Status::internal("")); return Err(tonic::Status::internal(""));
}; };
@@ -104,15 +91,11 @@ impl Settings for SettingsService {
) -> Result<tonic::Response<RefreshPathResponse>, tonic::Status> { ) -> Result<tonic::Response<RefreshPathResponse>, tonic::Status> {
let input = request.into_inner(); let input = request.into_inner();
let Ok(db) = self.pool.get() else { let Ok(library_path) = get_library_path(&self.pool, input.id).await else {
return Err(tonic::Status::internal(""));
};
let Ok(library_path) = get_library_path(&db, input.id) else {
return Err(tonic::Status::not_found("")); return Err(tonic::Status::not_found(""));
}; };
let _ = index_path(library_path.path.into(), db, library_path.id); let _ = index_path(library_path.path.into(), &self.pool, library_path.id).await;
let response = RefreshPathResponse {}; let response = RefreshPathResponse {};

View File

@@ -7,11 +7,11 @@ use crate::music::player::AudioPlayer;
pub type GrooveState = Arc<Mutex<GrooveStateData>>; pub type GrooveState = Arc<Mutex<GrooveStateData>>;
pub struct GrooveStateData { pub struct GrooveStateData {
pub player: AudioPlayer, pub player: Arc<Mutex<AudioPlayer>>,
} }
impl GrooveStateData { impl GrooveStateData {
pub fn new(player: AudioPlayer) -> Self { pub fn new(player: Arc<Mutex<AudioPlayer>>) -> Self {
Self { player } Self { player }
} }
} }