Compare commits

..

No commits in common. "main" and "1.5.4" have entirely different histories.
main ... 1.5.4

18 changed files with 410 additions and 1876 deletions

258
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
version = 3
[[package]]
name = "Inflector"
@ -134,9 +134,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-broadcast"
version = "0.7.2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
dependencies = [
"event-listener",
"event-listener-strategy",
@ -266,9 +266,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.85"
version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
@ -368,9 +368,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.21.0"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a"
[[package]]
name = "byteorder"
@ -386,9 +386,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cc"
version = "1.2.7"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
dependencies = [
"jobserver",
"libc",
@ -435,9 +435,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.24"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd"
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
dependencies = [
"clap_builder",
"clap_derive",
@ -445,9 +445,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.24"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd"
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
dependencies = [
"anstream",
"anstyle",
@ -457,9 +457,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.24"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
@ -469,9 +469,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.4"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
[[package]]
name = "colorchoice"
@ -568,9 +568,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crossterm"
@ -766,7 +766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -802,9 +802,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.3.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]]
name = "fnv"
@ -973,7 +973,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
"unicode-width 0.1.14",
]
[[package]]
@ -995,9 +995,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "h2"
@ -1058,9 +1058,9 @@ dependencies = [
[[package]]
name = "http"
version = "1.2.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
@ -1098,9 +1098,9 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
[[package]]
name = "hyper"
version = "1.5.2"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
dependencies = [
"bytes",
"futures-channel",
@ -1118,9 +1118,9 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.27.5"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http",
@ -1383,9 +1383,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.76"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705"
dependencies = [
"once_cell",
"wasm-bindgen",
@ -1399,9 +1399,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.169"
version = "0.2.167"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
[[package]]
name = "libloading"
@ -1410,7 +1410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@ -1453,7 +1453,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lowfi"
version = "1.6.0"
version = "1.5.3"
dependencies = [
"Inflector",
"arc-swap",
@ -1472,6 +1472,7 @@ dependencies = [
"scraper",
"tokio",
"unicode-segmentation",
"unicode-width 0.2.0",
"url",
]
@ -1533,9 +1534,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
@ -1683,9 +1684,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.36.7"
version = "0.36.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
dependencies = [
"memchr",
]
@ -1816,22 +1817,22 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "phf"
version = "0.11.3"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros",
"phf_shared 0.11.3",
"phf_shared 0.11.2",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"phf_generator 0.11.2",
"phf_shared 0.11.2",
]
[[package]]
@ -1846,22 +1847,22 @@ dependencies = [
[[package]]
name = "phf_generator"
version = "0.11.3"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared 0.11.3",
"phf_shared 0.11.2",
"rand",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn",
@ -1873,23 +1874,23 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
"siphasher 0.3.11",
"siphasher",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher 1.0.1",
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
@ -1964,9 +1965,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.38"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
@ -2003,9 +2004,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.8"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags 2.6.0",
]
@ -2052,9 +2053,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.12"
version = "0.12.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
dependencies = [
"base64",
"bytes",
@ -2085,7 +2086,6 @@ dependencies = [
"system-configuration",
"tokio",
"tokio-native-tls",
"tower",
"tower-service",
"url",
"wasm-bindgen",
@ -2133,22 +2133,22 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.42"
version = "0.38.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
dependencies = [
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.23.20"
version = "0.23.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1"
dependencies = [
"once_cell",
"rustls-pki-types",
@ -2168,9 +2168,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.10.1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
[[package]]
name = "rustls-webpki"
@ -2244,9 +2244,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.14.0"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
dependencies = [
"core-foundation-sys",
"libc",
@ -2273,18 +2273,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
@ -2293,9 +2293,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.135"
version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
@ -2388,12 +2388,6 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.9"
@ -2526,9 +2520,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.95"
version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [
"proc-macro2",
"quote",
@ -2578,16 +2572,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.15.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [
"cfg-if",
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -2633,9 +2626,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.42.0"
version = "1.41.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
dependencies = [
"backtrace",
"bytes",
@ -2670,19 +2663,20 @@ dependencies = [
[[package]]
name = "tokio-rustls"
version = "0.26.1"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls",
"rustls-pki-types",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.13"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
dependencies = [
"bytes",
"futures-core",
@ -2708,27 +2702,6 @@ dependencies = [
"winnow",
]
[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
@ -2818,6 +2791,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -2898,9 +2877,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.99"
version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c"
dependencies = [
"cfg-if",
"once_cell",
@ -2909,12 +2888,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.99"
version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
@ -2923,9 +2903,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.49"
version = "0.4.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
checksum = "9dfaf8f50e5f293737ee323940c7d8b08a66a95a419223d9f41610ca08b0833d"
dependencies = [
"cfg-if",
"js-sys",
@ -2936,9 +2916,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"
version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -2946,9 +2926,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.99"
version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d"
dependencies = [
"proc-macro2",
"quote",
@ -2959,15 +2939,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.99"
version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49"
[[package]]
name = "web-sys"
version = "0.3.76"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -2995,7 +2975,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@ -3279,9 +3259,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.22"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]

View File

@ -1,6 +1,6 @@
[package]
name = "lowfi"
version = "1.6.0"
version = "1.5.4"
edition = "2021"
description = "An extremely simple lofi player."
license = "MIT"
@ -50,4 +50,5 @@ Inflector = "0.11.4"
lazy_static = "1.5.0"
libc = "0.2.167"
url = "2.5.4"
unicode-width = "0.2.0"
unicode-segmentation = "1.12.0"

View File

@ -1,6 +0,0 @@
# Environment Variables
Lowfi has some more specific options, usually as a result of minor feature requests, which are only documented here.
If you have some behaviour you'd like to change, which is quite specific, then see if one of these options suits you.
* `LOWFI_FIXED_MPRIS_NAME` - Limits the number of lowfi instances to one, but ensures the player name is always `lowfi`.

View File

@ -7,19 +7,20 @@ It'll do this as simply as it can: no albums, no ads, just lofi.
## Disclaimer
**All** of the audio files embedded into in lowfi by default are from [Lofi Girl's](https://lofigirl.com/) website,
**All** of the audio files played in lowfi are from [Lofi Girl's](https://lofigirl.com/) website,
under their [licensing guidelines](https://form.lofigirl.com/CommercialLicense).
If, god forbid, you're planning to use lowfi in a commercial setting, please
If god forbid you're planning to use this in a commercial setting, please
follow their rules.
## Why?
I really hate modern music platforms, and I wanted a small, "suckless"
app that would just play random lofi without video.
app that would literally just play lofi without video so I could use it
whenever.
It was also designed to be fairly resilient to inconsistent networks,
and as such it buffers 5 whole songs at a time instead of parts of the same song.
I also wanted it to be fairly resiliant to inconsistent networks,
so it buffers 5 whole songs at a time instead of parts of the same song.
Although, lowfi is yet to be properly tested in difficult conditions,
so don't rely on it too much until I do that. See [Scraping](#scraping) if
@ -43,7 +44,7 @@ On Linux, you'll also need openssl & alsa, as well as their headers.
- `alsa-lib` on Arch, `libasound2-dev` on Ubuntu.
- `openssl` on Arch, `libssl-dev` on Ubuntu.
Make sure to also install `pulseaudio-alsa` if you're using PulseAudio.
Make sure to also install `pulseaudio-alsa` if you're using pulseaudio.
### Cargo
@ -65,6 +66,8 @@ precompiled binaries from the [latest release](https://github.com/talwat/lowfi/r
### AUR
If you're on Arch, you can also use the AUR:
```sh
yay -S lowfi
```
@ -75,27 +78,6 @@ yay -S lowfi
zypper install lowfi
```
### Debian
> [!NOTE]
> This uses an unofficial Debian repository maintained by [Dario Griffo](https://github.com/dariogriffo).
```sh
curl -sS https://debian.griffo.io/3B9335DF576D3D58059C6AA50B56A1A69762E9FF.asc | gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/debian.griffo.io.gpg
echo "deb https://debian.griffo.io//apt $(lsb_release -sc 2>/dev/null) main" | sudo tee /etc/apt/sources.list.d/debian.griffo.io.list
sudo apt install -y lowfi
```
### Fedora (COPR)
> [!NOTE]
> This uses an unofficial COPR repository by [FurqanHun](https://github.com/FurqanHun).
```sh
sudo dnf copr enable furqanhun/lowfi
sudo dnf install lowfi
```
### Manual
This is good for debugging, especially in issues.
@ -120,39 +102,17 @@ Yeah, that's it.
### Controls
| Key | Function |
| ------------------ | --------------- |
| `s`, `n`, `l` | Skip Song |
| `p`, Space | Play/Pause |
| `+`, `=`, `k`, `↑` | Volume Up 10% |
| `→` | Volume Up 1% |
| `-`, `_`, `j`, `↓` | Volume Down 10% |
| `←` | Volume Down 1% |
| `q`, CTRL+C | Quit |
> [!NOTE]
> Besides its regular controls, lowfi offers compatibility with Media Keys
> and [MPRIS](https://wiki.archlinux.org/title/MPRIS) (with tools like `playerctl`).
>
> MPRIS is currently optional feature in cargo (enabled with `--features mpris`)
> due to it being only for Linux, as well as the fact that the main point of
> lowfi is it's unique & minimal interface.
| Key | Function |
|-------|----------------|
| `s` | Skip song |
| `p` | Play/Pause |
| `+/-` | Volume Up/Down |
| `q` | Quit |
### Extra Flags
If you have something you'd like to tweak about lowfi, you use additional flags which
slightly tweak the UI or behaviour of the menu. The flags can be viewed with `lowfi help`.
| Flag | Function |
| ----------------------------------- | ---------------------------------------------- |
| `-a`, `--alternate` | Use an alternate terminal screen |
| `-m`, `--minimalist` | Hide the bottom control bar |
| `-b`, `--borderless` | Exclude borders in UI |
| `-p`, `--paused` | Start lowfi paused |
| `-d`, `--debug` | Include ALSA & other logs |
| `-w`, `--width <WIDTH>` | Width of the player, from 0 to 32 [default: 3] |
| `-t`, `--track-list <TRACK_LIST>` | Use a [custom track list](#custom-track-lists) |
| `-s`, `--buffer-size <BUFFER_SIZE>` | Internal song buffer size [default: 5] |
If you have something you'd like to tweak about lowfi, you can run `lowfi help`
to view the available options.
### Scraping
@ -185,21 +145,19 @@ or it could also be the name of a file (without the `.txt` extension) in the dat
directory, so on Linux it's `~/.local/share/lowfi`.
For example, `lowfi --tracks minipop` would load `~/.local/share/lowfi/minipop.txt`.
Whereas if you did `lowfi --tracks ~/Music/minipop.txt` it would load from that
Whereas if you did `lowfi --tracks /home/user/Music/minipop.txt` it would load from that
specified directory.
#### The Format
In lists, the first line should be the base URL, followed by the rest of the tracks.
This is also known as the "header", because it comes first.
In Lists, the first line should be the base URL, followed by the rest of the tracks.
Each track will be first appended to the base URL, and then the result use to download
the track. All tracks must be in the MP3 format, as lowfi doesn't support any others currently.
the track. All tracks should end in `.mp3` and as such must be in the MP3 format.
Additionally, lowfi _won't_ put a `/` between the base & track for added flexibility,
so for most cases you should have a trailing `/` in your base url.
The exception to this is if the track name begins with something like `https://`,
where in that case the base will not be prepended to it.
lowfi won't put a `/` between the base & track for added flexibility, so for most cases you
should have a trailing `/` in your base url. The exception to this is if the track name begins
with something like `https://`, where in that case the base will not be prepended to it.
For example, in this list:
@ -215,14 +173,3 @@ lowfi would download these three URLs:
- `https://lofigirl.com/wp-content/uploads/2023/06/Foudroie-Finding-The-Edge-V2.mp3`
- `https://file-examples.com/storage/fea570b16e6703ef79e65b4/2017/11/file_example_MP3_5MG.mp3`
- `https://lofigirl.com/wp-content/uploads/2023/04/2-In-Front-Of-Me.mp3`
Additionally, you may also specify a custom display name for the track which is indicated by a `!`.
For example, if you had an entry like this:
```txt
2023/04/2-In-Front-Of-Me.mp3!custom name
```
Then lowfi would download from the first section, and display the second as the track name.
Further examples can be found in the [data](https://github.com/talwat/lowfi/tree/main/data) folder.

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
file:///home/user/Music/
Anomaly.mp3

View File

@ -1,13 +1,45 @@
//! An extremely simple lofi player.
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
use std::path::PathBuf;
#![warn(clippy::all, clippy::restriction, clippy::pedantic, clippy::nursery)]
#![allow(
clippy::single_call_fn,
clippy::struct_excessive_bools,
clippy::implicit_return,
clippy::question_mark_used,
clippy::shadow_reuse,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::std_instead_of_core,
clippy::print_stdout,
clippy::float_arithmetic,
clippy::integer_division_remainder_used,
clippy::used_underscore_binding,
clippy::print_stderr,
clippy::semicolon_outside_block,
clippy::non_send_fields_in_send_ty,
clippy::non_ascii_literal,
clippy::let_underscore_untyped,
clippy::let_underscore_must_use,
clippy::shadow_unrelated,
clippy::std_instead_of_alloc,
clippy::partial_pub_fields,
clippy::unseparated_literal_suffix,
clippy::self_named_module_files,
// TODO: Disallow these lints later.
clippy::unwrap_used,
clippy::pattern_type_mismatch,
clippy::tuple_array_conversions,
clippy::as_conversions,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::wildcard_enum_match_arm,
clippy::integer_division,
clippy::cast_sign_loss,
clippy::cast_lossless,
)]
use clap::{Parser, Subcommand};
use eyre::OptionExt;
mod messages;
mod play;
mod player;
mod tracks;
@ -16,44 +48,32 @@ mod tracks;
mod scrape;
/// An extremely simple lofi player.
#[derive(Parser, Clone)]
#[derive(Parser)]
#[command(about, version)]
#[allow(
clippy::struct_excessive_bools,
reason = "señor clippy, i assure you this is not a state machine"
)]
struct Args {
/// Use an alternate terminal screen.
/// Whether to use an alternate terminal screen.
#[clap(long, short)]
alternate: bool,
/// Hide the bottom control bar.
/// Whether to hide the bottom control bar.
#[clap(long, short)]
minimalist: bool,
/// Exclude borders in UI.
#[clap(long, short)]
borderless: bool,
/// Start lowfi paused.
/// Whether to start lowfi paused.
#[clap(long, short)]
paused: bool,
/// Include ALSA & other logs.
/// Whether to include ALSA & other logs.
#[clap(long, short)]
debug: bool,
/// Width of the player, from 0 to 32.
/// The width of the player, from 0 to 32.
#[clap(long, short, default_value_t = 3)]
width: usize,
/// Use a custom track list
/// This is either a path, or a name of a file in the data directory (eg. ~/.local/share/lowfi).
#[clap(long, short, alias = "list", short_alias = 'l')]
track_list: Option<String>,
/// Internal song buffer size.
#[clap(long, short = 's', alias = "buffer", default_value_t = 5)]
buffer_size: usize,
tracks: Option<String>,
/// The command that was ran.
/// This is [None] if no command was specified.
@ -62,7 +82,7 @@ struct Args {
}
/// Defines all of the extra commands lowfi can run.
#[derive(Subcommand, Clone)]
#[derive(Subcommand)]
enum Commands {
/// Scrapes the lofi girl website file server for files.
Scrape {
@ -76,20 +96,8 @@ enum Commands {
},
}
/// Gets lowfi's data directory.
pub fn data_dir() -> eyre::Result<PathBuf> {
let dir = dirs::data_dir()
.ok_or_eyre("data directory not found, are you *really* running this on wasm?")?
.join("lowfi");
Ok(dir)
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
#[cfg(target_os = "android")]
compile_error!("Android Audio API not supported due to threading shenanigans");
let cli = Args::parse();
if let Some(command) = cli.command {

View File

@ -1,37 +0,0 @@
/// Handles communication between the frontend & audio player.
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum Messages {
/// Notifies the audio server that it should update the track.
Next,
/// Special in that this isn't sent in a "client to server" sort of way,
/// but rather is sent by a child of the server when a song has not only
/// been requested but also downloaded aswell.
NewSong,
/// This signal is only sent if a track timed out. In that case,
/// lowfi will try again and again to retrieve the track.
TryAgain,
/// Similar to Next, but specific to the first track.
Init,
/// Unpause the [Sink].
#[allow(dead_code, reason = "this code may not be dead depending on features")]
Play,
/// Pauses the [Sink].
Pause,
/// Pauses the [Sink]. This will also unpause it if it is paused.
PlayPause,
/// Change the volume of playback.
ChangeVolume(f32),
/// Bookmark the current track.
Bookmark,
/// Quits gracefully.
Quit,
}

View File

@ -1,6 +1,5 @@
//! Responsible for the basic initialization & shutdown of the audio server & frontend.
use std::io::{stdout, IsTerminal};
use std::path::PathBuf;
use std::sync::Arc;
@ -8,9 +7,8 @@ use eyre::eyre;
use tokio::fs;
use tokio::{sync::mpsc, task};
use crate::messages::Messages;
use crate::player::ui;
use crate::player::Player;
use crate::player::{ui, Messages};
use crate::Args;
/// This is the representation of the persistent volume,
@ -25,7 +23,7 @@ impl PersistentVolume {
/// Retrieves the config directory.
async fn config() -> eyre::Result<PathBuf> {
let config = dirs::config_dir()
.ok_or_else(|| eyre!("Couldn't find config directory"))?
.ok_or(eyre!("Couldn't find config directory"))?
.join(PathBuf::from("lowfi"));
if !config.exists() {
@ -37,7 +35,7 @@ impl PersistentVolume {
/// Returns the volume as a float from 0 to 1.
pub fn float(self) -> f32 {
f32::from(self.inner) / 100.0
self.inner as f32 / 100.0
}
/// Loads the [`PersistentVolume`] from [`dirs::config_dir()`].
@ -66,65 +64,31 @@ impl PersistentVolume {
let config = Self::config().await?;
let path = config.join(PathBuf::from("volume.txt"));
#[expect(
clippy::as_conversions,
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
reason = "already rounded & absolute, therefore this should be safe"
)]
let percentage = (volume * 100.0).abs().round() as u16;
fs::write(path, percentage.to_string()).await?;
fs::write(path, ((volume * 100.0).abs().round() as u16).to_string()).await?;
Ok(())
}
}
/// Wrapper around [`rodio::OutputStream`] to implement [Send], currently unsafely.
///
/// This is more of a temporary solution until cpal implements [Send] on it's output stream.
pub struct SendableOutputStream(pub rodio::OutputStream);
// SAFETY: This is necessary because [OutputStream] does not implement [Send],
// due to some limitation with Android's Audio API.
// I'm pretty sure nobody will use lowfi with android, so this is safe.
#[expect(
clippy::non_send_fields_in_send_ty,
reason = "this is expected because of the nature of the struct"
)]
unsafe impl Send for SendableOutputStream {}
/// Initializes the audio server, and then safely stops
/// it when the frontend quits.
pub async fn play(args: Args) -> eyre::Result<()> {
// Actually initializes the player.
// Stream kept here in the master thread to keep it alive.
let (player, stream) = Player::new(&args).await?;
let player = Arc::new(player);
let player = Arc::new(Player::new(&args).await?);
// Initialize the UI, as well as the internal communication channel.
let (tx, rx) = mpsc::channel(8);
let ui = if stdout().is_terminal() {
Some(task::spawn(ui::start(
Arc::clone(&player),
tx.clone(),
args.clone(),
)))
} else {
None
};
let ui = task::spawn(ui::start(Arc::clone(&player), tx.clone(), args));
// Sends the player an "init" signal telling it to start playing a song straight away.
tx.send(Messages::Init).await?;
// Actually starts the player.
Player::play(Arc::clone(&player), tx.clone(), rx, args.buffer_size).await?;
Player::play(Arc::clone(&player), tx.clone(), rx).await?;
// Save the volume.txt file for the next session.
PersistentVolume::save(player.sink.volume()).await?;
drop(stream.0);
player.sink.stop();
ui.and_then(|x| Some(x.abort()));
ui.abort();
Ok(())
}

View File

@ -2,17 +2,11 @@
//! This also has the code for the underlying
//! audio server which adds new tracks.
use std::{
collections::VecDeque,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use std::{collections::VecDeque, ffi::CString, sync::Arc, time::Duration};
use arc_swap::ArcSwapOption;
use downloader::Downloader;
use libc::freopen;
use reqwest::Client;
use rodio::{OutputStream, OutputStreamHandle, Sink};
use tokio::{
@ -29,9 +23,8 @@ use tokio::{
use mpris_server::{PlaybackStatus, PlayerInterface, Property};
use crate::{
messages::Messages,
play::{PersistentVolume, SendableOutputStream},
tracks::{self, bookmark, list::List},
play::PersistentVolume,
tracks::{self, list::List},
Args,
};
@ -41,12 +34,50 @@ pub mod ui;
#[cfg(feature = "mpris")]
pub mod mpris;
/// Handles communication between the frontend & audio player.
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum Messages {
/// Notifies the audio server that it should update the track.
Next,
/// Special in that this isn't sent in a "client to server" sort of way,
/// but rather is sent by a child of the server when a song has not only
/// been requested but also downloaded aswell.
NewSong,
/// This signal is only sent if a track timed out. In that case,
/// lowfi will try again and again to retrieve the track.
TryAgain,
/// Similar to Next, but specific to the first track.
Init,
/// Unpause the [Sink].
Play,
/// Pauses the [Sink].
Pause,
/// Pauses the [Sink]. This will also unpause it if it is paused.
PlayPause,
/// Change the volume of playback.
ChangeVolume(f32),
/// Quits gracefully.
Quit,
}
/// The time to wait in between errors.
const TIMEOUT: Duration = Duration::from_secs(3);
const TIMEOUT: Duration = Duration::from_secs(5);
/// The amount of songs to buffer up.
const BUFFER_SIZE: usize = 5;
/// Main struct responsible for queuing up & playing tracks.
// TODO: Consider refactoring [Player] from being stored in an [Arc], into containing many smaller [Arc]s.
// TODO: In other words, this would change the type from `Arc<Player>` to just `Player`.
// TODO: Consider refactoring [Player] from being stored in an [Arc],
// TODO: so `Arc<Player>` into containing many smaller [Arc]s, being just
// TODO: `Player` as the type.
// TODO:
// TODO: This is conflicting, since then it'd clone ~10 smaller [Arc]s
// TODO: every single time, which could be even worse than having an
@ -55,9 +86,6 @@ pub struct Player {
/// [rodio]'s [`Sink`] which can control playback.
pub sink: Sink,
/// Whether the current track has been bookmarked.
bookmarked: AtomicBool,
/// The [`TrackInfo`] of the current track.
/// This is [`None`] when lowfi is buffering/loading.
current: ArcSwapOption<tracks::Info>,
@ -82,16 +110,23 @@ pub struct Player {
/// playback, is for now unused and is here just to keep it
/// alive so the playback can function properly.
_handle: OutputStreamHandle,
/// The [`OutputStream`], which is just here to keep the playback
/// alive and functioning.
_stream: OutputStream,
}
// SAFETY: This is necessary because [OutputStream] does not implement [Send],
// due to some limitation with Android's Audio API.
// I'm pretty sure nobody will use lowfi with android, so this is safe.
unsafe impl Send for Player {}
// SAFETY: See implementation for [Send].
unsafe impl Sync for Player {}
impl Player {
/// This gets the output stream while also shutting up alsa with [libc].
/// Uses raw libc calls, and therefore is functional only on Linux.
#[cfg(target_os = "linux")]
fn silent_get_output_stream() -> eyre::Result<(OutputStream, OutputStreamHandle)> {
use libc::freopen;
use std::ffi::CString;
// Get the file descriptor to stderr from libc.
extern "C" {
static stderr: *mut libc::FILE;
@ -101,25 +136,23 @@ impl Player {
// output to `/dev/null` so that it wont be shoved down our throats.
// The mode which to redirect terminal output with.
let mode = CString::new("w")?;
let mode = CString::new("w")?.as_ptr();
// First redirect to /dev/null, which basically silences alsa.
let null = CString::new("/dev/null")?;
let null = CString::new("/dev/null")?.as_ptr();
// SAFETY: Simple enough to be impossible to fail. Hopefully.
unsafe {
freopen(null.as_ptr(), mode.as_ptr(), stderr);
freopen(null, mode, stderr);
}
// Make the OutputStream while stderr is still redirected to /dev/null.
let (stream, handle) = OutputStream::try_default()?;
// Redirect back to the current terminal, so that other output isn't silenced.
let tty = CString::new("/dev/tty")?;
let tty = CString::new("/dev/tty")?.as_ptr();
// SAFETY: See the first call to `freopen`.
unsafe {
freopen(tty.as_ptr(), mode.as_ptr(), stderr);
freopen(tty, mode, stderr);
}
Ok((stream, handle))
@ -143,25 +176,20 @@ impl Player {
/// Initializes the entire player, including audio devices & sink.
///
/// This also will load the track list & persistent volume.
pub async fn new(args: &Args) -> eyre::Result<(Self, SendableOutputStream)> {
pub async fn new(args: &Args) -> eyre::Result<Self> {
// Load the volume file.
let volume = PersistentVolume::load().await?;
// Load the track list.
let list = List::load(args.track_list.as_ref()).await?;
let list = List::load(&args.tracks).await?;
// We should only shut up alsa forcefully on Linux if we really have to.
#[cfg(target_os = "linux")]
let (stream, handle) = if !args.alternate && !args.debug {
// We should only shut up alsa forcefully if we really have to.
let (_stream, handle) = if cfg!(target_os = "linux") && !args.alternate && !args.debug {
Self::silent_get_output_stream()?
} else {
OutputStream::try_default()?
};
// If we're not on Linux, then there's no problem.
#[cfg(not(target_os = "linux"))]
let (stream, handle) = OutputStream::try_default()?;
let sink = Sink::try_new(&handle)?;
if args.paused {
sink.pause();
@ -177,26 +205,24 @@ impl Player {
.build()?;
let player = Self {
tracks: RwLock::new(VecDeque::with_capacity(args.buffer_size)),
tracks: RwLock::new(VecDeque::with_capacity(5)),
current: ArcSwapOption::new(None),
client,
sink,
volume,
list,
_handle: handle,
bookmarked: AtomicBool::new(false),
_stream,
};
Ok((player, SendableOutputStream(stream)))
Ok(player)
}
/// This will play the next track, as well as refilling the buffer in the background.
///
/// This will also set `current` to the newly loaded song.
pub async fn next(&self) -> Result<tracks::Decoded, bool> {
// TODO: Consider replacing this with `unwrap_or_else` when async closures are stablized.
let track = self.tracks.write().await.pop_front();
let track = if let Some(track) = track {
pub async fn next(&self) -> eyre::Result<tracks::Decoded> {
let track = if let Some(track) = self.tracks.write().await.pop_front() {
track
} else {
// If the queue is completely empty, then fallback to simply getting a new track.
@ -206,10 +232,11 @@ impl Player {
// We're doing it here so that we don't get the "loading" display
// for only a frame in the other case that the buffer is not empty.
self.current.store(None);
self.list.random(&self.client).await?
};
let decoded = track.decode().map_err(|_| false)?;
let decoded = track.decode()?;
// Set the current track.
self.set_current(decoded.info.clone());
@ -247,8 +274,8 @@ impl Player {
// Notify the audio server that the next song has actually been downloaded.
tx.send(Messages::NewSong).await?;
}
Err(timeout) => {
if !timeout {
Err(error) => {
if !error.downcast::<reqwest::Error>()?.is_timeout() {
sleep(TIMEOUT).await;
}
@ -265,12 +292,10 @@ impl Player {
/// skip tracks or pause.
///
/// This will also initialize a [Downloader] as well as an MPRIS server if enabled.
/// The [Downloader]s internal buffer size is determined by `buf_size`.
pub async fn play(
player: Arc<Self>,
tx: Sender<Messages>,
mut rx: Receiver<Messages>,
buf_size: usize,
) -> eyre::Result<()> {
// Initialize the mpris player.
//
@ -286,7 +311,7 @@ impl Player {
})?;
// `itx` is used to notify the `Downloader` when it needs to download new tracks.
let downloader = Downloader::new(Arc::clone(&player), buf_size);
let downloader = Downloader::new(Arc::clone(&player));
let (itx, downloader) = downloader.start();
// Start buffering tracks immediately.
@ -393,25 +418,6 @@ impl Player {
continue;
}
Messages::Bookmark => {
if player.bookmarked.load(Ordering::Relaxed) {
continue;
}
player.bookmarked.swap(true, Ordering::Relaxed);
let current = player.current.load();
let current = current.as_ref().unwrap();
bookmark::bookmark(
current.full_path.clone(),
if current.custom_name {
Some(current.display_name.clone())
} else {
None
},
)
.await?;
}
Messages::Quit => break,
}
}

View File

@ -8,7 +8,7 @@ use tokio::{
time::sleep,
};
use super::{Player, TIMEOUT};
use super::{Player, BUFFER_SIZE, TIMEOUT};
/// This struct is responsible for downloading tracks in the background.
///
@ -24,9 +24,6 @@ pub struct Downloader {
/// A copy of the internal sender, which can be useful for keeping
/// track of it.
tx: Sender<()>,
/// The size of the internal download buffer.
buf_size: usize,
}
impl Downloader {
@ -40,14 +37,9 @@ impl Downloader {
///
/// This also sends a [`Sender`] which can be used to notify
/// when the downloader needs to begin downloading more tracks.
pub fn new(player: Arc<Player>, buf_size: usize) -> Self {
pub fn new(player: Arc<Player>) -> Self {
let (tx, rx) = mpsc::channel(8);
Self {
player,
rx,
tx,
buf_size,
}
Self { player, rx, tx }
}
/// Actually starts & consumes the [Downloader].
@ -58,12 +50,11 @@ impl Downloader {
// Loop through each update notification.
while self.rx.recv().await == Some(()) {
// For each update notification, we'll push tracks until the buffer is completely full.
while self.player.tracks.read().await.len() < self.buf_size {
let data = self.player.list.random(&self.player.client).await;
match data {
while self.player.tracks.read().await.len() < BUFFER_SIZE {
match self.player.list.random(&self.player.client).await {
Ok(track) => self.player.tracks.write().await.push_back(track),
Err(timeout) => {
if !timeout {
Err(error) => {
if !error.is_timeout() {
sleep(TIMEOUT).await;
}
}

View File

@ -1,6 +1,6 @@
//! Contains the code for the MPRIS server & other helper functions.
use std::{env, process, sync::Arc};
use std::{process, sync::Arc};
use mpris_server::{
zbus::{self, fdo, Result},
@ -9,7 +9,6 @@ use mpris_server::{
};
use tokio::sync::mpsc::Sender;
use super::ui;
use super::Messages;
const ERROR: fdo::Error = fdo::Error::Failed(String::new());
@ -168,10 +167,7 @@ impl PlayerInterface for Player {
.load()
.as_ref()
.map_or_else(Metadata::new, |track| {
let mut metadata = Metadata::builder()
.title(track.display_name.clone())
.album(self.player.list.name.clone())
.build();
let mut metadata = Metadata::builder().title(track.name.clone()).build();
metadata.set_length(
track
@ -191,7 +187,6 @@ impl PlayerInterface for Player {
async fn set_volume(&self, volume: Volume) -> Result<()> {
self.player.set_volume(volume as f32);
ui::flash_audio();
Ok(())
}
@ -267,11 +262,7 @@ impl Server {
/// Creates a new MPRIS server.
pub async fn new(player: Arc<super::Player>, sender: Sender<Messages>) -> eyre::Result<Self> {
let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") {
String::from("lowfi")
} else {
format!("lowfi.{}.instance{}", player.list.name, process::id())
};
let suffix = format!("lowfi.{}.instance{}", player.list.name, process::id());
let server = mpris_server::Server::new(&suffix, Player { player, sender }).await?;

View File

@ -1,15 +1,7 @@
//! The module which manages all user interface, including inputs.
#![allow(
clippy::as_conversions,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "the ui is full of these because of various layout & positioning aspects, and for a simple music player making all casts safe is not worth the effort"
)]
use std::{
fmt::Write as _,
fmt::Write,
io::{stdout, Stdout},
sync::{
atomic::{AtomicUsize, Ordering},
@ -23,13 +15,12 @@ use crate::Args;
use crossterm::{
cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show},
event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags},
style::{Print, Stylize as _},
style::{Print, Stylize},
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use lazy_static::lazy_static;
use tokio::{sync::mpsc::Sender, task, time::sleep};
use unicode_segmentation::UnicodeSegmentation;
use super::{Messages, Player};
@ -57,83 +48,57 @@ lazy_static! {
static ref VOLUME_TIMER: AtomicUsize = AtomicUsize::new(0);
}
/// Sets the volume timer to one, effectively flashing the audio display in lowfi's UI.
///
/// The amount of frames the audio display is visible for is determined by [`AUDIO_BAR_DURATION`].
pub fn flash_audio() {
VOLUME_TIMER.store(1, Ordering::Relaxed);
}
/// Represents an abstraction for drawing the actual lowfi window itself.
///
/// The main purpose of this struct is just to add the fancy border,
/// as well as clear the screen before drawing.
pub struct Window {
/// Whether or not to include borders in the output.
borderless: bool,
/// The top & bottom borders, which are here since they can be
/// prerendered, as they don't change from window to window.
///
/// If the option to not include borders is set, these will just be empty [String]s.
borders: [String; 2],
/// The width of the window.
width: usize,
/// The output, currently just an [`Stdout`].
out: Stdout,
}
impl Window {
/// Initializes a new [Window].
///
/// * `width` - Width of the windows.
/// * `borderless` - Whether to include borders in the window, or not.
pub fn new(width: usize, borderless: bool) -> Self {
let borders = if borderless {
[String::new(), String::new()]
} else {
let middle = "".repeat(width + 2);
[format!("{middle}"), format!("{middle}")]
};
pub fn new(width: usize) -> Self {
Self {
borders,
borderless,
width,
borders: [
format!("{}\r\n", "".repeat(width + 2)),
// This one doesn't have a leading \r\n to avoid extra space under the window.
format!("{}", "".repeat(width + 2)),
],
out: stdout(),
}
}
/// Actually draws the window, with each element in `content` being on a new line.
pub fn draw(&mut self, content: Vec<String>, space: bool) -> eyre::Result<()> {
let len: u16 = content.len().try_into()?;
pub fn draw(&mut self, content: Vec<String>) -> eyre::Result<()> {
let len = content.len() as u16;
// Note that this will have a trailing newline, which we use later.
let menu: String = content.into_iter().fold(String::new(), |mut output, x| {
// Horizontal Padding & Border
let padding = if self.borderless { " " } else { "" };
let space = if space {
" ".repeat(self.width.saturating_sub(x.graphemes(true).count()))
} else {
String::new()
};
write!(output, "{padding} {}{space} {padding}\r\n", x.reset()).unwrap();
write!(output, "│ {} │\r\n", x.reset()).unwrap();
output
});
// We're doing this because Windows is stupid and can't stand
// writing to the last line repeatedly.
// writing to the last line repeatedly. Again, it's stupid.
#[cfg(windows)]
let (height, suffix) = (len + 2, "\r\n");
#[cfg(not(windows))]
let (height, suffix) = (len + 1, "");
let (rendered, height) = (
format!("{}{}{}\r\n", self.borders[0], menu, self.borders[1]),
len + 2,
);
// There's no need for another newline after the main menu content, because it already has one.
let rendered = format!("{}\r\n{menu}{}{suffix}", self.borders[0], self.borders[1]);
// Unix has no such ridiculous limitations, so we calculate
// the height of the window accurately.
#[cfg(not(windows))]
let (rendered, height) = (
format!("{}{}{}", self.borders[0], menu, self.borders[1]),
len + 1,
);
crossterm::execute!(
self.out,
@ -151,15 +116,9 @@ impl Window {
/// The code for the terminal interface itself.
///
/// * `minimalist` - All this does is hide the bottom control bar.
/// * `borderless` - Whether to include borders or not.
/// * `width` - The width of player
async fn interface(
player: Arc<Player>,
minimalist: bool,
borderless: bool,
width: usize,
) -> eyre::Result<()> {
let mut window = Window::new(width, borderless);
async fn interface(player: Arc<Player>, minimalist: bool, width: usize) -> eyre::Result<()> {
let mut window = Window::new(width);
loop {
// Load `current` once so that it doesn't have to be loaded over and over
@ -181,7 +140,7 @@ async fn interface(
if timer > 0 && timer <= AUDIO_BAR_DURATION {
// We'll keep increasing the timer until it eventually hits `AUDIO_BAR_DURATION`.
VOLUME_TIMER.fetch_add(1, Ordering::Relaxed);
} else {
} else if timer > AUDIO_BAR_DURATION {
// If enough time has passed, we'll reset it back to 0.
VOLUME_TIMER.store(0, Ordering::Relaxed);
}
@ -194,7 +153,7 @@ async fn interface(
vec![action, middle, controls]
};
window.draw(menu, false)?;
window.draw(menu)?;
sleep(Duration::from_secs_f32(FRAME_DELTA)).await;
}
@ -278,7 +237,6 @@ pub async fn start(player: Arc<Player>, sender: Sender<Messages>, args: Args) ->
let interface = task::spawn(interface(
Arc::clone(&player),
args.minimalist,
args.borderless,
21 + args.width.min(32) * 2,
));

View File

@ -1,14 +1,10 @@
//! Various different individual components that
//! appear in lowfi's UI, like the progress bar.
use std::{
ops::Deref as _,
sync::{atomic::Ordering, Arc},
time::Duration,
};
use std::{ops::Deref, sync::Arc, time::Duration};
use crossterm::style::Stylize as _;
use unicode_segmentation::UnicodeSegmentation as _;
use crossterm::style::Stylize;
use unicode_segmentation::UnicodeSegmentation;
use crate::{player::Player, tracks::Info};
@ -76,21 +72,16 @@ enum ActionBar {
impl ActionBar {
/// Formats the action bar to be displayed.
/// The second value is the character length of the result.
fn format(&self, star: bool) -> (String, usize) {
fn format(&self) -> (String, usize) {
let (word, subject) = match self {
Self::Playing(x) => ("playing", Some((x.display_name.clone(), x.width))),
Self::Paused(x) => ("paused", Some((x.display_name.clone(), x.width))),
Self::Playing(x) => ("playing", Some((x.name.clone(), x.width))),
Self::Paused(x) => ("paused", Some((x.name.clone(), x.width))),
Self::Loading => ("loading", None),
};
subject.map_or_else(
|| (word.to_owned(), word.len()),
|(subject, len)| {
(
format!("{} {}{}", word, if star { "*" } else { "" }, subject.bold()),
word.len() + 1 + len + if star { 1 } else { 0 },
)
},
|(subject, len)| (format!("{} {}", word, subject.bold()), word.len() + 1 + len),
)
}
}
@ -108,12 +99,12 @@ pub fn action(player: &Player, current: Option<&Arc<Info>>, width: usize) -> Str
ActionBar::Playing(info)
}
})
.format(player.bookmarked.load(Ordering::Relaxed));
.format();
if len > width {
let chopped: String = main.graphemes(true).take(width + 1).collect();
format!("{chopped}...")
format!("{}...", chopped)
} else {
format!("{}{}", main, " ".repeat(width - len))
}

View File

@ -1,11 +1,15 @@
//! Responsible for specifically recieving terminal input
//! using [`crossterm`].
use std::sync::atomic::Ordering;
use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers};
use futures::{FutureExt as _, StreamExt as _};
use futures::{FutureExt, StreamExt};
use tokio::sync::mpsc::Sender;
use crate::player::{ui, Messages};
use crate::player::Messages;
use super::VOLUME_TIMER;
/// Starts the listener to recieve input from the terminal for various events.
pub async fn listen(sender: Sender<Messages>) -> eyre::Result<()> {
@ -34,17 +38,14 @@ pub async fn listen(sender: Sender<Messages>) -> eyre::Result<()> {
'q' => Messages::Quit,
// Skip/Next
's' | 'n' | 'l' => Messages::Next,
's' | 'n' => Messages::Next,
// Pause
'p' | ' ' => Messages::PlayPause,
'p' => Messages::PlayPause,
// Volume up & down
'+' | '=' | 'k' => Messages::ChangeVolume(0.1),
'-' | '_' | 'j' => Messages::ChangeVolume(-0.1),
// Bookmark
'b' => Messages::Bookmark,
'+' | '=' => Messages::ChangeVolume(0.1),
'-' | '_' => Messages::ChangeVolume(-0.1),
_ => continue,
},
@ -63,8 +64,10 @@ pub async fn listen(sender: Sender<Messages>) -> eyre::Result<()> {
_ => continue,
};
// If it's modifying the volume, then we'll set the `VOLUME_TIMER` to 1
// so that the UI thread will know that it should show the audio bar.
if let Messages::ChangeVolume(_) = messages {
ui::flash_audio();
VOLUME_TIMER.store(1, Ordering::Relaxed);
}
sender.send(messages).await?;

View File

@ -1,85 +1,28 @@
//! Has all of the structs for managing the state
//! of tracks, as well as downloading them & finding new ones.
//!
//! There are several structs which represent the different stages
//! that go on in downloading and playing tracks. The proccess for fetching tracks,
//! and what structs are relevant in each step, are as follows.
//!
//! First Stage, when a track is initially fetched.
//! 1. Raw entry selected from track list.
//! 2. Raw entry split into path & display name.
//! 3. Track data fetched, and [`Track`] is created which includes a [`TrackName`] that may be raw.
//!
//! Second Stage, when a track is played.
//! 1. Track data is decoded.
//! 2. [`Info`] created from decoded data.
//! 3. [`Decoded`] made from [`Info`] and the original decoded data.
//! of tracks, as well as downloading them &
//! finding new ones.
use std::{io::Cursor, time::Duration};
use bytes::Bytes;
use eyre::OptionExt as _;
use inflector::Inflector as _;
use rodio::{Decoder, Source as _};
use unicode_segmentation::UnicodeSegmentation;
use inflector::Inflector;
use rodio::{Decoder, Source};
use unicode_width::UnicodeWidthStr;
use url::form_urlencoded;
pub mod bookmark;
pub mod list;
/// Just a shorthand for a decoded [Bytes].
pub type DecodedData = Decoder<Cursor<Bytes>>;
/// Specifies a track's name, and specifically,
/// whether it has already been formatted or if it
/// is still in it's raw path form.
#[derive(Debug, Clone)]
pub enum TrackName {
/// Pulled straight from the list,
/// with no splitting done at all.
Raw(String),
/// If a track has a custom specified name
/// in the list, then it should be defined with this variant.
Formatted(String),
}
/// The main track struct, which only includes data & the track name.
pub struct Track {
/// Name of the track, which may be raw.
pub name: TrackName,
/// Full downloadable path/url of the track.
pub full_path: String,
/// The raw data of the track, which is not decoded and
/// therefore much more memory efficient.
pub data: Bytes,
}
impl Track {
/// This will actually decode and format the track,
/// returning a [`DecodedTrack`] which can be played
/// and also has a duration & formatted name.
pub fn decode(self) -> eyre::Result<Decoded> {
Decoded::new(self)
}
}
/// The [`Info`] struct, which has the name and duration of a track.
///
/// This is not included in [Track] as the duration has to be acquired
/// from the decoded data and not from the raw data.
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Info {
/// The full downloadable path/url of the track.
pub full_path: String,
/// Whether the track entry included a custom name, or not.
pub custom_name: bool,
/// This is a formatted name, so it doesn't include the full path.
pub display_name: String,
pub name: String,
/// This is the *actual* terminal width of the track name, used to make
/// the UI consistent.
@ -93,10 +36,6 @@ pub struct Info {
impl Info {
/// Decodes a URL string into normal UTF-8.
fn decode_url(text: &str) -> String {
#[expect(
clippy::tuple_array_conversions,
reason = "the tuple contains smart pointers, so it's not really practical to use `into()`"
)]
form_urlencoded::parse(text.as_bytes())
.map(|(key, val)| [key, val].concat())
.collect()
@ -105,26 +44,26 @@ impl Info {
/// Formats a name with [Inflector].
/// This will also strip the first few numbers that are
/// usually present on most lofi tracks.
fn format_name(name: &str) -> eyre::Result<String> {
let split = name
.split('/')
.last()
.ok_or_eyre("split is never supposed to return nothing")?;
let stripped = split.strip_suffix(".mp3").unwrap_or(split);
let formatted = Self::decode_url(stripped)
.to_lowercase()
.to_title_case()
// Inflector doesn't like contractions...
// Replaces a few very common ones.
// TODO: Properly handle these.
.replace(" S ", "'s ")
.replace(" T ", "'t ")
.replace(" D ", "'d ")
.replace(" Ve ", "'ve ")
.replace(" Ll ", "'ll ")
.replace(" Re ", "'re ")
.replace(" M ", "'m ");
fn format_name(name: &str) -> String {
let formatted = Self::decode_url(
name.split('/')
.last()
.unwrap()
.strip_suffix(".mp3")
.unwrap(),
)
.to_lowercase()
.to_title_case()
// Inflector doesn't like contractions...
// Replaces a few very common ones.
// TODO: Properly handle these.
.replace(" S ", "'s ")
.replace(" T ", "'t ")
.replace(" D ", "'d ")
.replace(" Ve ", "'ve ")
.replace(" Ll ", "'ll ")
.replace(" Re ", "'re ")
.replace(" M ", "'m ");
// This is incremented for each digit in front of the song name.
let mut skip = 0;
@ -137,32 +76,19 @@ impl Info {
}
}
// If the entire name of the track is a number, then just return it.
if skip == formatted.len() {
Ok(formatted)
} else {
#[expect(
clippy::string_slice,
reason = "We've already checked before that the bound is at an ASCII digit."
)]
Ok(String::from(&formatted[skip..]))
}
#[allow(clippy::string_slice, /* We've already checked before that the bound is at an ASCII digit. */)]
String::from(&formatted[skip..])
}
/// Creates a new [`TrackInfo`] from a possibly raw name & decoded data.
pub fn new(name: TrackName, full_path: String, decoded: &DecodedData) -> eyre::Result<Self> {
let (display_name, custom_name) = match name {
TrackName::Raw(raw) => (Self::format_name(&raw)?, false),
TrackName::Formatted(custom) => (custom, true),
};
/// Creates a new [`TrackInfo`] from a raw name & decoded track data.
pub fn new(name: &str, decoded: &DecodedData) -> Self {
let name = Self::format_name(name);
Ok(Self {
Self {
duration: decoded.total_duration(),
width: display_name.graphemes(true).count(),
full_path,
custom_name,
display_name,
})
width: name.width(),
name,
}
}
}
@ -181,8 +107,27 @@ impl Decoded {
/// This is equivalent to [`Track::decode`].
pub fn new(track: Track) -> eyre::Result<Self> {
let data = Decoder::new(Cursor::new(track.data))?;
let info = Info::new(track.name, track.full_path, &data)?;
let info = Info::new(&track.name, &data);
Ok(Self { info, data })
}
}
/// The main track struct, which only includes data & the track name.
pub struct Track {
/// This name is not formatted, and also includes the month & year of the track.
pub name: String,
/// The raw data of the track, which is not decoded and
/// therefore much more memory efficient.
pub data: Bytes,
}
impl Track {
/// This will actually decode and format the track,
/// returning a [`DecodedTrack`] which can be played
/// and also has a duration & formatted name.
pub fn decode(self) -> eyre::Result<Decoded> {
Decoded::new(self)
}
}

View File

@ -1,27 +0,0 @@
use tokio::fs::{create_dir_all, OpenOptions};
use tokio::io::AsyncWriteExt;
use crate::data_dir;
pub async fn bookmark(path: String, custom: Option<String>) -> eyre::Result<()> {
let mut entry = format!("\n{path}");
if let Some(custom) = custom {
entry.push('!');
entry.push_str(&custom);
}
let data_dir = data_dir()?;
create_dir_all(data_dir.clone()).await?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(data_dir.join("bookmarks.txt"))
.await?;
file.write_all(entry.as_bytes()).await?;
Ok(())
}

View File

@ -2,13 +2,11 @@
//! as well as obtaining track names & downloading the raw mp3 data.
use bytes::Bytes;
use eyre::OptionExt as _;
use rand::Rng as _;
use eyre::OptionExt;
use rand::Rng;
use reqwest::Client;
use tokio::fs;
use crate::data_dir;
use super::Track;
/// Represents a list of tracks that can be played.
@ -17,7 +15,6 @@ use super::Track;
#[derive(Clone)]
pub struct List {
/// The "name" of the list, usually derived from a filename.
#[allow(dead_code, reason = "this code may not be dead depending on features")]
pub name: String,
/// Just the raw file, but seperated by `/n` (newlines).
@ -31,84 +28,45 @@ impl List {
self.lines[0].trim()
}
/// Gets the path of a random track.
///
/// The second value in the tuple specifies whether the
/// track has a custom display name.
fn random_path(&self) -> (String, Option<String>) {
/// Gets the name of a random track.
fn random_name(&self) -> String {
// We're getting from 1 here, since the base is at `self.lines[0]`.
//
// We're also not pre-trimming `self.lines` into `base` & `tracks` due to
// how rust vectors work, since it is slower to drain only a single element from
// how rust vectors work, sinceslow to drain only a single element from
// the start, so it's faster to just keep it in & work around it.
let random = rand::thread_rng().gen_range(1..self.lines.len());
let line = self.lines[random].clone();
if let Some((first, second)) = line.split_once('!') {
(first.to_owned(), Some(second.to_owned()))
} else {
(line, None)
}
self.lines[random].clone()
}
/// Downloads a raw track, but doesn't decode it.
async fn download(&self, track: &str, client: &Client) -> Result<(Bytes, String), bool> {
async fn download(&self, track: &str, client: &Client) -> reqwest::Result<Bytes> {
// If the track has a protocol, then we should ignore the base for it.
let full_path = if track.contains("://") {
let url = if track.contains("://") {
track.to_owned()
} else {
format!("{}{}", self.base(), track)
};
let data: Bytes = if let Some(x) = full_path.strip_prefix("file://") {
let path = if x.starts_with("~") {
let home_path = dirs::home_dir().ok_or(false)?;
let home = home_path.to_str().ok_or(false)?;
let response = client.get(url).send().await?;
let data = response.bytes().await?;
x.replace("~", home)
} else {
x.to_owned()
};
let result = tokio::fs::read(path).await.map_err(|_| false)?;
result.into()
} else {
let response = client
.get(full_path.clone())
.send()
.await
.map_err(|x| x.is_timeout())?;
response.bytes().await.map_err(|_| false)?
};
Ok((data, full_path))
Ok(data)
}
/// Fetches and downloads a random track from the [List].
///
/// The Result's error is a bool, which is true if a timeout error occured,
/// and false otherwise. This tells lowfi if it shouldn't wait to try again.
pub async fn random(&self, client: &Client) -> Result<Track, bool> {
let (path, custom_name) = self.random_path();
let (data, full_path) = self.download(&path, client).await?;
pub async fn random(&self, client: &Client) -> reqwest::Result<Track> {
let name = self.random_name();
let data = self.download(&name, client).await?;
let name = custom_name.map_or(super::TrackName::Raw(path.clone()), |formatted| {
super::TrackName::Formatted(formatted)
});
Ok(Track {
name,
data,
full_path,
})
Ok(Track { name, data })
}
/// Parses text into a [List].
pub fn new(name: &str, text: &str) -> Self {
let lines: Vec<String> = text
.trim()
.lines()
.map(|x| x.trim_end().to_owned())
.split_ascii_whitespace()
.map(ToOwned::to_owned)
.collect();
Self {
@ -118,10 +76,13 @@ impl List {
}
/// Reads a [List] from the filesystem using the CLI argument provided.
pub async fn load(tracks: Option<&String>) -> eyre::Result<Self> {
pub async fn load(tracks: &Option<String>) -> eyre::Result<Self> {
if let Some(arg) = tracks {
// Check if the track is in ~/.local/share/lowfi, in which case we'll load that.
let name = data_dir()?.join(format!("{arg}.txt"));
let name = dirs::data_dir()
.unwrap()
.join("lowfi")
.join(format!("{}.txt", arg));
let name = if name.exists() { name } else { arg.into() };