From a09a7c8976882e75c042d74ade770fead2cb35e2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 18 Jul 2026 00:14:35 +0600 Subject: [PATCH] feat: integrate Uniffi for audio metadata reading and writing with Lofty library --- .github/workflows/spotube-release-binary.yml | 12 + .gitignore | 4 +- build.gradle.kts | 2 + composeApp/Cargo.lock | 723 ++++++++++++++++++ composeApp/Cargo.toml | 12 + composeApp/build.gradle.kts | 11 + .../modules/downloads/DownloadManager.kt | 49 +- composeApp/src/commonMain/rust/lib.rs | 160 ++++ .../media/JvmLocalMediaDiscoveryService.kt | 23 +- gradle/libs.versions.toml | 7 +- 10 files changed, 977 insertions(+), 26 deletions(-) create mode 100644 composeApp/Cargo.lock create mode 100644 composeApp/Cargo.toml create mode 100644 composeApp/src/commonMain/rust/lib.rs diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 6c63bae5..2d0f771c 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -112,6 +112,9 @@ jobs: distribution: temurin java-version: 21 + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 @@ -170,6 +173,9 @@ jobs: distribution: temurin java-version: 21 + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 @@ -231,6 +237,9 @@ jobs: distribution: temurin java-version: 21 + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 @@ -268,6 +277,9 @@ jobs: distribution: temurin java-version: 21 + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 diff --git a/.gitignore b/.gitignore index cfc7da21..68c4954a 100644 --- a/.gitignore +++ b/.gitignore @@ -112,4 +112,6 @@ android/app/.cxx /node_modules /package.json /package-lock.json -/bun.lock \ No newline at end of file +/bun.lock + +**/target/ \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index eb5958c7..bd0d4631 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,4 +30,6 @@ plugins { alias(libs.plugins.zipline.gradle.plugin) apply false alias(libs.plugins.spotubeGradle) apply false alias(libs.plugins.vlcjBundler) apply false + alias(libs.plugins.uniffi) apply false + alias(libs.plugins.cargo) apply false } \ No newline at end of file diff --git a/composeApp/Cargo.lock b/composeApp/Cargo.lock new file mode 100644 index 00000000..1e6bd60d --- /dev/null +++ b/composeApp/Cargo.lock @@ -0,0 +1,723 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "compose-app" +version = "0.1.0" +dependencies = [ + "lofty", + "uniffi", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lofty" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec4feeff6c7d75093278133a06e827d7af6d2bfe20b0f331f9d10338a5ec7ca" +dependencies = [ + "byteorder", + "data-encoding", + "flate2", + "lofty_attr", + "log", + "ogg_pager", + "paste", +] + +[[package]] +name = "lofty_attr" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ogg_pager" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d36b1d6964c3ac92b7aea701057e02b6b91143d70d83b20abf75a231a3c0216" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "uniffi" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +dependencies = [ + "anyhow", + "cargo_metadata", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/composeApp/Cargo.toml b/composeApp/Cargo.toml new file mode 100644 index 00000000..8617b0f7 --- /dev/null +++ b/composeApp/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "compose-app" +version = "0.1.0" +edition = "2024" + +[dependencies] +uniffi = "0.29.4" +lofty = "0.24.0" + +[lib] +crate-type = ["cdylib", "staticlib"] +path = "src/commonMain/rust/lib.rs" \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 5b0e6c41..a2dc6e7f 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -15,6 +15,8 @@ * along with this program. If not, see . */ +import gobley.gradle.GobleyHost +import gobley.gradle.cargo.dsl.jvm import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.reload.gradle.ComposeHotRun import org.jetbrains.kotlin.gradle.dsl.JvmTarget @@ -30,6 +32,9 @@ plugins { alias(libs.plugins.kotlinSerialization) alias(libs.plugins.zipline.gradle.plugin) alias(libs.plugins.vlcjBundler) + alias(libs.plugins.uniffi) + alias(libs.plugins.cargo) + kotlin("plugin.atomicfu") version libs.versions.kotlin } vlcjBundler { @@ -334,3 +339,9 @@ tasks.withType().configureEach { project.layout.buildDirectory.dir("compose/tmp/prepareAppResources").get() ) } + +cargo { + builds.jvm { + embedRustLibrary = (rustTarget == GobleyHost.current.rustTarget) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadManager.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadManager.kt index f00da582..cc1aea59 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadManager.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadManager.kt @@ -392,25 +392,42 @@ class DownloadManager( } } - private fun applyMetadata(item: DownloadItem, filename: String, downloadDir: Path) { + private suspend fun applyMetadata(item: DownloadItem, filename: String, downloadDir: Path) { val filePath = downloadDir / filename.toPath() if (!fileSystem.exists(filePath)) return - // TODO: Write metadata/audio tags to the downloaded file. - // This is the dedicated placeholder for metadata tagging. - // Implementation should use a platform-specific audio tagging library - // to write ID3 tags (MP3), MP4 atoms (M4A/AAC), Vorbis comments (OGG/FLAC), etc. - // - // Tags to write from the track metadata: - // - Title: item.title - // - Artist: item.artists - // - Album: item.album - // - Track number: item.track?.trackNumber - // - Disc number: item.track?.discNumber - // - Duration: item.track?.durationMs - // - Album art: item.track?.thumbnails (download and embed) - // - ISRC: item.track?.isrcCode - // - External URI: item.track?.externalUri + val track = item.track ?: return + + val coverBytes = try { + val thumbnail = track.thumbnails?.maxByOrNull { it.width * it.height } + if (thumbnail != null) { + val response = httpClient.get(thumbnail.url) + val channel = response.bodyAsChannel() + val packet = channel.readRemaining() + val bytes = packet.readByteArray() + packet.close() + bytes + } else { + null + } + } catch (e: Exception) { + logger.w(e) { "Failed to download cover art for ${item.title}" } + null + } + + try { + uniffi.compose_app.writeAudioMetadata( + filePath = filePath.toString(), + title = item.title, + artists = item.artists, + album = item.album, + trackNumber = track.trackNumber, + discNumber = track.discNumber, + coverBytes = coverBytes, + ) + } catch (e: Exception) { + logger.w(e) { "Failed to write audio metadata for ${item.title}" } + } } private fun resolveDownloadDir(): Path { diff --git a/composeApp/src/commonMain/rust/lib.rs b/composeApp/src/commonMain/rust/lib.rs new file mode 100644 index 00000000..8700d014 --- /dev/null +++ b/composeApp/src/commonMain/rust/lib.rs @@ -0,0 +1,160 @@ +use lofty::config::WriteOptions; +use lofty::file::{AudioFile, FileType, TaggedFileExt}; +use lofty::picture::Picture; +use lofty::probe::Probe; +use lofty::tag::{Accessor, Tag, TagExt}; +use std::path::Path; + +#[derive(uniffi::Record)] +pub struct AudioMetadata { + pub title: Option, + pub artists: Vec, + pub album: Option, + pub duration_ms: i64, + pub track_number: Option, + pub disc_number: Option, + pub cover_bytes: Option>, +} + +#[derive(Debug, uniffi::Error)] +pub enum AudioTagError { + FileNotFound, + ReadError { reason: String }, + WriteError { reason: String }, +} + +impl std::fmt::Display for AudioTagError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AudioTagError::FileNotFound => write!(f, "File not found"), + AudioTagError::ReadError { reason } => write!(f, "Read error: {}", reason), + AudioTagError::WriteError { reason } => write!(f, "Write error: {}", reason), + } + } +} + +#[uniffi::export] +fn read_audio_metadata(file_path: String) -> Result { + let path = Path::new(&file_path); + if !path.exists() { + return Err(AudioTagError::FileNotFound); + } + + let tagged_file = Probe::open(path) + .map_err(|e| AudioTagError::ReadError { reason: e.to_string() })? + .read() + .map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?; + + let duration = tagged_file.properties().duration(); + let duration_ms = duration.as_millis() as i64; + + let tag = tagged_file.primary_tag().or_else(|| tagged_file.first_tag()); + + let mut title = None; + let mut artists = Vec::new(); + let mut album = None; + let mut track_number = None; + let mut disc_number = None; + let mut cover_bytes = None; + + if let Some(tag) = tag { + title = tag.title().map(|s| s.to_string()); + + if let Some(artist) = tag.artist() { + artists = artist + .split(';') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + + album = tag.album().map(|s| s.to_string()); + track_number = tag.track(); + disc_number = tag.disk(); + + if let Some(pic) = tag.pictures().first() { + cover_bytes = Some(pic.data().to_vec()); + } + } + + Ok(AudioMetadata { + title, + artists, + album, + duration_ms, + track_number, + disc_number, + cover_bytes, + }) +} + +#[uniffi::export] +fn write_audio_metadata( + file_path: String, + title: String, + artists: String, + album: Option, + track_number: Option, + disc_number: Option, + cover_bytes: Option>, +) -> Result<(), AudioTagError> { + let path = Path::new(&file_path); + if !path.exists() { + return Err(AudioTagError::FileNotFound); + } + + let mut tagged_file = Probe::open(path) + .map_err(|e| AudioTagError::ReadError { reason: e.to_string() })? + .read() + .map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?; + + let file_type = tagged_file.file_type(); + + if tagged_file.primary_tag().is_none() && tagged_file.first_tag().is_none() { + let new_tag = match file_type { + FileType::Mpeg => Tag::new(lofty::tag::TagType::Id3v2), + FileType::Mp4 => Tag::new(lofty::tag::TagType::Mp4Ilst), + FileType::Flac => Tag::new(lofty::tag::TagType::VorbisComments), + FileType::Opus | FileType::Vorbis => Tag::new(lofty::tag::TagType::VorbisComments), + _ => Tag::new(lofty::tag::TagType::Id3v2), + }; + tagged_file.insert_tag(new_tag); + } + + let tag = if tagged_file.primary_tag().is_some() { + tagged_file.primary_tag_mut().unwrap() + } else if tagged_file.first_tag().is_some() { + tagged_file.first_tag_mut().unwrap() + } else { + unreachable!() + }; + + tag.set_title(title); + tag.set_artist(artists); + + if let Some(album_name) = album { + tag.set_album(album_name); + } + + if let Some(num) = track_number { + tag.set_track(num as u32); + } + + if let Some(num) = disc_number { + tag.set_disk(num as u32); + } + + if let Some(bytes) = cover_bytes { + if !bytes.is_empty() { + let picture = Picture::unchecked(bytes).build(); + tag.push_picture(picture); + } + } + + tag.save_to_path(path, WriteOptions::default()) + .map_err(|e| AudioTagError::WriteError { reason: e.to_string() })?; + + Ok(()) +} + +uniffi::setup_scaffolding!(); diff --git a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/modules/library/local_tracks/media/JvmLocalMediaDiscoveryService.kt b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/modules/library/local_tracks/media/JvmLocalMediaDiscoveryService.kt index 16718db9..fa2b1cf5 100644 --- a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/modules/library/local_tracks/media/JvmLocalMediaDiscoveryService.kt +++ b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/modules/library/local_tracks/media/JvmLocalMediaDiscoveryService.kt @@ -54,16 +54,23 @@ class JvmLocalMediaDiscoveryService : LocalMediaDiscoveryService { if (scanned.incrementAndGet() > MAX_FILES_PER_SCAN) return@forEach val parent = filePath.parent?.absolutePathString() ?: return@forEach val trackPath = filePath.toAbsolutePath().toString() - println("Debug: Found track path = $trackPath") + + val metadata = try { + uniffi.compose_app.readAudioMetadata(trackPath) + } catch (_: Exception) { + null + } + val track = LocalMediaTrack( path = trackPath, - name = filePath.nameWithoutExtension.ifBlank { - filePath.fileName.toString() - }, - artists = emptyList(), - durationMs = 0L, - album = null, - coverBytes = null, + name = metadata?.title?.takeIf { it.isNotBlank() } + ?: filePath.nameWithoutExtension.ifBlank { + filePath.fileName.toString() + }, + artists = metadata?.artists?.takeIf { it.isNotEmpty() } ?: emptyList(), + durationMs = metadata?.durationMs ?: 0L, + album = metadata?.album, + coverBytes = metadata?.coverBytes, ) folders.getOrPut(parent) { mutableListOf() }.add(track) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99253336..3f71336f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,6 +72,8 @@ spotube-gradle = "0.1.0" cryptography = "0.6.0" reorderable = "3.1.0" vlcjBundler = "0.1.0" +uniffi = "0.3.7" +kotlinx-atomicfu = "0.33.0" [libraries] appdirs = { module = "net.harawata:appdirs", version.ref = "appdirs" } @@ -174,4 +176,7 @@ androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.lib zipline-gradle-plugin = { id = "app.cash.zipline", version.ref = "ziplineVersion" } mavenPublish = { id = "maven-publish" } spotubeGradle = { id = "dev.krtirtho.spotube.gradle-plugin", version.ref = "spotube-gradle" } -vlcjBundler = { id = "dev.krtirtho.vlcj-bundler.gradle-plugin", version.ref = "vlcjBundler" } \ No newline at end of file +vlcjBundler = { id = "dev.krtirtho.vlcj-bundler.gradle-plugin", version.ref = "vlcjBundler" } +kotlinxAtomicFu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "kotlinx-atomicfu" } +uniffi = { id = "dev.gobley.uniffi", version.ref = "uniffi" } +cargo = { id = "dev.gobley.cargo", version.ref = "uniffi" } \ No newline at end of file