diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf79eff..30a7754 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,13 @@ jobs: - name: Setup Rust cache uses: Swatinem/rust-cache@v2 - + + - name: Setup yt-dlp PPA + run: sudo add-apt-repository ppa:tomtomtom/yt-dlp + + - name: Install yt-dlp + run: sudo apt update && sudo apt install -y yt-dlp + - name: Run cargo test run: cargo test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa105cf..cd57883 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: target: aarch64-unknown-linux-gnu cargo_cmd: cross - build: x86_64-apple - os: macos-14-large + os: macos-15-intel rust: stable target: x86_64-apple-darwin cargo_cmd: cargo diff --git a/Cargo.lock b/Cargo.lock index a8a6a1c..e24ac88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,9 +16,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -29,12 +29,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -46,9 +40,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -61,9 +55,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -76,41 +70,44 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -151,15 +148,15 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.3" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitpacking" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" +checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019" dependencies = [ "crunchy", ] @@ -175,9 +172,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.6.4" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61138465baf186c63e8d9b6b613b508cd832cba4ce93cf37ce5f096f91ac1a6" +checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83" dependencies = [ "bon-macros", "rustversion", @@ -185,11 +182,11 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.6.4" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40d1dad34aa19bf02295382f08d9bc40651585bd497266831d40ee6296fb49ca" +checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0" dependencies = [ - "darling", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -212,9 +209,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byteorder" @@ -237,10 +234,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.29" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -254,9 +252,9 @@ checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -266,11 +264,10 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -281,9 +278,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.46" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -291,9 +288,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.46" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", @@ -303,9 +300,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -315,15 +312,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clipboard-win" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ "error-code", ] @@ -336,9 +333,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -378,9 +375,9 @@ dependencies = [ [[package]] name = "crokey" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5282b45c96c5978c8723ea83385cb9a488b64b7d175733f48d07bf9da514a863" +checksum = "04a63daf06a168535c74ab97cdba3ed4fa5d4f32cb36e437dcceb83d66854b7c" dependencies = [ "crokey-proc_macros", "crossterm", @@ -391,9 +388,9 @@ dependencies = [ [[package]] name = "crokey-proc_macros" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ea0218d3fedf0797fa55676f1964ef5d27103d41ed0281b4bbd2a6e6c3d8d28" +checksum = "847f11a14855fc490bd5d059821895c53e77eeb3c2b73ee3dded7ce77c93b231" dependencies = [ "crossterm", "proc-macro2", @@ -493,9 +490,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -507,8 +504,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -525,25 +532,49 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn", ] [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -561,7 +592,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn", @@ -579,22 +610,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", + "rustc_version", "syn", ] @@ -621,18 +653,18 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] [[package]] name = "downcast-rs" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "either" @@ -654,19 +686,19 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "error-code" -version = "3.2.0" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "fallible-iterator" @@ -703,6 +735,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -748,15 +786,15 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -765,21 +803,20 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", - "pin-utils", "slab", ] @@ -795,32 +832,45 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", ] [[package]] name = "git2" -version = "0.20.2" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ "bitflags", "libc", @@ -842,13 +892,19 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -865,11 +921,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -889,9 +945,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -913,9 +969,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -926,9 +982,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -939,11 +995,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -954,42 +1009,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -997,6 +1048,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1026,25 +1083,30 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -1057,25 +1119,25 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1083,9 +1145,9 @@ dependencies = [ [[package]] name = "lazy-regex" -version = "3.4.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" +checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496" dependencies = [ "lazy-regex-proc_macros", "once_cell", @@ -1094,9 +1156,9 @@ dependencies = [ [[package]] name = "lazy-regex-proc_macros" -version = "3.4.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" +checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358" dependencies = [ "proc-macro2", "quote", @@ -1110,6 +1172,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "levenshtein_automata" version = "0.2.1" @@ -1118,15 +1186,15 @@ checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libgit2-sys" -version = "0.18.2+1.9.1" +version = "0.18.3+1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" dependencies = [ "cc", "libc", @@ -1138,9 +1206,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libsqlite3-sys" @@ -1169,9 +1237,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -1181,37 +1249,36 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" @@ -1219,7 +1286,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -1228,16 +1295,6 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" -[[package]] -name = "mantra-miner" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45cb3048a51f3fb7066acc6383678f462176057b346a57ed1258fc43e4023899" -dependencies = [ - "anyhow", - "parking_lot", -] - [[package]] name = "measure_time" version = "0.9.0" @@ -1249,15 +1306,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.7" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -1279,14 +1336,14 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1328,9 +1385,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-traits" @@ -1348,11 +1405,17 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oneshot" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" +checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" [[package]] name = "openssl-probe" @@ -1362,9 +1425,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -1383,9 +1446,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1393,15 +1456,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1426,12 +1489,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.32" @@ -1440,9 +1497,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1464,9 +1521,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -1474,27 +1531,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "r2d2" @@ -1546,7 +1603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1566,7 +1623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1575,16 +1632,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -1619,18 +1676,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1640,9 +1697,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1651,9 +1708,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rusqlite" @@ -1695,30 +1752,39 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustyline" -version = "17.0.1" +version = "17.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6614df0b6d4cfb20d1d5e295332921793ce499af3ebc011bf1e393380e1e492" +checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" dependencies = [ "bitflags", "cfg-if", @@ -1731,7 +1797,7 @@ dependencies = [ "nix", "radix_trie", "unicode-segmentation", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "utf8parse", "windows-sys 0.60.2", ] @@ -1747,12 +1813,6 @@ dependencies = [ "syn", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -1779,27 +1839,38 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +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.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1808,14 +1879,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -1856,9 +1928,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -1867,10 +1939,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -1885,12 +1958,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -1900,9 +1970,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strict" @@ -1927,22 +1997,21 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn", ] [[package]] name = "syn" -version = "2.0.104" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -2108,15 +2177,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2137,18 +2206,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2157,30 +2226,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -2188,9 +2257,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2239,19 +2308,15 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "trane" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0375c0cf854db2c0af69ad0234c7415c062eda928e7a596f9dca5315bfcc8d3" +checksum = "3f7e32534c8dea800559b774383547683caef11a9487bad4426c446a9eae6aa8" dependencies = [ "anyhow", "bincode", "chrono", "derive_builder", - "fs_extra", - "git2", - "hex", "indoc", - "mantra-miner", "parking_lot", "r2d2", "r2d2_sqlite", @@ -2261,45 +2326,50 @@ dependencies = [ "rusqlite_migration", "serde", "serde_json", - "sha1", "strum", "tantivy", "tempfile", "thiserror", - "url", "ustr", "walkdir", ] [[package]] name = "trane-cli" -version = "0.25.0" +version = "0.26.0" dependencies = [ "anyhow", "built", "chrono", "clap", + "fs_extra", + "git2", + "hex", "indoc", "rand 0.9.2", "rustyline", "rustyline-derive", + "serde", "serde_json", + "sha1", + "tempfile", "termimad", "trane", + "url", "ustr", ] [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -2315,9 +2385,15 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unty" @@ -2327,9 +2403,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -2370,14 +2446,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.1", "js-sys", "rand 0.9.2", - "serde", + "serde_core", "wasm-bindgen", ] @@ -2411,50 +2487,46 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasm-bindgen" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2462,26 +2534,60 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2500,11 +2606,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2515,22 +2621,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.58.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", + "windows-link", "windows-result", "windows-strings", - "windows-targets 0.52.6", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -2539,9 +2645,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -2550,27 +2656,26 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -2588,7 +2693,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -2609,19 +2723,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -2632,9 +2746,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -2644,9 +2758,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -2656,9 +2770,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -2668,9 +2782,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -2680,9 +2794,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -2692,9 +2806,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -2704,9 +2818,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -2716,41 +2830,119 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2758,9 +2950,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -2770,18 +2962,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -2811,9 +3003,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -2822,9 +3014,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -2833,15 +3025,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zstd" version = "0.13.3" @@ -2862,9 +3060,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index db15600..52b1fff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "trane-cli" -version = "0.25.0" +version = "0.26.0" build = "build.rs" default-run = "trane" @@ -17,14 +17,21 @@ path = "src/simple_build.rs" anyhow = "1.0.99" chrono = "0.4.41" clap = { version = "4.5.46", features = ["derive"] } +hex = "0.4.3" +fs_extra = "1.3.0" +git2 = "0.20.2" indoc = "2.0.6" rand = "0.9.2" +sha1 = "0.10.6" +serde = "1.0.219" rustyline = "17.0.1" rustyline-derive = "0.11.1" +tempfile = "3.21.0" serde_json = "1.0.143" termimad = "0.33.0" -trane = "0.25.0" +trane = "0.26.0" ustr = { version = "1.1.0", features = ["serde"] } +url = "2.5.7" # Commented out for use in local development. # trane = { path = "../trane" } diff --git a/README.md b/README.md index 355d256..d84ce09 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,6 @@ SUBCOMMANDS: help Print this message or the help of the given subcommand(s) instructions Subcommands for showing course and lesson instructions list Subcommands for listing course, lesson, and exercise IDs - mantra-count Show the number of Tara Sarasvati mantras recited in the background during - the current session material Subcommands for showing course and lesson materials next Submits the score for the current exercise and proceeds to the next open Open the course library at the given location diff --git a/src/app.rs b/src/app.rs index 2a005b1..91b7da7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,22 +13,23 @@ use trane::{ }, ExerciseManifest, MasteryScore, SchedulerOptions, UnitType, }, - exercise_scorer::{ExerciseScorer, ExponentialDecayScorer}, + exercise_scorer::{ExerciseScorer, PowerLawScorer}, filter_manager::FilterManager, graph::UnitGraph, practice_rewards::PracticeRewards, practice_stats::PracticeStats, - repository_manager::RepositoryManager, + preferences_manager::PreferencesManager, review_list::ReviewList, reward_scorer::{RewardScorer, WeightedRewardScorer}, scheduler::ExerciseScheduler, study_session_manager::StudySessionManager, - transcription_downloader::TranscriptionDownloader, Trane, }; use ustr::Ustr; use crate::display::{DisplayAnswer, DisplayAsset, DisplayExercise}; +use crate::repository_manager::{LocalRepositoryManager, RepositoryManager}; +use crate::transcription_downloader::LocalTranscriptionDownloader; use crate::{built_info, cli::KeyValue}; /// Stores the app and its configuration. @@ -37,6 +38,12 @@ pub(crate) struct TraneApp { /// The instance of the Trane library. trane: Option, + /// The object managing git repositories containing courses. + repo_manager: Option, + + /// The object managing transcription asset downloads. + transcription_downloader: Option, + /// The filter used to select exercises. filter: Option, @@ -200,11 +207,7 @@ impl TraneApp { /// Adds the unit with the given ID to the blacklist. pub fn blacklist_unit(&mut self, unit_id: Ustr) -> Result<()> { ensure!(self.trane.is_some(), "no Trane instance is open"); - ensure!( - self.unit_exists(unit_id)?, - "unit {} does not exist", - unit_id - ); + ensure!(self.unit_exists(unit_id)?, "unit {unit_id} does not exist"); self.trane.as_mut().unwrap().add_to_blacklist(unit_id)?; self.reset_batch(); @@ -293,7 +296,7 @@ impl TraneApp { for course_id in &course_ids { let unit_type = self.get_unit_type(*course_id)?; if unit_type != UnitType::Course { - bail!("Unit with ID {} is not a course", course_id); + bail!("Unit with ID {course_id} is not a course"); } } @@ -310,7 +313,7 @@ impl TraneApp { for lesson_id in &lesson_ids { let unit_type = self.get_unit_type(*lesson_id)?; if unit_type != UnitType::Lesson { - bail!("Unit with ID {} is not a lesson", lesson_id); + bail!("Unit with ID {lesson_id} is not a lesson"); } } @@ -406,7 +409,7 @@ impl TraneApp { .as_ref() .unwrap() .get_unit_type(unit_id) - .ok_or_else(|| anyhow!("missing type for unit with ID {}", unit_id)) + .ok_or_else(|| anyhow!("missing type for unit with ID {unit_id}")) } /// Prints the list of all the saved unit filters. @@ -702,8 +705,20 @@ impl TraneApp { /// Opens the course library at the given path. pub fn open_library(&mut self, library_root: &str) -> Result<()> { - let trane = Trane::new_local(&std::env::current_dir()?, Path::new(library_root))?; + let library_root = Path::new(library_root); + let trane = Trane::new_local(&std::env::current_dir()?, library_root)?; + let repo_manager = LocalRepositoryManager::new(library_root)?; self.trane = Some(trane); + self.repo_manager = Some(repo_manager); + let transcription_preferences = self + .trane + .as_ref() + .and_then(|trane| trane.get_user_preferences().ok()) + .and_then(|preferences| preferences.transcription) + .unwrap_or_default(); + self.transcription_downloader = Some(LocalTranscriptionDownloader { + preferences: transcription_preferences, + }); self.batch.drain(..); self.batch_index = 0; Ok(()) @@ -719,7 +734,7 @@ impl TraneApp { 3 => Ok(MasteryScore::Three), 4 => Ok(MasteryScore::Four), 5 => Ok(MasteryScore::Five), - _ => Err(anyhow!("invalid score {}", score)), + _ => Err(anyhow!("invalid score {score}")), }?; self.current_score = Some(mastery_score); Ok(()) @@ -735,7 +750,7 @@ impl TraneApp { .as_ref() .unwrap() .get_filter(filter_id) - .ok_or_else(|| anyhow!("no filter with ID {}", filter_id))?; + .ok_or_else(|| anyhow!("no filter with ID {filter_id}"))?; self.filter = Some(saved_filter.filter); self.study_session = None; self.reset_batch(); @@ -775,12 +790,13 @@ impl TraneApp { /// Shows the currently set filter. pub fn show_filter(&self) { - if self.filter.is_none() { + let Some(filter) = self.filter.as_ref() else { println!("No filter is set"); - } else { - println!("Filter:"); - println!("{:#?}", self.filter.as_ref().unwrap()); - } + return; + }; + + println!("Filter:"); + println!("{filter:#?}"); } /// Shows the course instructions for the given course. @@ -793,7 +809,7 @@ impl TraneApp { .as_ref() .unwrap() .get_course_manifest(course_id) - .ok_or_else(|| anyhow!("no manifest for course with ID {}", course_id))?; + .ok_or_else(|| anyhow!("no manifest for course with ID {course_id}"))?; match manifest.course_instructions { None => { println!("Course has no instructions"); @@ -813,7 +829,7 @@ impl TraneApp { .as_ref() .unwrap() .get_lesson_manifest(lesson_id) - .ok_or_else(|| anyhow!("no manifest for lesson with ID {}", lesson_id))?; + .ok_or_else(|| anyhow!("no manifest for lesson with ID {lesson_id}"))?; match manifest.lesson_instructions { None => { println!("Lesson has no instructions"); @@ -833,7 +849,7 @@ impl TraneApp { .as_ref() .unwrap() .get_course_manifest(course_id) - .ok_or_else(|| anyhow!("no manifest for course with ID {}", course_id))?; + .ok_or_else(|| anyhow!("no manifest for course with ID {course_id}"))?; match manifest.course_material { None => { println!("Course has no material"); @@ -853,7 +869,7 @@ impl TraneApp { .as_ref() .unwrap() .get_lesson_manifest(lesson_id) - .ok_or_else(|| anyhow!("no manifest for lesson with ID {}", lesson_id))?; + .ok_or_else(|| anyhow!("no manifest for lesson with ID {lesson_id}"))?; match manifest.lesson_material { None => { println!("Lesson has no material"); @@ -863,19 +879,6 @@ impl TraneApp { } } - /// Shows the current count of Tara Sarasvati mantras. Her mantra is "recited" by the - /// `mantra-mining` library in the background as a symbolic way in which users can contribute - /// back to the maintainers of this program. See more information in the README of the - /// `mantra-mining` library. - pub fn show_mantra_count(&self) -> Result<()> { - ensure!(self.trane.is_some(), "no Trane instance is open"); - println!( - "Mantra count: {}", - self.trane.as_ref().unwrap().mantra_count() - ); - Ok(()) - } - /// Shows the most recent scores for the given exercise. pub fn show_scores( &self, @@ -885,27 +888,34 @@ impl TraneApp { ) -> Result<()> { ensure!(self.trane.is_some(), "no Trane instance is open"); - // Retrieve and validate the exercise, course, and lesson IDs. + // Retrieve and validate information about the exercise. let exercise_id = self.exercise_id_or_current(exercise_id)?; if let Some(UnitType::Exercise) = self.trane.as_ref().unwrap().get_unit_type(exercise_id) { } else { - bail!("Unit with ID {} is not a valid exercise", exercise_id); + bail!("Unit with ID {exercise_id} is not a valid exercise"); } let lesson_id = self .trane .as_ref() .unwrap() .get_exercise_lesson(exercise_id) - .ok_or_else(|| anyhow!("no lesson for exercise with ID {}", exercise_id))?; + .ok_or_else(|| anyhow!("no lesson for exercise with ID {exercise_id}"))?; let course_id = self .trane .as_ref() .unwrap() .get_lesson_course(lesson_id) - .ok_or_else(|| anyhow!("no course for lesson with ID {}", lesson_id))?; + .ok_or_else(|| anyhow!("no course for lesson with ID {lesson_id}"))?; + let exercise_type = self + .trane + .as_ref() + .unwrap() + .get_exercise_manifest(exercise_id) + .ok_or_else(|| anyhow!("no manifest for exercise with ID {exercise_id}"))? + .exercise_type; // Retrieve the scores and rewards and compute the aggregate score. - let scores = self + let previous_trials = self .trane .as_ref() .unwrap() @@ -923,15 +933,18 @@ impl TraneApp { .get_rewards(course_id, num_rewards) .unwrap_or_default(); - let decay_scorer = ExponentialDecayScorer {}; + // Compute the score, reward, and decide if it needs to be applied. + let scorer = PowerLawScorer {}; let reward_scorer = WeightedRewardScorer {}; - let score = decay_scorer.score(&scores)?; - let reward = reward_scorer.score_rewards(&course_rewards, &lesson_rewards)?; - let total_score = if score > 0.0 { - (score + reward).clamp(0.0, 5.0) + let score = scorer.score(exercise_type, &previous_trials)?; + let mut reward = reward_scorer.score_rewards(&course_rewards, &lesson_rewards)?; + let mut total_score = score; + let apply_reward = reward_scorer.apply_reward(reward, &previous_trials); + if apply_reward { + total_score += reward; } else { - 0.0 - }; + reward = 0.0; + } // Print the scores. println!("Scores for exercise {exercise_id}:"); @@ -944,7 +957,7 @@ impl TraneApp { println!(); println!("Raw scores:"); println!("{:<25} {:>6}", "Date", "Score"); - for score in scores { + for score in previous_trials { if let Some(dt) = Local.timestamp_opt(score.timestamp, 0).earliest() { println!( "{:<25} {:>6}", @@ -967,7 +980,7 @@ impl TraneApp { .as_ref() .unwrap() .get_exercise_manifest(unit_id) - .ok_or_else(|| anyhow!("missing manifest for exercise {}", unit_id))?; + .ok_or_else(|| anyhow!("missing manifest for exercise {unit_id}"))?; println!("Unit manifest:"); println!("{manifest:#?}"); } @@ -977,7 +990,7 @@ impl TraneApp { .as_ref() .unwrap() .get_lesson_manifest(unit_id) - .ok_or_else(|| anyhow!("missing manifest for lesson {}", unit_id))?; + .ok_or_else(|| anyhow!("missing manifest for lesson {unit_id}"))?; println!("Unit manifest:"); println!("{manifest:#?}"); } @@ -987,7 +1000,7 @@ impl TraneApp { .as_ref() .unwrap() .get_course_manifest(unit_id) - .ok_or_else(|| anyhow!("missing manifest for course {}", unit_id))?; + .ok_or_else(|| anyhow!("missing manifest for course {unit_id}"))?; println!("Unit manifest:"); println!("{manifest:#?}"); } @@ -1052,21 +1065,31 @@ impl TraneApp { /// Adds a new repository to the Trane instance. pub fn add_repo(&mut self, url: &str, repo_id: Option) -> Result<()> { ensure!(self.trane.is_some(), "no Trane instance is open"); - self.trane.as_mut().unwrap().add_repo(url, repo_id)?; + self.repo_manager + .as_mut() + .ok_or_else(|| anyhow!("repository manager unavailable"))? + .add_repo(url, repo_id)?; Ok(()) } /// Removes the given repository from the Trane instance. pub fn remove_repo(&mut self, repo_id: &str) -> Result<()> { ensure!(self.trane.is_some(), "no Trane instance is open"); - self.trane.as_mut().unwrap().remove_repo(repo_id)?; + self.repo_manager + .as_mut() + .ok_or_else(|| anyhow!("repository manager unavailable"))? + .remove_repo(repo_id)?; Ok(()) } /// Lists all the repositories managed by the Trane instance. pub fn list_repos(&self) -> Result<()> { ensure!(self.trane.is_some(), "no Trane instance is open"); - let repos = self.trane.as_ref().unwrap().list_repos(); + let repos = self + .repo_manager + .as_ref() + .ok_or_else(|| anyhow!("repository manager unavailable"))? + .list_repos(); if repos.is_empty() { println!("No repositories are managed by Trane"); return Ok(()); @@ -1082,25 +1105,27 @@ impl TraneApp { /// Updates the given repository. pub fn update_repo(&mut self, repo_id: &str) -> Result<()> { ensure!(self.trane.is_some(), "no Trane instance is open"); - self.trane.as_mut().unwrap().update_repo(repo_id)?; + self.repo_manager + .as_ref() + .ok_or_else(|| anyhow!("repository manager unavailable"))? + .update_repo(repo_id)?; Ok(()) } /// Updates all the repositories managed by the Trane instance. pub fn update_all_repos(&mut self) -> Result<()> { ensure!(self.trane.is_some(), "no Trane instance is open"); - self.trane.as_mut().unwrap().update_all_repos()?; + self.repo_manager + .as_ref() + .ok_or_else(|| anyhow!("repository manager unavailable"))? + .update_all_repos()?; Ok(()) } /// Adds the given unit to the review list. pub fn add_to_review_list(&mut self, unit_id: Ustr) -> Result<()> { ensure!(self.trane.is_some(), "no Trane instance is open"); - ensure!( - self.unit_exists(unit_id)?, - "unit {} does not exist", - unit_id - ); + ensure!(self.unit_exists(unit_id)?, "unit {unit_id} does not exist"); self.trane.as_mut().unwrap().add_to_review_list(unit_id)?; self.reset_batch(); @@ -1132,11 +1157,13 @@ impl TraneApp { println!("Review list:"); println!("{:<10} {:<50}", "Unit Type", "Unit ID"); for unit_id in entries { - let unit_type = self.get_unit_type(unit_id); - if unit_type.is_err() { - println!("{:<10} {:<50}", "Unknown", unit_id.as_str()); - } else { - println!("{:<10} {:<50}", unit_type.unwrap(), unit_id.as_str()); + match self.get_unit_type(unit_id) { + Ok(unit_type) => { + println!("{:<10} {:<50}", unit_type, unit_id.as_str()); + } + Err(_) => { + println!("{:<10} {:<50}", "Unknown", unit_id.as_str()); + } } } Ok(()) @@ -1233,7 +1260,7 @@ impl TraneApp { .as_ref() .unwrap() .get_study_session(session_id) - .ok_or_else(|| anyhow!("no study session with ID {}", session_id))?; + .ok_or_else(|| anyhow!("no study session with ID {session_id}"))?; self.filter = None; self.study_session = Some(StudySessionData { start_time: Utc::now(), @@ -1255,14 +1282,18 @@ impl TraneApp { /// Prints the path to the transcription asset for the given exercise. pub fn transcription_path(&self, exercise_id: Ustr) -> Result<()> { - ensure!(self.trane.is_some(), "no Trane instance is open"); + let Some((trane, downloader)) = self.transcription_context() else { + return Ok(()); + }; + let exercise_id = self.exercise_id_or_current(exercise_id)?; + let get_exercise_manifest = |exercise_id| trane.get_exercise_manifest(exercise_id); - let trane = self.trane.as_ref().unwrap(); - let path = trane.transcription_download_path(exercise_id); + let path = downloader.transcription_download_path(exercise_id, &get_exercise_manifest); if let Some(path) = path { println!("Transcription asset download path: {}", path.display()); } - let alias_path = trane.transcription_download_path_alias(exercise_id); + let alias_path = + downloader.transcription_download_path_alias(exercise_id, &get_exercise_manifest); if let Some(alias_path) = alias_path { println!( "Transcription asset download path alias: {}", @@ -1275,13 +1306,13 @@ impl TraneApp { /// Downloads the transcription asset from the given exercise to the specified directory in the /// user preferences. pub fn download_transcription_asset(&self, exercise_id: Ustr, redownload: bool) -> Result<()> { - ensure!(self.trane.is_some(), "no Trane instance is open"); - + let Some((trane, downloader)) = self.transcription_context() else { + return Ok(()); + }; let exercise_id = self.exercise_id_or_current(exercise_id)?; - self.trane - .as_ref() - .unwrap() - .download_transcription_asset(exercise_id, redownload)?; + let get_exercise_manifest = |exercise_id| trane.get_exercise_manifest(exercise_id); + + downloader.download_transcription_asset(exercise_id, redownload, &get_exercise_manifest)?; println!("Transcription asset for exercise {exercise_id} downloaded"); println!(); self.transcription_path(exercise_id)?; @@ -1290,11 +1321,14 @@ impl TraneApp { /// Prints whether the transcription asset for the given exercise has been downloaded. pub fn is_transcription_asset_downloaded(&self, exercise_id: Ustr) -> Result<()> { - ensure!(self.trane.is_some(), "no Trane instance is open"); - + let Some((trane, downloader)) = self.transcription_context() else { + return Ok(()); + }; let exercise_id = self.exercise_id_or_current(exercise_id)?; - let trane = self.trane.as_ref().unwrap(); - let is_downloaded = trane.is_transcription_asset_downloaded(exercise_id); + let get_exercise_manifest = |exercise_id| trane.get_exercise_manifest(exercise_id); + + let is_downloaded = + downloader.is_transcription_asset_downloaded(exercise_id, &get_exercise_manifest); if is_downloaded { println!("Transcription for exercise {exercise_id} is downloaded"); println!(); @@ -1304,4 +1338,12 @@ impl TraneApp { } Ok(()) } + + /// Returns the open library and transcription downloader used by transcription commands. + fn transcription_context(&self) -> Option<(&Trane, &LocalTranscriptionDownloader)> { + Some(( + self.trane.as_ref()?, + self.transcription_downloader.as_ref()?, + )) + } } diff --git a/src/cli.rs b/src/cli.rs index 33eb4c6..cbffc5c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -427,17 +427,6 @@ pub(crate) enum Subcommands { #[clap(subcommand)] List(ListSubcommands), - #[clap( - about = "Show the number of Tara Sarasvati mantras recited in the background during \ - the current session" - )] - #[clap( - long_about = "Trane \"recites\" Tara Sarasvati's mantra in the background as a symbolic \ - way in which users can contribute back to the Trane Project. This command shows the \ - number of mantras that Trane has recited so far." - )] - MantraCount, - #[clap(about = "Subcommands for showing course and lesson materials")] #[clap(subcommand)] Material(MaterialSubcommands), @@ -713,11 +702,6 @@ impl TraneCli { } }, - Subcommands::MantraCount => { - app.show_mantra_count()?; - Ok(true) - } - Subcommands::Next => { app.next()?; Ok(true) diff --git a/src/main.rs b/src/main.rs index 6be1b12..f394fb5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,8 @@ mod built_info { mod cli; mod display; mod helper; +mod repository_manager; +mod transcription_downloader; use anyhow::Result; use app::TraneApp; @@ -89,20 +91,19 @@ fn main() -> Result<()> { // Parse the arguments. let cli = TraneCli::try_parse_from(args.iter()); - if cli.is_err() { - println!("{}", cli.unwrap_err()); - continue; - } - - // Execute the subcommand. - match cli.unwrap().execute_subcommand(&mut app) { - Ok(continue_execution) => { - if continue_execution { - continue; + match cli { + Ok(cli) => match cli.execute_subcommand(&mut app) { + Ok(continue_execution) => { + if continue_execution { + continue; + } + break; } - break; + Err(err) => println!("Error: {err:#}"), + }, + Err(err) => { + println!("{err}"); } - Err(err) => println!("Error: {err:#}"), } } Err(ReadlineError::Interrupted) => { diff --git a/src/repository_manager.rs b/src/repository_manager.rs new file mode 100644 index 0000000..ac468a9 --- /dev/null +++ b/src/repository_manager.rs @@ -0,0 +1,564 @@ +//! A module containing functions to download and manage courses from git repositories, which is +//! meant to simplify the process of adding new courses to Trane. + +use anyhow::{anyhow, bail, Context, Result}; +use serde::Serialize; +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, +}; +use url::Url; + +use trane::{data::RepositoryMetadata, error::RepositoryManagerError, TRANE_CONFIG_DIR_PATH}; + +// The name of the directory where repositories will be downloaded. +const DOWNLOAD_DIRECTORY: &str = "managed_courses"; + +// The name of the directory containing metadata for all repositories. +const REPOSITORY_DIRECTORY: &str = "repositories"; + +/// The prefix for HTTPS URLs. Only HTTP URLs are supported at the moment because SSH URLs require +/// Trane to have access to the user's SSH keys. +const HTTPS_PREFIX: &str = "https://"; + +/// A trait to manage git repositories of courses, with functions to add new repositories, remove +/// existing ones, and update repositories to the latest version. +pub trait RepositoryManager { + /// Downloads the courses from the given git repository into the given directory. The ID will + /// also be used to identify the repository in the future and as the name of the directory. If + /// omitted, the name of the repository will be used to generate an ID. + fn add_repo( + &mut self, + url: &str, + repo_id: Option, + ) -> Result<(), RepositoryManagerError>; + + /// Removes the repository with the given ID. + fn remove_repo(&mut self, repo_id: &str) -> Result<(), RepositoryManagerError>; + + /// Attempts to pull the latest version of the given repository. + fn update_repo(&self, repo_id: &str) -> Result<(), RepositoryManagerError>; + + /// Attempts to pull the latest version of all repositories. + fn update_all_repos(&self) -> Result<(), RepositoryManagerError>; + + /// Returns a list of all the repositories that are currently being managed. + fn list_repos(&self) -> Vec; +} + +/// An implementation of [`RepositoryManager`] backed by the local file system. All repositories +/// will be downloaded to the `managed_courses` directory in the root of the Trane library. +pub struct LocalRepositoryManager { + /// A map of repository IDs to its metadata. + repositories: HashMap, + + /// The path to the directory where repositories will be downloaded. + download_directory: PathBuf, + + /// The path to the directory where repository metadata will be stored. + metadata_directory: PathBuf, +} + +impl LocalRepositoryManager { + /// Returns the default ID for the repository based on the URL. + fn id_from_url(url: &Url) -> Result { + Ok(url + .path_segments() + .and_then(Iterator::last) + .ok_or(anyhow!("invalid repository URL"))? + .trim_end_matches(".git") + .into()) + } + + /// Reads the repository metadata from the given path. + fn read_metadata(path: &Path) -> Result { + let repo = serde_json::from_str::( + &fs::read_to_string(path) + .context(format!("failed to read metadata file {}", path.display()))?, + ) + .context(format!("failed to parse metadata file {}", path.display()))?; + Ok(repo) + } + + /// Writes the repository metadata to metadata directory. + fn write_metadata(&self, metadata: &RepositoryMetadata) -> Result<()> { + let path = self + .metadata_directory + .join(format!("{}.json", metadata.id)); + + let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); + let mut buf = Vec::new(); + let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter); + metadata + .serialize(&mut ser) + .context("failed to serialize repository metadata")?; + fs::write(&path, buf).context(format!("failed to write metadata to {}", path.display()))?; + Ok(()) + } + + /// Clones the repository at the given URL into the given directory. If the directory already + /// exists, it will be deleted and replaced with the new repository. + fn clone_repo(&self, url: &str, repo_id: &str) -> Result<()> { + // Clone the repo into a temp directory. + let temp_dir = tempfile::tempdir()?; + let temp_clone_path = temp_dir.path().join(repo_id); + git2::Repository::clone(url, &temp_clone_path)?; + + // Copy the repo into the download directory. + let clone_dir = self.download_directory.join(repo_id); + if clone_dir.exists() { + fs::remove_dir_all(&clone_dir).context(format!( + "cannot remove repository directory {}", + clone_dir.display() + ))?; + } + fs::create_dir_all(&clone_dir).context(format!( + "cannot create repository directory {}", + clone_dir.display() + ))?; + fs_extra::copy_items( + &[temp_clone_path.to_str().unwrap()], + &self.download_directory, + &fs_extra::dir::CopyOptions::new().copy_inside(true), + ) + .context("failed to copy repository")?; + Ok(()) + } + + /// Opens the download directory and tracks all the existing repositories. + pub fn new(library_root: &Path) -> Result { + // Create the repository manager and the repository directory if it doesn't exist. + let repo_dir = library_root + .join(TRANE_CONFIG_DIR_PATH) + .join(REPOSITORY_DIRECTORY); + if !repo_dir.exists() { + fs::create_dir(&repo_dir)?; + } + let mut manager = LocalRepositoryManager { + repositories: HashMap::new(), + download_directory: library_root.join(DOWNLOAD_DIRECTORY), + metadata_directory: repo_dir.clone(), + }; + + // Read the repository directory and add all the repositories to the map. + let read_repo_dir = fs::read_dir(&repo_dir)?; + for entry in read_repo_dir.flatten() { + if !entry.path().is_file() || entry.path().extension().unwrap_or_default() != "json" { + continue; + } + + // Read the repository metadata and add it to the map. + let repo_metadata = Self::read_metadata(&entry.path())?; + manager + .repositories + .insert(repo_metadata.id.clone(), repo_metadata.clone()); + + // Verify that the repository exists and is a valid git repository. + let download_directory = library_root + .join(DOWNLOAD_DIRECTORY) + .join(&repo_metadata.id); + if !download_directory.exists() { + // Try to clone the repository if it doesn't exist. + manager.clone_repo(&repo_metadata.url, &repo_metadata.id)?; + } + git2::Repository::open(&download_directory)?; + } + Ok(manager) + } + + /// Helper function to add a repository to the manager. + fn add_repo_helper(&mut self, url: &str, repo_id: Option) -> Result<()> { + // Check that the repository URL is not an SSH URL. + if !url.starts_with(HTTPS_PREFIX) { + bail!("repository URL must be an HTTPS URL"); + } + + // Extract the repository ID from the URL if it wasn't provided. + let parsed_url = url.parse::()?; + let repo_id = if let Some(repo_id) = repo_id { + repo_id + } else { + Self::id_from_url(&parsed_url)? + }; + + // Check that no other repository has the same ID. + if self.repositories.contains_key(&repo_id) { + bail!("another repository with ID {repo_id} already exists"); + } + + // Clone the repository into the download directory. + self.clone_repo(url, &repo_id)?; + + // Add the metadata to the repository directory and the map. + let repo_metadata = RepositoryMetadata { + id: repo_id.clone(), + url: url.to_string(), + }; + self.write_metadata(&repo_metadata)?; + self.repositories.insert(repo_id, repo_metadata); + Ok(()) + } + + /// Helper function to remove a repository from the manager. + fn remove_repo_helper(&mut self, repo_id: &str) -> Result<()> { + // Do nothing if no repository with the given ID exists. + if !self.repositories.contains_key(repo_id) { + return Ok(()); + } + + // Remove the repository from the map and delete the cloned repository and metadata. + self.repositories.remove(repo_id); + let clone_dir = self.download_directory.join(repo_id); + fs::remove_dir_all(clone_dir.clone()).context(format!( + "cannot remove repository directory {}", + clone_dir.display() + ))?; + let repo_metadata_path = self.metadata_directory.join(format!("{repo_id}.json")); + fs::remove_file(repo_metadata_path).context("cannot remove repository metadata")?; + Ok(()) + } + + /// Helper function to update a repository. + fn update_repo_helper(&self, repo_id: &str) -> Result<()> { + let repo_metadata = self.repositories.get(repo_id); + if repo_metadata.is_none() { + bail!("no repository with the given ID exists"); + } + + // Re-clone the repository to make the logic easier. Otherwise, it would be harder to handle + // corner cases. Users should not directly modify the cloned repositories. + let repo_metadata = repo_metadata.unwrap(); + self.clone_repo(&repo_metadata.url, &repo_metadata.id) + } +} + +impl RepositoryManager for LocalRepositoryManager { + fn add_repo( + &mut self, + url: &str, + repo_id: Option, + ) -> Result<(), RepositoryManagerError> { + self.add_repo_helper(url, repo_id) + .map_err(|e| RepositoryManagerError::AddRepo(url.into(), e)) + } + + fn remove_repo(&mut self, repo_id: &str) -> Result<(), RepositoryManagerError> { + self.remove_repo_helper(repo_id) + .map_err(|e| RepositoryManagerError::RemoveRepo(repo_id.into(), e)) + } + + fn update_repo(&self, repo_id: &str) -> Result<(), RepositoryManagerError> { + self.update_repo_helper(repo_id) + .map_err(|e| RepositoryManagerError::UpdateRepo(repo_id.into(), e)) + } + + fn update_all_repos(&self) -> Result<(), RepositoryManagerError> { + for repo_id in self.repositories.keys() { + self.update_repo(repo_id)?; + } + Ok(()) + } + + fn list_repos(&self) -> Vec { + self.repositories.values().cloned().collect() + } +} + +#[cfg(test)] +mod test { + use std::os::unix::prelude::PermissionsExt; + + use anyhow::Result; + + use super::*; + + const REPO_URL: &str = "https://github.com/trane-project/trane-leetcode.git"; + const REPO_ID: &str = "trane-leetcode"; + + fn setup_directories(library_root: &Path) -> Result<()> { + let metadata_dir = library_root + .join(TRANE_CONFIG_DIR_PATH) + .join(REPOSITORY_DIRECTORY); + fs::create_dir_all(metadata_dir)?; + Ok(()) + } + + /// Verifies opening a repository manager with empty directories. + #[test] + fn new_empty() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let _ = LocalRepositoryManager::new(library_root.path())?; + Ok(()) + } + + /// Verifies adding a repository. + #[test] + fn add() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + assert!(manager.repositories.contains_key(REPO_ID)); + let repo_dir = library_root.path().join(DOWNLOAD_DIRECTORY).join(REPO_ID); + assert!(repo_dir.exists()); + let metadata_path = library_root + .path() + .join(TRANE_CONFIG_DIR_PATH) + .join(REPOSITORY_DIRECTORY) + .join(format!("{REPO_ID}.json")); + assert!(metadata_path.exists()); + assert!(LocalRepositoryManager::read_metadata(&metadata_path).is_ok()); + Ok(()) + } + + /// Verifies adding a repository with a custom ID. + #[test] + fn add_with_id() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, Some("custom-id".to_string()))?; + assert!(manager.repositories.contains_key("custom-id")); + assert!(library_root + .path() + .join(DOWNLOAD_DIRECTORY) + .join("custom-id") + .exists()); + let metadata_path = library_root + .path() + .join(TRANE_CONFIG_DIR_PATH) + .join(REPOSITORY_DIRECTORY) + .join("custom-id.json"); + assert!(metadata_path.exists()); + assert!(LocalRepositoryManager::read_metadata(&metadata_path).is_ok()); + Ok(()) + } + + /// Verifies adding a repository with an SSH URL. + #[test] + fn add_ssh_repo() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + assert!(manager + .add_repo("git@github.com:trane-project/trane-leetcode.git", None) + .is_err()); + Ok(()) + } + + /// Verifies adding a repository with an existing ID. + #[test] + fn add_duplicate() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + assert!(manager.add_repo(REPO_URL, None).is_err()); + Ok(()) + } + + /// Verifies adding a repository where the clone directory already exists and is not a + /// directory. + #[test] + fn add_existing_file() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + let download_dir = library_root.path().join(DOWNLOAD_DIRECTORY); + fs::create_dir_all(download_dir)?; + let repo_dir = library_root.path().join(DOWNLOAD_DIRECTORY).join(REPO_ID); + fs::File::create(repo_dir)?; + assert!(manager.add_repo(REPO_URL, None).is_err()); + Ok(()) + } + + /// Verifies adding a repository where the download directory cannot be created. + #[test] + fn add_bad_download_directory() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + let download_dir = library_root.path().join(DOWNLOAD_DIRECTORY); + fs::create_dir_all(&download_dir)?; + // Set permissions to 0 so that the download directory cannot be created. + fs::set_permissions(&download_dir, fs::Permissions::from_mode(0o0))?; + + assert!(manager.add_repo(REPO_URL, None).is_err()); + Ok(()) + } + + /// Verifies removing a repository. + #[test] + fn remove() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + manager.remove_repo(REPO_ID)?; + assert!(!manager.repositories.contains_key(REPO_ID)); + assert!(!library_root + .path() + .join(DOWNLOAD_DIRECTORY) + .join(REPO_ID) + .exists()); + assert!(!library_root + .path() + .join(TRANE_CONFIG_DIR_PATH) + .join(REPOSITORY_DIRECTORY) + .join(format!("{REPO_ID}.json")) + .exists()); + Ok(()) + } + + /// Verifies removing a repository that does not exist. + #[test] + fn remove_nonexistent() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.remove_repo(REPO_ID)?; + Ok(()) + } + + /// Verifies an error is thrown when removing a repository where the download directory cannot + /// be removed. + #[test] + fn remove_bad_directory() -> Result<()> { + // Add the repository. + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + assert!(manager.repositories.contains_key(REPO_ID)); + + // Set permissions so that the download directory cannot be removed. + let repo_dir = library_root.path().join(DOWNLOAD_DIRECTORY).join(REPO_ID); + assert!(repo_dir.exists()); + fs::set_permissions(&repo_dir, fs::Permissions::from_mode(0o000))?; + assert!(manager.remove_repo(REPO_ID).is_err()); + + Ok(()) + } + + /// Verifies an error is thrown when removing a repository where the metadata file cannot + /// be removed. + #[test] + fn remove_bad_metadata() -> Result<()> { + // Add the repository. + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + assert!(manager.repositories.contains_key(REPO_ID)); + + // Set the permissions of the metadata directory so that the metadata file cannot be + // removed. + let metadata_dir = library_root + .path() + .join(TRANE_CONFIG_DIR_PATH) + .join(REPOSITORY_DIRECTORY); + assert!(metadata_dir.exists()); + fs::set_permissions(&metadata_dir, fs::Permissions::from_mode(0o000))?; + assert!(manager.remove_repo(REPO_ID).is_err()); + + Ok(()) + } + + /// Verifies updating a repository. + #[test] + fn update() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + manager.update_repo(REPO_ID)?; + Ok(()) + } + + /// Verifies updating a repository that does not exist. + #[test] + fn update_nonexistent() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let manager = LocalRepositoryManager::new(library_root.path())?; + assert!(manager.update_repo(REPO_ID).is_err()); + Ok(()) + } + + /// Verifies updating all repositories. + #[test] + fn update_all() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + manager.update_all_repos()?; + Ok(()) + } + + /// Verifies listing all repositories. + #[test] + fn list() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + let repos = manager.list_repos(); + assert_eq!(repos.len(), 1); + assert_eq!(repos[0].id, REPO_ID); + assert_eq!(repos[0].url, REPO_URL); + Ok(()) + } + + /// Verifies opening an existing repository manager. + #[test] + fn new_existing() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + + // Add a non JSON file to verify it is ignored. + let ignored_file = library_root + .path() + .join(TRANE_CONFIG_DIR_PATH) + .join(REPOSITORY_DIRECTORY) + .join("ignored_file.txt"); + fs::write(ignored_file, "This file should be ignored")?; + + let _ = LocalRepositoryManager::new(library_root.path())?; + Ok(()) + } + + /// Verifies opening a repository manager with a missing repo. + #[test] + fn new_missing_repo() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + let repo_dir = library_root.path().join(DOWNLOAD_DIRECTORY).join(REPO_ID); + fs::remove_dir_all(repo_dir)?; + let _ = LocalRepositoryManager::new(library_root.path())?; + Ok(()) + } + + /// Verifies opening a repository manager with a bad repo. + #[test] + fn new_bad_repo() -> Result<()> { + let library_root = tempfile::tempdir()?; + setup_directories(library_root.path())?; + let mut manager = LocalRepositoryManager::new(library_root.path())?; + manager.add_repo(REPO_URL, None)?; + let git_dir = library_root + .path() + .join(DOWNLOAD_DIRECTORY) + .join(REPO_ID) + .join(".git"); + fs::remove_dir_all(git_dir)?; + assert!(LocalRepositoryManager::new(library_root.path()).is_err()); + Ok(()) + } +} diff --git a/src/transcription_downloader.rs b/src/transcription_downloader.rs new file mode 100644 index 0000000..45129bf --- /dev/null +++ b/src/transcription_downloader.rs @@ -0,0 +1,446 @@ +//! Manages the download of asset files for transcription courses. +//! +//! Transcription courses include references to external assets. Manually downloading them is a +//! cumbersome process, so this module automates the process. + +use std::{ + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use anyhow::{bail, Result}; +use sha1::{Digest, Sha1}; +use ustr::Ustr; + +use trane::{ + data::{ + course_generator::transcription::{TranscriptionLink, TranscriptionPreferences}, + ExerciseAsset, ExerciseManifest, + }, + error::TranscriptionDownloaderError, +}; + +/// Extracts the transcription link from an exercise manifest. +fn extract_transcription_link(manifest: &ExerciseManifest) -> Option { + match &manifest.exercise_asset { + ExerciseAsset::TranscriptionAsset { external_link, .. } => external_link.clone(), + _ => None, + } +} + +/// Gets the transcription link for an exercise. +fn get_transcription_link( + exercise_id: Ustr, + get_exercise_manifest: &(impl Fn(Ustr) -> Option + Send + Sync), +) -> Option { + get_exercise_manifest(exercise_id).and_then(|manifest| extract_transcription_link(&manifest)) +} + +/// Downloads transcription assets to local storage. +pub struct LocalTranscriptionDownloader { + /// Preferences for transcription courses. + pub preferences: TranscriptionPreferences, +} + +impl LocalTranscriptionDownloader { + /// Gets the name of the directory where the asset should be downloaded. + fn download_dir_name(link: &TranscriptionLink) -> String { + let TranscriptionLink::YouTube(input) = link; + let mut hasher = Sha1::new(); + hasher.update(input.as_bytes()); + let hash = hasher.finalize(); + hex::encode(hash) + } + + /// Gets the name of the file to which download the asset. + fn download_file_name(link: &TranscriptionLink) -> String { + match link { + TranscriptionLink::YouTube(_) => "audio.m4a".to_string(), + } + } + + /// Generates a download path relative to the root download directory. + fn rel_download_path(link: &TranscriptionLink) -> PathBuf { + Path::new(&Self::download_dir_name(link)).join(Self::download_file_name(link)) + } + + /// Gets the full path to the asset file with the download directory prepended. + fn full_download_path(&self, link: &TranscriptionLink) -> Option { + self.preferences + .download_path + .as_ref() + .map(|download_path| Path::new(download_path).join(Self::rel_download_path(link))) + } + + /// Gets the full path to the asset file with the alias directory prepended. + fn full_alias_path(&self, link: &TranscriptionLink) -> Option { + self.preferences + .download_path_alias + .as_ref() + .map(|path_alias| Path::new(path_alias).join(Self::rel_download_path(link))) + } + + /// Verifies that a binary is installed. The argument should be something simple, like a version + /// flag, that will exit quickly. + fn verify_binary(name: &str, arg: &str) -> Result<()> { + let status = Command::new(name) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg(arg) + .status(); + let Ok(status) = status else { + bail!("command \"{name}\" cannot be found"); + }; + if !status.success() { + bail!("command \"{name}\" failed"); + } + Ok(()) + } + + /// Checks that the prerequisites to use the downloader are met. + fn check_prerequisites(&self) -> Result<()> { + // Check yt-dlp is installed. + Self::verify_binary("yt-dlp", "--version")?; + + // Check the download path is valid. + let Some(download_path) = self.preferences.download_path.as_ref() else { + bail!("transcription download path is not set"); + }; + let download_path = Path::new(download_path); + if !download_path.exists() { + bail!("transcription download path does not exist"); + } + Ok(()) + } + + /// Helper function to download an asset. + fn download_asset_helper( + &self, + exercise_id: Ustr, + force_download: bool, + get_exercise_manifest: &(impl Fn(Ustr) -> Option + Send + Sync), + ) -> Result<()> { + // Check if the asset has already been downloaded. + self.check_prerequisites()?; + let Some(link) = get_transcription_link(exercise_id, get_exercise_manifest) else { + return Ok(()); + }; + let download_path = self.full_download_path(&link).unwrap(); + if download_path.exists() && !force_download { + return Ok(()); + } + + // Create a temporary directory, download the asset, and copy it to the final location. + let temp_dir = tempfile::tempdir()?; + match link { + TranscriptionLink::YouTube(yt_link) => { + let temp_file = temp_dir.path().join("audio.m4a"); + let output = Command::new("yt-dlp") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .arg("--enable-file-urls") + .arg("--extract-audio") + .arg("--audio-format") + .arg("m4a") + .arg("--output") + .arg(temp_file.to_str().unwrap()) + .arg(&yt_link) + .output()?; + if !output.status.success() { + let err = String::from_utf8_lossy(&output.stderr); + bail!("yt-dlp failed to download audio from URL {yt_link}: {err}"); + } + std::fs::create_dir_all(download_path.parent().unwrap())?; + std::fs::copy(temp_file, &download_path)?; + } + } + Ok(()) + } + + /// Checks if the given asset has been downloaded. + pub fn is_transcription_asset_downloaded( + &self, + exercise_id: Ustr, + get_exercise_manifest: &(impl Fn(Ustr) -> Option + Send + Sync), + ) -> bool { + if self.preferences.download_path.is_none() { + return false; + } + let Some(link) = get_transcription_link(exercise_id, get_exercise_manifest) else { + return false; + }; + let download_path = self.full_download_path(&link).unwrap(); + download_path.exists() + } + + /// Downloads the given asset. + pub fn download_transcription_asset( + &self, + exercise_id: Ustr, + force_download: bool, + get_exercise_manifest: &(impl Fn(Ustr) -> Option + Send + Sync), + ) -> Result<(), TranscriptionDownloaderError> { + self.download_asset_helper(exercise_id, force_download, get_exercise_manifest) + .map_err(|e| TranscriptionDownloaderError::DownloadAsset(exercise_id, e)) + } + + /// Returns the download path for the given asset. + pub fn transcription_download_path( + &self, + exercise_id: Ustr, + get_exercise_manifest: &(impl Fn(Ustr) -> Option + Send + Sync), + ) -> Option { + let link = get_transcription_link(exercise_id, get_exercise_manifest)?; + self.full_download_path(&link) + } + + /// Returns the download path alias for the given asset. + pub fn transcription_download_path_alias( + &self, + exercise_id: Ustr, + get_exercise_manifest: &(impl Fn(Ustr) -> Option + Send + Sync), + ) -> Option { + let link = get_transcription_link(exercise_id, get_exercise_manifest)?; + self.full_alias_path(&link) + } +} + +#[cfg(test)] +mod test { + use std::path::{self, Path}; + use ustr::Ustr; + + use super::*; + use trane::data::{ + course_generator::transcription::{TranscriptionLink, TranscriptionPreferences}, + BasicAsset, ExerciseAsset, ExerciseManifest, ExerciseType, + }; + + // Test link to a real YouTube video: Margaret Glaspy and Julian Lage perform “Best Behavior”. + const YT_LINK: &str = "https://www.youtube.com/watch?v=p4LgzLjF4xE"; + + // A local copy of the file above to avoid using the network in tests. + const LOCAL_FILE: &str = "./testdata/test_audio.m4a"; + + fn build_manifest(link: Option) -> ExerciseManifest { + ExerciseManifest { + exercise_asset: ExerciseAsset::TranscriptionAsset { + content: "content".to_string(), + external_link: link, + }, + id: Ustr::from("exercise_id"), + lesson_id: Ustr::from("lesson_id"), + course_id: Ustr::from("course_id"), + name: "Exercise Name".to_string(), + description: None, + exercise_type: ExerciseType::Procedural, + } + } + + fn build_resolver( + link: Option, + ) -> impl Fn(Ustr) -> Option { + let manifest = build_manifest(link); + move |_id: Ustr| Some(manifest.clone()) + } + + /// Verifies extracting the link from a valid exercise manifest. + #[test] + fn test_extract_link() { + // Transcription asset with no link. + let mut manifest = build_manifest(None); + assert!(extract_transcription_link(&manifest).is_none()); + + // Transcription asset with a link. + manifest.exercise_asset = ExerciseAsset::TranscriptionAsset { + content: "content".to_string(), + external_link: Some(TranscriptionLink::YouTube(YT_LINK.into())), + }; + assert_eq!( + TranscriptionLink::YouTube(YT_LINK.into()), + extract_transcription_link(&manifest).unwrap() + ); + + // Other type of asset. + manifest.exercise_asset = ExerciseAsset::BasicAsset(BasicAsset::InlinedAsset { + content: "content".to_string(), + }); + assert!(extract_transcription_link(&manifest).is_none()); + } + + /// Verifies that exercises with no links are marked as not downloaded. + #[test] + fn test_is_downloaded_no_link() { + let downloader = LocalTranscriptionDownloader { + preferences: TranscriptionPreferences::default(), + }; + let resolver = build_resolver(None); + assert!(!downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + } + + /// Verifies that exercises that have not been downloaded are marked as such. + #[test] + fn test_is_downloaded_no_download() { + let downloader = LocalTranscriptionDownloader { + preferences: TranscriptionPreferences::default(), + }; + let resolver = build_resolver(Some(TranscriptionLink::YouTube(YT_LINK.into()))); + assert!(!downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + } + + /// Verifies that downloading an asset fails if there's no download path set. + #[test] + fn test_download_asset_no_path_set() { + let downloader = LocalTranscriptionDownloader { + preferences: TranscriptionPreferences { + instruments: vec![], + download_path: None, + download_path_alias: None, + }, + }; + let resolver = build_resolver(Some(TranscriptionLink::YouTube(YT_LINK.into()))); + assert!(downloader + .download_transcription_asset(Ustr::from("exercise"), false, &resolver) + .is_err()); + } + + /// Verifies that downloading an asset fails if the download path does not exist. + #[test] + fn test_download_asset_missing_dir() { + let downloader = LocalTranscriptionDownloader { + preferences: TranscriptionPreferences { + instruments: vec![], + download_path: Some("/some/missing/dir".to_string()), + download_path_alias: None, + }, + }; + let resolver = build_resolver(Some(TranscriptionLink::YouTube(YT_LINK.into()))); + assert!(!downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + assert!(downloader + .download_transcription_asset(Ustr::from("exercise"), false, &resolver) + .is_err()); + } + + /// Verifies downloading an exercise with no link. + #[test] + fn test_download_asset_no_link() { + let temp_dir = tempfile::tempdir().unwrap(); + let downloader = LocalTranscriptionDownloader { + preferences: TranscriptionPreferences { + instruments: vec![], + download_path: Some(temp_dir.path().to_str().unwrap().to_string()), + download_path_alias: None, + }, + }; + let resolver = build_resolver(None); + assert!(!downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + downloader + .download_transcription_asset(Ustr::from("exercise"), false, &resolver) + .unwrap(); + assert!(!downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + } + + /// Verifies downloading a valid asset. + #[test] + fn test_download_valid_asset() { + let temp_dir = tempfile::tempdir().unwrap(); + let local_path = path::absolute(Path::new(LOCAL_FILE)).unwrap(); + let file_link = format!("file://{}", local_path.to_str().unwrap()); + let downloader = LocalTranscriptionDownloader { + preferences: TranscriptionPreferences { + instruments: vec![], + download_path: Some(temp_dir.path().to_str().unwrap().to_string()), + download_path_alias: None, + }, + }; + let resolver = build_resolver(Some(TranscriptionLink::YouTube(file_link))); + assert!(!downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + downloader + .download_transcription_asset(Ustr::from("exercise"), false, &resolver) + .unwrap(); + assert!(downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + + // The asset won't be redownloaded if it already exists. + downloader + .download_transcription_asset(Ustr::from("exercise"), false, &resolver) + .unwrap(); + assert!(downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + + // Verify re-downloading the asset as well. + downloader + .download_transcription_asset(Ustr::from("exercise"), true, &resolver) + .unwrap(); + assert!(downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + } + + /// Verifies downloading an invalid asset. + #[test] + fn test_download_bad_asset() { + let temp_dir = tempfile::tempdir().unwrap(); + let downloader = LocalTranscriptionDownloader { + preferences: TranscriptionPreferences { + instruments: vec![], + download_path: Some(temp_dir.path().to_str().unwrap().to_string()), + download_path_alias: None, + }, + }; + let resolver = build_resolver(Some(TranscriptionLink::YouTube( + "https://www.youtube.com/watch?v=badID".into(), + ))); + assert!(!downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + assert!(downloader + .download_transcription_asset(Ustr::from("exercise"), false, &resolver) + .is_err()); + assert!(!downloader.is_transcription_asset_downloaded(Ustr::from("exercise"), &resolver)); + } + + /// Verifies that the download paths are correctly generated. + #[test] + fn test_download_paths() { + let temp_dir = tempfile::tempdir().unwrap(); + let downloader = LocalTranscriptionDownloader { + preferences: TranscriptionPreferences { + instruments: vec![], + download_path: Some(temp_dir.path().to_str().unwrap().to_string()), + download_path_alias: Some("C:/Users/username/Music".to_string()), + }, + }; + let resolver = build_resolver(Some(TranscriptionLink::YouTube(YT_LINK.into()))); + + let download_path = downloader + .transcription_download_path(Ustr::from("exercise"), &resolver) + .unwrap(); + assert!(download_path.ends_with("audio.m4a")); + assert!(download_path.starts_with(temp_dir.path())); + assert_eq!( + 40, + download_path + .parent() + .unwrap() + .file_name() + .unwrap() + .to_str() + .unwrap() + .len() + ); + + let alias_path = downloader + .transcription_download_path_alias(Ustr::from("exercise"), &resolver) + .unwrap(); + assert!(alias_path.ends_with("audio.m4a")); + assert!(alias_path.starts_with("C:/Users/username/Music")); + assert_eq!( + 40, + alias_path + .parent() + .unwrap() + .file_name() + .unwrap() + .to_str() + .unwrap() + .len() + ); + } +} diff --git a/testdata/test_audio.m4a b/testdata/test_audio.m4a new file mode 100644 index 0000000..75d62ce Binary files /dev/null and b/testdata/test_audio.m4a differ