diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2d7beeab..48051a35 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "vuio", "displayName": "VuIO Media Server", "description": "Browse, search and cast your VuIO media library — and control the TVs and speakers on your network — from Claude.", - "version": "0.0.49", + "version": "0.0.50", "author": { "name": "vyrti", "url": "https://github.com/vuiodev" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcc1d481..dbb93a1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,7 +143,7 @@ jobs: - name: Run Clippy env: RUSTC_WRAPPER: sccache - run: cargo clippy -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web --all-targets --all-features -- -D clippy::all -D warnings + run: cargo clippy -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web --all-targets --all-features -- -D clippy::all -D clippy::string_slice -D warnings - name: Show sccache stats if: always() run: sccache --show-stats || true diff --git a/Cargo.lock b/Cargo.lock index 7287ec14..96941c8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,7 +122,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -216,6 +216,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -224,9 +230,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -272,9 +278,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.4.3" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "shlex", @@ -366,7 +372,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -389,9 +395,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", @@ -425,6 +431,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -436,27 +448,27 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +checksum = "e71406cd8807725f7ac2f999a4cdd32e98f829fdf65f528343cebf945e41df1e" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -467,18 +479,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -486,27 +498,27 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crypto-common" @@ -605,9 +617,9 @@ dependencies = [ [[package]] name = "dirs" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" dependencies = [ "dirs-sys", ] @@ -632,7 +644,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -662,17 +674,23 @@ dependencies = [ [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -732,18 +750,19 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -804,7 +823,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -897,14 +916,14 @@ dependencies = [ [[package]] name = "hap-transport" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fcc34631adc245cd0f9614a6c7b4aaa210e462813a2a206f21e7524ddfb1b6" +checksum = "645e484ca0c495ae17b16ed1091f55f94565c90e0e25e1360f9efbebfc986009" dependencies = [ - "base64", + "base64 0.22.1", "hap-crypto", "hap-tlv8", - "mdns-sd 0.20.3", + "mdns-sd", "thiserror 2.0.20", "tokio", ] @@ -929,9 +948,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" dependencies = [ "hashbrown 0.17.1", ] @@ -1042,18 +1061,18 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -1091,7 +1110,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1202,9 +1221,9 @@ checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -1259,28 +1278,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "inotify" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" dependencies = [ "bitflags", "inotify-sys", @@ -1307,9 +1317,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is_terminal_polyfill" @@ -1383,9 +1393,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", @@ -1436,9 +1446,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.20" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" dependencies = [ "libc", ] @@ -1484,9 +1494,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "matchers" @@ -1505,24 +1515,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "mdns-sd" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86dbb9f00c8c367f75ed3a775d3eb31d0375a72f58275ef64a1bc53c255a2ce2" -dependencies = [ - "fastrand", - "flume", - "if-addrs", - "log", - "mio", - "socket-pktinfo", - "socket2", -] - -[[package]] -name = "mdns-sd" -version = "0.21.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba97b4c886eea3c2823388120d92063c011ac866919ffc4200a0e4b1642a54a5" +checksum = "a63c9b854b5ad0812ac5969f8db92a59f28b45d0a5ae599b9a45b84c5c8a08e8" dependencies = [ "fastrand", "flume", @@ -1539,15 +1534,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "metaflac" version = "0.2.8" @@ -1566,9 +1552,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -1576,9 +1562,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", @@ -1619,6 +1605,33 @@ dependencies = [ "version_check", ] +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "notify" version = "8.2.0" @@ -1841,11 +1854,11 @@ checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plist" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" dependencies = [ - "base64", + "base64 0.23.1", "indexmap", "quick-xml", "serde", @@ -1927,35 +1940,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.27.2" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" dependencies = [ - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.27.2" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.27.2" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" dependencies = [ "libc", "pyo3-build-config", @@ -1963,9 +1973,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.27.2" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1975,22 +1985,21 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.27.2" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn 2.0.119", ] [[package]] name = "quick-xml" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" dependencies = [ "memchr", ] @@ -2090,11 +2099,11 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64", + "base64 0.23.1", "bytes", "futures-core", "http", @@ -2201,9 +2210,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -2264,9 +2273,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.14" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -2365,7 +2374,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -2431,7 +2440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] @@ -2499,9 +2508,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "socket-pktinfo" @@ -2790,9 +2799,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -2889,7 +2898,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -2943,6 +2952,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "tokio" version = "1.53.1" @@ -2968,14 +2983,14 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ "rustls", "tokio", @@ -3008,9 +3023,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.4+spec-1.1.0" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ "indexmap", "serde_core", @@ -3032,9 +3047,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.13+spec-1.1.0" +version = "0.25.15+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" dependencies = [ "indexmap", "toml_datetime", @@ -3196,10 +3211,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unindent" -version = "0.2.4" +name = "unicode-normalization" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] [[package]] name = "universal-hash" @@ -3243,9 +3261,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -3272,7 +3290,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vuio-bench" -version = "0.0.49" +version = "0.0.50" dependencies = [ "anyhow", "clap", @@ -3291,7 +3309,7 @@ version = "0.0.4" dependencies = [ "axum", "bytes", - "mdns-sd 0.21.0", + "mdns-sd", "prost", "rustls", "rustls-native-certs", @@ -3307,7 +3325,7 @@ dependencies = [ [[package]] name = "vuio-cli" -version = "0.0.49" +version = "0.0.50" dependencies = [ "anyhow", "audiotags", @@ -3348,7 +3366,7 @@ dependencies = [ [[package]] name = "vuio-core" -version = "0.0.49" +version = "0.0.50" dependencies = [ "anyhow", "async-stream", @@ -3373,7 +3391,7 @@ dependencies = [ "ipnet", "jwalk", "libc", - "mdns-sd 0.21.0", + "mdns-sd", "notify", "notify-debouncer-full", "num-bigint 0.5.1", @@ -3399,6 +3417,7 @@ dependencies = [ "tower", "tracing", "tracing-subscriber", + "unicode-normalization", "uuid", "vuio-cast", "vuio-codec-ac3", @@ -3412,7 +3431,7 @@ dependencies = [ [[package]] name = "vuio-web" -version = "0.0.49" +version = "0.0.50" dependencies = [ "axum", ] @@ -3444,9 +3463,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -3457,9 +3476,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.77" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -3467,9 +3486,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3477,31 +3496,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -3925,9 +3944,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -3936,15 +3955,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.23" diff --git a/claude/mcpb/manifest.json b/claude/mcpb/manifest.json index 384e9d4a..678504b5 100644 --- a/claude/mcpb/manifest.json +++ b/claude/mcpb/manifest.json @@ -2,7 +2,7 @@ "manifest_version": "0.3", "name": "vuio", "display_name": "VuIO Media Server", - "version": "0.0.49", + "version": "0.0.50", "description": "Browse, search and cast your VuIO media library from Claude.", "long_description": "Connects Claude to a VuIO media server on your network. Search the library, browse folders, build playlists, and cast to DLNA, Chromecast and AirPlay devices.\n\nThis bundle runs `vuio mcp`, which bridges Claude's stdio connection to a VuIO server that is already running. It does not start a server or open the library database itself — point it at the machine that does.", "author": { diff --git a/claude/plugin/.claude-plugin/plugin.json b/claude/plugin/.claude-plugin/plugin.json index ef7413d2..aa4a9100 100644 --- a/claude/plugin/.claude-plugin/plugin.json +++ b/claude/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "vuio", "displayName": "VuIO Media Server", - "version": "0.0.49", + "version": "0.0.50", "description": "Browse, search and cast your VuIO media library — and control the TVs and speakers on your network — from Claude.", "author": { "name": "vyrti", diff --git a/crates/vendor/libxaac-sys/Cargo.toml b/crates/vendor/libxaac-sys/Cargo.toml index 27e7d5d6..92a4fd4b 100644 --- a/crates/vendor/libxaac-sys/Cargo.toml +++ b/crates/vendor/libxaac-sys/Cargo.toml @@ -67,4 +67,4 @@ path = "examples/sample.rs" [dependencies] [build-dependencies] -cc = "1.0" +cc = "1.4" diff --git a/crates/vendor/xaac-rs/Cargo.toml b/crates/vendor/xaac-rs/Cargo.toml index d3a0eb19..830147fa 100644 --- a/crates/vendor/xaac-rs/Cargo.toml +++ b/crates/vendor/xaac-rs/Cargo.toml @@ -76,6 +76,6 @@ version = "0.1.0" path = "../libxaac-sys" [dependencies.pyo3] -version = "0.27" +version = "0.29" features = ["extension-module"] optional = true diff --git a/crates/vuio-bench/Cargo.toml b/crates/vuio-bench/Cargo.toml index 4bfb688a..b0228283 100644 --- a/crates/vuio-bench/Cargo.toml +++ b/crates/vuio-bench/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vuio-bench" -version = "0.0.49" +version = "0.0.50" edition = "2021" authors = ["vyrti"] description = "Generates large VuIO libraries for performance work. Not published." @@ -37,7 +37,7 @@ tokio = { version = "1.53", features = ["rt-multi-thread", "macros"] } # because it opens every internal module and carries no stability promise. This # crate is `publish = false` and exists only to drive the database from the # inside, which is the same category as core's own dev-dependency on itself. -vuio-core = { path = "../vuio-core", version = "0.0.49", features = ["unstable-internals", "transcode-aac", "transcode-ac3", "transcode-dts"] } +vuio-core = { path = "../vuio-core", version = "0.0.50", features = ["unstable-internals", "transcode-aac", "transcode-ac3", "transcode-dts"] } vuio-codec-ac3 = { path = "../vuio-codec-ac3" } oxideav-dts = { package = "vuio-codec-dts", path = "../vuio-codec-dts" } oxideav-core = { package = "vuio-codec-core", path = "../vuio-codec-core" } diff --git a/crates/vuio-cli/Cargo.toml b/crates/vuio-cli/Cargo.toml index 8cc973f4..21786029 100644 --- a/crates/vuio-cli/Cargo.toml +++ b/crates/vuio-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vuio-cli" -version = "0.0.49" +version = "0.0.50" edition = "2021" rust-version = "1.98" authors = ["vyrti"] @@ -41,6 +41,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.53", features = ["rt-multi-thread", "macros", "signal", "io-std", "io-util"] } tracing = "0.1" -uuid = { version = "1.24", features = ["v4"] } -vuio-core = { path = "../vuio-core", version = "0.0.49" } +uuid = { version = "1.26", features = ["v4"] } +vuio-core = { path = "../vuio-core", version = "0.0.50" } diff --git a/crates/vuio-codec-ac3/Cargo.toml b/crates/vuio-codec-ac3/Cargo.toml index e4ae88a5..fe23d101 100644 --- a/crates/vuio-codec-ac3/Cargo.toml +++ b/crates/vuio-codec-ac3/Cargo.toml @@ -24,7 +24,7 @@ include = [ [dependencies] oxideav-core = { package = "vuio-codec-core", version = "0.0.1", path = "../vuio-codec-core" } -rayon = "1.10" +rayon = "1.12" [lints.rust] warnings = "allow" diff --git a/crates/vuio-codec-core/src/lib.rs b/crates/vuio-codec-core/src/lib.rs index 15760f3b..af1b5533 100644 --- a/crates/vuio-codec-core/src/lib.rs +++ b/crates/vuio-codec-core/src/lib.rs @@ -7,6 +7,7 @@ //! source / filter) into a single value that consumers pass around. #![warn(missing_docs)] +#![deny(clippy::string_slice)] pub mod arena; pub mod bits; diff --git a/crates/vuio-codec-core/src/registry/source.rs b/crates/vuio-codec-core/src/registry/source.rs index 35bf2e62..a3b9cc29 100644 --- a/crates/vuio-codec-core/src/registry/source.rs +++ b/crates/vuio-codec-core/src/registry/source.rs @@ -309,10 +309,7 @@ fn dispatch(entry: &OpenerEntry, uri_str: &str) -> Result { /// `"file"` and `rest = uri`. Path-like inputs that happen to start with /// `c:` on Windows are treated as bare paths. pub(crate) fn split_scheme(uri: &str) -> (&str, &str) { - if let Some(idx) = uri.find(':') { - let (scheme, rest) = uri.split_at(idx); - let rest = &rest[1..]; // skip ':' - + if let Some((scheme, rest)) = uri.split_once(':') { // Reject single-letter scheme that looks like a Windows drive letter. if scheme.len() == 1 && scheme.chars().next().unwrap().is_ascii_alphabetic() { return ("file", uri); diff --git a/crates/vuio-core/Cargo.toml b/crates/vuio-core/Cargo.toml index 3ba5fc5e..a7eef154 100644 --- a/crates/vuio-core/Cargo.toml +++ b/crates/vuio-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vuio-core" -version = "0.0.49" +version = "0.0.50" edition = "2021" rust-version = "1.98" authors = ["vyrti"] @@ -137,7 +137,7 @@ unstable-internals = [] axum = { version = "0.8", features = ["multipart"] } tokio = { version = "1.53", features = ["rt-multi-thread", "net", "fs", "time", "sync", "macros", "io-util"] } serde = { version = "1.0", features = ["derive"] } -uuid = { version = "1.24", features = ["v4"] } +uuid = { version = "1.26", features = ["v4"] } tokio-util = { version = "0.7", features = ["io", "rt"] } thiserror = "2.0" anyhow = "1.0" @@ -153,7 +153,7 @@ notify-debouncer-full = "0.7" toml = "1.1" toml_edit = "0.25" serde_json = "1.0" -dirs = "6.0" +dirs = "7.0" hostname = "0.4" ipnet = "2.12" http = "1.5" @@ -162,13 +162,16 @@ hyper = { version = "1.11", features = ["client", "http1"] } hyper-util = { version = "0.1", features = ["client-legacy", "http1", "tokio"] } bytes = "1.12" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } -quick-xml = "0.41" +quick-xml = "0.42" percent-encoding = "2.3" +# Media text arrives in whichever normalization form the tagger or filesystem +# used; comparing and indexing it requires one form. See `src/text.rs`. +unicode-normalization = "0.1" sysinfo = { version = "0.39", default-features = false, features = ["system", "disk", "network"], optional = true } socket2 = { version = "0.6", features = ["all"] } mdns-sd = { version = "0.21", default-features = false, features = ["async"] } vuio-cast = { path = "../vuio-cast", version = "0.0.4", default-features = false, optional = true } -vuio-web = { path = "../vuio-web", version = "0.0.49", optional = true } +vuio-web = { path = "../vuio-web", version = "0.0.50", optional = true } jwalk = "0.9" tokio-stream = "0.1" hap-crypto = { version = "1.4", optional = true } @@ -190,7 +193,8 @@ oxideav-core = { package = "vuio-codec-core", version = "0.0.1", path = "../vuio vuio-codec-ac3 = { version = "0.0.1", path = "../vuio-codec-ac3", optional = true } oxideav-dts = { package = "vuio-codec-dts", version = "0.0.1", path = "../vuio-codec-dts", optional = true } xaac-rs = { version = "0.2", path = "../vendor/xaac-rs", optional = true } -rusqlite = { version = "0.40.2", features = ["bundled", "collation"] } +# `functions` registers the `nfc()` scalar the normalization migration uses. +rusqlite = { version = "0.40.2", features = ["bundled", "collation", "functions"] } # Only the `mediainfo` feature uses this, and only to talk to public metadata APIs # over TLS. `rustls-no-provider` rather than a provider-selecting feature because # `Runtime::start` already installs the ring provider process-wide, and a second @@ -206,7 +210,7 @@ windows = { version = "0.62", features = [ ] } [dev-dependencies] -vuio-core = { path = ".", version = "0.0.49", features = ["unstable-internals"] } +vuio-core = { path = ".", version = "0.0.50", features = ["unstable-internals"] } # Symphonia reads tags but cannot write them, and the audio tests build their # fixtures by tagging generated files. Test-only, so it ships with nothing. audiotags = "0.5" diff --git a/crates/vuio-core/src/casting/airplay/mod.rs b/crates/vuio-core/src/casting/airplay/mod.rs index f26f8183..c17fe775 100644 --- a/crates/vuio-core/src/casting/airplay/mod.rs +++ b/crates/vuio-core/src/casting/airplay/mod.rs @@ -2170,10 +2170,10 @@ fn parse_playback_info(xml: &str) -> anyhow::Result> { loop { match reader.read_event()? { Event::Start(element) => { - current = String::from_utf8_lossy(element.name().as_ref()).into_owned(); + current = element.name().as_ref().to_owned(); } Event::Text(text) => { - let value = reader.decoder().decode(text.as_ref())?.into_owned(); + let value = text.as_ref().to_owned(); match current.as_str() { "key" => pending_key = Some(value), "real" | "integer" => { diff --git a/crates/vuio-core/src/casting/airplay/raop.rs b/crates/vuio-core/src/casting/airplay/raop.rs index 157d6847..d7f49ea1 100644 --- a/crates/vuio-core/src/casting/airplay/raop.rs +++ b/crates/vuio-core/src/casting/airplay/raop.rs @@ -460,15 +460,15 @@ mod tests { bits.push_str(&format!("{byte:08b}")); } assert!(bits.starts_with("001"), "element type must be CPE: {bits}"); - assert_eq!(&bits[3..7], "0000"); - assert_eq!(&bits[7..19], "000000000000"); - assert_eq!(&bits[19..20], "0", "hasSize"); - assert_eq!(&bits[20..22], "00"); - assert_eq!(&bits[22..23], "1", "isNotCompressed"); + assert_eq!(bits.get(3..7), Some("0000")); + assert_eq!(bits.get(7..19), Some("000000000000")); + assert_eq!(bits.get(19..20), Some("0"), "hasSize"); + assert_eq!(bits.get(20..22), Some("00")); + assert_eq!(bits.get(22..23), Some("1"), "isNotCompressed"); // Samples follow MSB-first, left then right. - assert_eq!(&bits[23..39], "0001001000110100", "left = 0x1234"); - assert_eq!(&bits[39..55], "0101011001111000", "right = 0x5678"); - assert_eq!(&bits[55..58], "111", "END element"); + assert_eq!(bits.get(23..39), Some("0001001000110100"), "left = 0x1234"); + assert_eq!(bits.get(39..55), Some("0101011001111000"), "right = 0x5678"); + assert_eq!(bits.get(55..58), Some("111"), "END element"); // A full packet: 23 + 352*32 + 3 bits, rounded up to whole bytes. let packet = vec![0u8; FRAMES_PER_PACKET * BYTES_PER_FRAME]; diff --git a/crates/vuio-core/src/database/playlist_formats.rs b/crates/vuio-core/src/database/playlist_formats.rs index 87a0d2e3..3ccea8c8 100644 --- a/crates/vuio-core/src/database/playlist_formats.rs +++ b/crates/vuio-core/src/database/playlist_formats.rs @@ -186,13 +186,10 @@ impl PlaylistFileManager { for line in content.lines() { let line = line.trim(); - if line.starts_with("File") { - if let Some(eq_pos) = line.find('=') { - let (key, value) = line.split_at(eq_pos); - let value = &value[1..]; // Skip the '=' - + if let Some(line) = line.strip_prefix("File") { + if let Some((key, value)) = line.split_once('=') { // Extract the number from "File1", "File2", etc. - if let Ok(track_num) = key[4..].parse::() { + if let Ok(track_num) = key.parse::() { tracks.push((track_num, resolve_playlist_entry(base_dir, value.trim()))); } } @@ -520,8 +517,8 @@ impl PlaylistFileManager { while i < lines.len() { let line = lines[i].trim(); if line.starts_with("#EXTINF") { - let name = if let Some(comma_pos) = line.find(',') { - line[comma_pos + 1..].trim().to_string() + let name = if let Some((_, name)) = line.split_once(',') { + name.trim().to_string() } else { "Unknown Radio".to_string() }; @@ -545,19 +542,19 @@ impl PlaylistFileManager { for line in file_content.lines() { let line = line.trim(); - if line.starts_with("File") { - if let Some(eq_pos) = line.find('=') { - if let Ok(num) = line[4..eq_pos].parse::() { - let val = line[eq_pos + 1..].trim().to_string(); + if let Some(line) = line.strip_prefix("File") { + if let Some((number, value)) = line.split_once('=') { + if let Ok(num) = number.parse::() { + let val = value.trim().to_string(); if is_http_stream(&val) { urls.insert(num, val); } } } - } else if line.starts_with("Title") { - if let Some(eq_pos) = line.find('=') { - if let Ok(num) = line[5..eq_pos].parse::() { - let val = line[eq_pos + 1..].trim().to_string(); + } else if let Some(line) = line.strip_prefix("Title") { + if let Some((number, value)) = line.split_once('=') { + if let Ok(num) = number.parse::() { + let val = value.trim().to_string(); titles.insert(num, val); } } diff --git a/crates/vuio-core/src/database/sqlite/media_repo/bulk.rs b/crates/vuio-core/src/database/sqlite/media_repo/bulk.rs index 6a48b3cf..472a4939 100644 --- a/crates/vuio-core/src/database/sqlite/media_repo/bulk.rs +++ b/crates/vuio-core/src/database/sqlite/media_repo/bulk.rs @@ -218,16 +218,34 @@ pub(in crate::database::sqlite) fn upsert_media_file( } } +/// Fold the comparable and displayable text of a record to NFC. +/// +/// Every write goes through here, whatever produced the record — an embedded +/// tag, a filename, a scraper, a playlist — so one spelling reaches the columns +/// that `LIKE` and FTS read, and the same spelling reaches a renderer. `path` is +/// pointedly absent: it is the key the filesystem is opened with and keeps the +/// bytes the filesystem reported. See `crate::text`. +fn normalize_text_fields(file: &mut MediaFile) { + file.filename = crate::text::into_nfc(std::mem::take(&mut file.filename)); + crate::text::normalize_field(&mut file.title); + crate::text::normalize_field(&mut file.artist); + crate::text::normalize_field(&mut file.album); + crate::text::normalize_field(&mut file.genre); + crate::text::normalize_field(&mut file.album_artist); +} + impl SqliteDatabase { fn prepared_records(files: &[MediaFile], already_canonical: bool) -> Result> { files .iter() .map(|file| { - if already_canonical { - Ok(file.clone()) + let mut record = if already_canonical { + file.clone() } else { - Self::canonical_file(file) - } + Self::canonical_file(file)? + }; + normalize_text_fields(&mut record); + Ok(record) }) .collect() } diff --git a/crates/vuio-core/src/database/sqlite/query.rs b/crates/vuio-core/src/database/sqlite/query.rs index 6825d4d7..38a50cb1 100644 --- a/crates/vuio-core/src/database/sqlite/query.rs +++ b/crates/vuio-core/src/database/sqlite/query.rs @@ -147,6 +147,10 @@ const FTS_JOIN: &str = "media_files JOIN (\ /// Returns `None` when nothing searchable is left, which the caller turns into /// an empty result rather than an unfiltered one. pub(super) fn fts5_query_from_user_text(text: &str) -> Option { + // Folded first: a combining mark is `Mn`, not alphanumeric, so a decomposed + // `Fu\u{308}\u{df}en` would split here into `Fu` and `\u{df}en` and be searched as + // two tokens, while the composed spelling of the same word stays one. + let text = crate::text::to_nfc(text); let tokens: Vec = text .split(|character: char| !character.is_alphanumeric()) .filter(|token| !token.is_empty()) @@ -270,7 +274,9 @@ pub(super) fn plan(query: &MediaFileQuery) -> MediaQueryPlan { OR media_files.album LIKE ? ESCAPE '\')" .to_owned(), ); - let pattern = format!("%{}%", escape_like(text)); + // The stored columns are NFC (see `crate::text`), so the needle + // has to be, or a decomposed term matches nothing. + let pattern = format!("%{}%", escape_like(&crate::text::to_nfc(text))); for _ in 0..4 { params.push(Value::Text(pattern.clone())); } diff --git a/crates/vuio-core/src/database/sqlite/schema.rs b/crates/vuio-core/src/database/sqlite/schema.rs index 22465786..b4764edb 100644 --- a/crates/vuio-core/src/database/sqlite/schema.rs +++ b/crates/vuio-core/src/database/sqlite/schema.rs @@ -21,7 +21,7 @@ use crate::database::{ /// [`migrations`]; only a *newer* file — one written by a build that knows /// something this one does not — is refused, because there is no way to /// downgrade a schema without guessing at what to discard. -pub(super) const SCHEMA_VERSION: i64 = 7; +pub(super) const SCHEMA_VERSION: i64 = 8; /// Name of the collation that carries the application's natural ordering into /// SQL. Registered on every connection; see [`register_collations`]. @@ -350,6 +350,7 @@ fn migrations() -> Vec<(i64, String)> { (5, MIGRATION_V5.to_owned()), (6, MIGRATION_V6.to_owned()), (7, MIGRATION_V7.to_owned()), + (8, MIGRATION_V8.to_owned()), ] } @@ -495,6 +496,44 @@ const MIGRATION_V7: &str = r#" ALTER TABLE media_files ADD COLUMN video_codec TEXT; "#; +/// v7 → v8: one normalization form for stored text. +/// +/// Rows scanned before this release hold whatever form the filesystem or tagger +/// used, so a library scanned on macOS is largely NFD while the same files +/// scanned on Windows are NFC. Searches are folded to NFC now, which would leave +/// those older rows unfindable until something happened to rewrite them, so they +/// are folded once here instead. +/// +/// The `WHERE` keeps the write to rows that actually change: an ASCII-only +/// library — most libraries — matches nothing and the migration is a scan. +/// Updating `media_files` fires the FTS triggers, so the index follows. +/// `path` is not touched; it is the key the file is opened with. See `crate::text`. +const MIGRATION_V8: &str = r#" +UPDATE media_files SET + filename = nfc(filename), + title = nfc(title), + artist = nfc(artist), + album = nfc(album), + genre = nfc(genre), + album_artist = nfc(album_artist) +WHERE filename IS NOT nfc(filename) + OR title IS NOT nfc(title) + OR artist IS NOT nfc(artist) + OR album IS NOT nfc(album) + OR genre IS NOT nfc(genre) + OR album_artist IS NOT nfc(album_artist); + +UPDATE mediainfo SET + title = nfc(title), + original_title = nfc(original_title), + overview = nfc(overview), + genres = nfc(genres) +WHERE title IS NOT nfc(title) + OR original_title IS NOT nfc(original_title) + OR overview IS NOT nfc(overview) + OR genres IS NOT nfc(genres); +"#; + /// Positions within [`MEDIA_COLUMNS`], shared by the owned decoder and the /// borrowed views so the two can never drift apart. pub(super) mod column { @@ -570,6 +609,31 @@ pub(super) fn register_collations(connection: &Connection) -> Result<()> { crate::natural_cmp(left, right) }) .context("Failed to register the natural collation")?; + register_nfc(connection)?; + Ok(()) +} + +/// Expose Unicode NFC folding to SQL, so a migration can normalize stored text +/// without pulling every row through Rust. Deterministic and null-preserving, +/// which is what lets `MIGRATION_V8` compare a column against its folded self. +fn register_nfc(connection: &Connection) -> Result<()> { + connection + .create_scalar_function( + "nfc", + 1, + rusqlite::functions::FunctionFlags::SQLITE_UTF8 + | rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC, + |context| { + let value = context.get_raw(0); + match value { + rusqlite::types::ValueRef::Text(_) => { + Ok(Some(crate::text::to_nfc(value.as_str()?).into_owned())) + } + _ => Ok(None), + } + }, + ) + .context("Failed to register the nfc function")?; Ok(()) } diff --git a/crates/vuio-core/src/database/sqlite/tests.rs b/crates/vuio-core/src/database/sqlite/tests.rs index c4d9e279..540ee0b8 100644 --- a/crates/vuio-core/src/database/sqlite/tests.rs +++ b/crates/vuio-core/src/database/sqlite/tests.rs @@ -446,3 +446,128 @@ async fn a_write_ahead_log_left_behind_does_not_resurrect_records() { .unwrap() .is_some()); } + +/// macOS hands back `Fu\u{308}\u{df}en` where Windows wrote `F\u{fc}\u{df}en`. The two look +/// identical, share no bytes, and used to be two different libraries as far as +/// search was concerned. Both spellings must reach the same row from either +/// spelling of the query, through the FTS index and the `LIKE` filter alike. +#[tokio::test] +async fn either_normalization_form_finds_the_other() { + use crate::database::{DatabaseReadSession, MediaFileView, MediaFileQuery}; + + const NFC: &str = "Füßen"; + const NFD: &str = "Fu\u{308}\u{df}en"; + assert_ne!(NFC, NFD, "the premise: the two spellings differ byte for byte"); + + let temp = tempdir().unwrap(); + let db = database(&temp, "normalization").await; + + // Stored decomposed, as a scan of an HFS+ volume would produce. + let mut decomposed = MediaFile::new( + PathBuf::from(format!("/media/{NFD}.flac")), + 1, + "audio/flac".to_owned(), + ); + decomposed.title = Some(NFD.to_owned()); + decomposed.artist = Some(NFD.to_owned()); + let id = db.store_media_file(&decomposed).await.unwrap(); + + // Found by the composed spelling, which is what a browser or a phone sends. + assert_eq!(search_ids(&db, NFC).await, vec![Some(id)], "FTS missed NFC"); + assert_eq!(search_ids(&db, NFD).await, vec![Some(id)], "FTS missed NFD"); + + let filtered = |text: &str| { + let db = db.clone(); + let text = text.to_owned(); + async move { + db.read(move |session| { + let mut ids = Vec::new(); + session.visit_files( + &MediaFileQuery::Filtered { + after_id: None, + mime_family: None, + text: Some(text), + }, + 0, + 10, + |file| { + ids.push(file.id()); + Ok(()) + }, + )?; + Ok(ids) + }) + .await + .unwrap() + } + }; + assert_eq!(filtered(NFC).await, vec![Some(id)], "LIKE missed NFC"); + assert_eq!(filtered(NFD).await, vec![Some(id)], "LIKE missed NFD"); + + // What we hand a renderer is the composed spelling, whatever was scanned: + // a TV that cannot place a combining mark shows "Fu" followed by a stray + // diaeresis otherwise. + let stored = db.get_file_by_path(&decomposed.path).await.unwrap().unwrap(); + assert_eq!(stored.title.as_deref(), Some(NFC)); + assert_eq!(stored.artist.as_deref(), Some(NFC)); + assert_eq!(stored.filename, format!("{NFC}.flac")); + + // The path is the exception: it is the key the file is opened with, so it + // keeps the bytes the filesystem reported. + assert_eq!(stored.path, decomposed.path); +} + +/// A library scanned before this release holds whatever form the filesystem gave +/// it. Those rows have to be folded by the migration, not left waiting for a +/// rescan — a user who upgrades and searches for their own music would otherwise +/// find nothing, which is exactly the state the fold is meant to end. +#[tokio::test] +async fn a_legacy_database_has_its_text_folded_by_the_migration() { + const NFC: &str = "Füßen"; + const NFD: &str = "Fu\u{308}\u{df}en"; + + let temp = tempdir().unwrap(); + let path = temp.path().join("legacy.db"); + + { + let connection = rusqlite::Connection::open(&path).unwrap(); + crate::database::sqlite::schema::register_collations(&connection).unwrap(); + connection.execute_batch(SCHEMA_V1).unwrap(); + connection + .execute( + "INSERT INTO media_files + (id, path, parent_path, filename, size, modified_secs, mime_type, + mime_family, title, artist, album, created_at_secs, updated_at_secs) + VALUES (11, ?1, '/media', ?2, 10, 100, 'audio/flac', 'audio', ?3, ?3, ?3, + 100, 100)", + rusqlite::params![ + format!("/media/{NFD}.flac"), + format!("{NFD}.flac"), + NFD, + ], + ) + .unwrap(); + } + + let db = SqliteDatabase::new(path.clone()).await.unwrap(); + db.initialize().await.unwrap(); + + // The path is untouched — it still has to open the file that is on disk. + let file = db + .get_file_by_path(std::path::Path::new(&format!("/media/{NFD}.flac"))) + .await + .unwrap() + .expect("the migrated record is still there"); + assert_eq!(file.id, Some(11), "the record kept its DIDL object id"); + + // Its text was folded in place. + assert_eq!(file.title.as_deref(), Some(NFC)); + assert_eq!(file.artist.as_deref(), Some(NFC)); + assert_eq!(file.album.as_deref(), Some(NFC)); + assert_eq!(file.filename, format!("{NFC}.flac")); + + // And the folded text reached the index the migration rebuilt. + let db = std::sync::Arc::new(db); + assert_eq!(search_ids(&db, NFC).await, vec![Some(11)]); + assert_eq!(search_ids(&db, NFD).await, vec![Some(11)]); +} diff --git a/crates/vuio-core/src/http_client.rs b/crates/vuio-core/src/http_client.rs index 3e0ca8e7..429a3d77 100644 --- a/crates/vuio-core/src/http_client.rs +++ b/crates/vuio-core/src/http_client.rs @@ -179,7 +179,7 @@ pub(crate) fn join_path(base: &Uri, reference: &str) -> Result { } else { let base_path = base.path(); let directory = match base_path.rfind('/') { - Some(index) => &base_path[..=index], + Some(index) => base_path.split_at(index + 1).0, None => "/", }; format!("{directory}{target_path}") diff --git a/crates/vuio-core/src/lib.rs b/crates/vuio-core/src/lib.rs index 4b908c72..df411e06 100644 --- a/crates/vuio-core/src/lib.rs +++ b/crates/vuio-core/src/lib.rs @@ -51,6 +51,7 @@ // every target platform rather than a macOS-only build. #![cfg_attr(not(feature = "unstable-internals"), allow(dead_code))] #![deny(clippy::undocumented_unsafe_blocks)] +#![deny(clippy::string_slice)] // Everything below is internal. `vuio-core` commits to the facade re-exported // after this block and nothing else: a surface small enough to keep stable for @@ -99,6 +100,11 @@ internal_modules!( web, ); +mod text; + +#[cfg(test)] +mod unicode_corpus; + // ── The stable public API ────────────────────────────────────────────────── pub use crate::error::{Error, ErrorKind, Result}; pub use crate::runtime::{Runtime, RuntimeHandle, RuntimeOptions, RuntimeStatus}; diff --git a/crates/vuio-core/src/mediainfo/artwork.rs b/crates/vuio-core/src/mediainfo/artwork.rs index 7c8aa2e0..14a23cca 100644 --- a/crates/vuio-core/src/mediainfo/artwork.rs +++ b/crates/vuio-core/src/mediainfo/artwork.rs @@ -48,7 +48,8 @@ impl ArtworkCache { /// Where a key's file lives, given the extension implied by its content type. fn path_for(&self, key: &str, extension: &str) -> PathBuf { - self.root.join(&key[..2]).join(format!("{key}.{extension}")) + let shard = key.get(..2).unwrap_or(key); + self.root.join(shard).join(format!("{key}.{extension}")) } /// Find a cached file for `key`, whatever image type it was stored as. @@ -141,7 +142,9 @@ mod tests { let path = cache.path_for(&key, "jpg"); assert_eq!( path, - Path::new("/tmp/artwork").join(&key[..2]).join(format!("{key}.jpg")) + Path::new("/tmp/artwork") + .join(key.get(..2).unwrap()) + .join(format!("{key}.jpg")) ); } diff --git a/crates/vuio-core/src/mediainfo/job.rs b/crates/vuio-core/src/mediainfo/job.rs index 6bd7b42d..191d8586 100644 --- a/crates/vuio-core/src/mediainfo/job.rs +++ b/crates/vuio-core/src/mediainfo/job.rs @@ -74,8 +74,7 @@ fn is_season_folder(folder_name: &str) -> bool { { return true; } - if trimmed.starts_with('s') && trimmed.len() <= 4 { - let rest = &trimmed[1..]; + if let Some(rest) = trimmed.strip_prefix('s').filter(|_| trimmed.len() <= 4) { if !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()) { return true; } diff --git a/crates/vuio-core/src/mediainfo/matching.rs b/crates/vuio-core/src/mediainfo/matching.rs index 3f5af935..d1ad2720 100644 --- a/crates/vuio-core/src/mediainfo/matching.rs +++ b/crates/vuio-core/src/mediainfo/matching.rs @@ -72,13 +72,8 @@ fn as_year(token: &str) -> Option { /// `S02E05`, `s2e5`, and the `2x05` form. fn as_season_episode(token: &str) -> Option<(u32, u32)> { let lowered = token.to_ascii_lowercase(); - let bytes = lowered.as_bytes(); - - if bytes.first() == Some(&b's') { - let rest = &lowered[1..]; - if let Some(split) = rest.find('e') { - let (season, episode) = rest.split_at(split); - let episode = &episode[1..]; + if let Some(rest) = lowered.strip_prefix('s') { + if let Some((season, episode)) = rest.split_once('e') { if !season.is_empty() && !episode.is_empty() && season.bytes().all(|b| b.is_ascii_digit()) @@ -90,9 +85,7 @@ fn as_season_episode(token: &str) -> Option<(u32, u32)> { return None; } - let split = lowered.find('x')?; - let (season, episode) = lowered.split_at(split); - let episode = &episode[1..]; + let (season, episode) = lowered.split_once('x')?; if season.is_empty() || episode.is_empty() || !season.bytes().all(|b| b.is_ascii_digit()) diff --git a/crates/vuio-core/src/platform/filesystem/metadata.rs b/crates/vuio-core/src/platform/filesystem/metadata.rs index f1f0fd95..390583f9 100644 --- a/crates/vuio-core/src/platform/filesystem/metadata.rs +++ b/crates/vuio-core/src/platform/filesystem/metadata.rs @@ -484,7 +484,7 @@ fn set_date(probed: &mut ProbedMetadata, value: &str, authoritative: bool) { fn standard_tag_name(tag: &StandardTag) -> String { let rendered = format!("{tag:?}"); match rendered.find('(') { - Some(index) => rendered[..index].to_owned(), + Some(index) => rendered.split_at(index).0.to_owned(), None => rendered, } } @@ -560,12 +560,13 @@ pub(crate) fn fallback_parse_filename(media_file: &mut MediaFile) { let mut artist_name = part0; let mut track_num = None; if let Some(first_space) = part0.find(' ') { - let maybe_num = &part0[..first_space].trim_end_matches('.'); + let (maybe_num, remainder) = part0.split_at(first_space); + let maybe_num = maybe_num.trim_end_matches('.'); let clean: String = maybe_num.chars().filter(|c| c.is_ascii_digit()).collect(); - if !clean.is_empty() && clean == *maybe_num { + if !clean.is_empty() && clean == maybe_num { if let Ok(num) = clean.parse::() { track_num = Some(num); - artist_name = &part0[first_space + 1..]; + artist_name = remainder.trim_start_matches(' '); } } } @@ -583,14 +584,15 @@ pub(crate) fn fallback_parse_filename(media_file: &mut MediaFile) { let mut title_part = filename_sans_ext.as_str(); if let Some(first_space) = filename_sans_ext.find(' ') { - let maybe_num = &filename_sans_ext[..first_space].trim_end_matches('.'); + let (maybe_num, remainder) = filename_sans_ext.split_at(first_space); + let maybe_num = maybe_num.trim_end_matches('.'); let clean: String = maybe_num.chars().filter(|c| c.is_ascii_digit()).collect(); - if !clean.is_empty() && clean == *maybe_num { + if !clean.is_empty() && clean == maybe_num { if let Ok(num) = clean.parse::() { if media_file.track_number.is_none() { media_file.track_number = Some(num); } - title_part = &filename_sans_ext[first_space + 1..]; + title_part = remainder.trim_start_matches(' '); } } } diff --git a/crates/vuio-core/src/platform/filesystem/normalization.rs b/crates/vuio-core/src/platform/filesystem/normalization.rs index 92d4f5c3..c25449fd 100644 --- a/crates/vuio-core/src/platform/filesystem/normalization.rs +++ b/crates/vuio-core/src/platform/filesystem/normalization.rs @@ -60,9 +60,9 @@ impl WindowsPathNormalizer { canonical = canonical.replace('\\', "/"); // Deduplicate slashes - if canonical.starts_with("//") { + if let Some(rest) = canonical.strip_prefix("//") { // UNC path: preserve leading double slash, clean the rest - let rest = canonical[2..].replace("//", "/"); + let rest = rest.replace("//", "/"); // Iterate until stable to handle multiple consecutive slashes let mut cleaned = rest; while cleaned.contains("//") { diff --git a/crates/vuio-core/src/platform/filesystem/tests.rs b/crates/vuio-core/src/platform/filesystem/tests.rs index d6821f80..666892c2 100644 --- a/crates/vuio-core/src/platform/filesystem/tests.rs +++ b/crates/vuio-core/src/platform/filesystem/tests.rs @@ -416,3 +416,51 @@ mod path_normalizer_tests { assert!(result.is_ok()); } } + +/// `fallback_parse_filename` splits on " - " and on the first space, then indexes +/// what it finds. Every sample is run as a bare stem, as an "artist - title" pair +/// and behind a track number, because each shape takes a different branch. +#[test] +fn filename_fallback_survives_every_script_and_alignment() { + use std::path::PathBuf; + use std::time::SystemTime; + + let blank = |path: PathBuf, filename: String| MediaFile { + id: None, + path, + filename, + size: 0, + modified: SystemTime::UNIX_EPOCH, + mime_type: "audio/mpeg".to_string(), + duration: None, + title: None, + artist: None, + album: None, + genre: None, + track_number: None, + year: None, + album_artist: None, + tags: Default::default(), + stream: Default::default(), + extra_tags: Vec::new(), + tags_version: 0, + subtitle_available: false, + created_at: SystemTime::UNIX_EPOCH, + updated_at: SystemTime::UNIX_EPOCH, + }; + + for sample in crate::unicode_corpus::alignment_sweep() { + for stem in [ + sample.clone(), + format!("{sample} - {sample}"), + format!("{sample} - {sample} - {sample}"), + format!("01 - {sample}"), + format!("{sample}.{sample}"), + ] { + let filename = format!("{stem}.mp3"); + let mut file = blank(PathBuf::from(format!("/music/{filename}")), filename); + fallback_parse_filename(&mut file); + assert!(file.title.is_some(), "no title parsed from {stem:?}"); + } + } +} diff --git a/crates/vuio-core/src/platform/filesystem/windows.rs b/crates/vuio-core/src/platform/filesystem/windows.rs index f797eb45..7569d686 100644 --- a/crates/vuio-core/src/platform/filesystem/windows.rs +++ b/crates/vuio-core/src/platform/filesystem/windows.rs @@ -32,9 +32,14 @@ impl WindowsFileSystemManager { } /// Check if a path contains a drive letter (C:\path) + /// + /// The first character must be ASCII: callers strip it with `path_str[1..]`, + /// which would split a multi-byte character mid-codepoint on a path like + /// `Ü:\Musik`. A drive letter is A-Z anyway. fn has_drive_letter(&self, path: &Path) -> bool { let path_str = path.to_string_lossy(); path_str.len() >= 3 + && path_str.starts_with(|c: char| c.is_ascii_alphabetic()) && path_str.chars().nth(1) == Some(':') && path_str.chars().nth(2) == Some('\\') } diff --git a/crates/vuio-core/src/platform/network/linux/discovery.rs b/crates/vuio-core/src/platform/network/linux/discovery.rs index c7794fa0..7dab5c1e 100644 --- a/crates/vuio-core/src/platform/network/linux/discovery.rs +++ b/crates/vuio-core/src/platform/network/linux/discovery.rs @@ -68,10 +68,8 @@ impl LinuxNetworkManager { let line = line.trim(); // Interface line: "2: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000" - if let Some(colon_pos) = line.find(':') { - if let Some(second_colon) = line[colon_pos + 1..].find(':') { - let second_colon_pos = colon_pos + 1 + second_colon; - + if let Some((_, interface)) = line.split_once(':') { + if let Some((interface_name, _)) = interface.split_once(':') { // Save previous interface with the best IP if let Some(name) = ¤t_interface { if !name.starts_with("lo") && !current_ips.is_empty() { @@ -96,15 +94,14 @@ impl LinuxNetworkManager { } // Parse new interface - let interface_name = line[colon_pos + 1..second_colon_pos].trim().to_string(); + let interface_name = interface_name.trim().to_string(); current_interface = Some(interface_name.clone()); current_ips.clear(); is_loopback = interface_name.starts_with("lo"); // Parse flags - if let Some(flags_start) = line.find('<') { - if let Some(flags_end) = line.find('>') { - let flags = &line[flags_start + 1..flags_end]; + if let Some((_, flags)) = line.split_once('<') { + if let Some((flags, _)) = flags.split_once('>') { is_up = flags.contains("UP"); supports_multicast = flags.contains("MULTICAST"); } @@ -114,8 +111,7 @@ impl LinuxNetworkManager { // IP address line: " inet 192.168.1.100/24 brd 192.168.1.255 scope global dynamic eth0" if line.contains("inet ") && !line.contains("inet6") { - if let Some(inet_pos) = line.find("inet ") { - let after_inet = &line[inet_pos + 5..]; + if let Some((_, after_inet)) = line.split_once("inet ") { if let Some(ip_part) = after_inet.split_whitespace().next() { // Remove CIDR notation if present let ip_str = ip_part.split('/').next().unwrap_or(ip_part); @@ -446,8 +442,7 @@ impl LinuxNetworkManager { let output_str = String::from_utf8_lossy(&output.stdout); for line in output_str.lines() { if line.contains("inet ") && !line.contains("inet6") { - if let Some(inet_pos) = line.find("inet ") { - let after_inet = &line[inet_pos + 5..]; + if let Some((_, after_inet)) = line.split_once("inet ") { if let Some(ip_part) = after_inet.split_whitespace().next() { let ip_str = ip_part.split('/').next().unwrap_or(ip_part); if let Ok(ip) = ip_str.parse::() { diff --git a/crates/vuio-core/src/text.rs b/crates/vuio-core/src/text.rs new file mode 100644 index 00000000..add9995d --- /dev/null +++ b/crates/vuio-core/src/text.rs @@ -0,0 +1,94 @@ +//! Normalization for the text VuIO compares, indexes and displays. +//! +//! The same name can be spelled more than one way in Unicode. `Füßen` is either +//! `F U+00FC ß e n` (NFC, what Windows and most taggers write) or +//! `F u U+0308 ß e n` (NFD, what macOS wrote for years and what HFS+ still hands +//! back). The two are canonically equivalent and look identical, but they share +//! no bytes, so every byte-oriented comparison we make — a `LIKE` clause, an FTS +//! token, a `==` between a scraped title and a parsed one — silently says "no". +//! +//! Everything we store for comparison or display is therefore folded to NFC on +//! the way into the database, and every search term is folded on the way in, so +//! both sides of a comparison are spelled the same way whatever the source used. +//! +//! # Paths are deliberately excluded +//! +//! A path is not text, it is a filesystem key. Linux and Windows compare the +//! bytes exactly: on ext4 the NFC and NFD spellings of `Füßen.flac` are two +//! different files, and only one of them exists. `canonical_to_platform` hands +//! the stored path straight to `PathBuf`, so normalizing it would produce a name +//! the kernel cannot open. Paths keep whatever the filesystem reported, byte for +//! byte; only `filename`, the display copy, is folded. + +use std::borrow::Cow; +use unicode_normalization::{is_nfc_quick, IsNormalized, UnicodeNormalization}; + +/// Fold `value` to NFC, borrowing when it is already in that form. +/// +/// `is_nfc_quick` answers `Yes` or `No` from character properties alone for the +/// overwhelming majority of input — ASCII included — so the common path costs a +/// scan and no allocation. Only a `Maybe` pays for the full composition. +pub(crate) fn to_nfc(value: &str) -> Cow<'_, str> { + match is_nfc_quick(value.chars()) { + IsNormalized::Yes => Cow::Borrowed(value), + _ => Cow::Owned(value.nfc().collect()), + } +} + +/// Fold an owned string in place, keeping the allocation when it is already NFC. +pub(crate) fn into_nfc(value: String) -> String { + match to_nfc(&value) { + Cow::Borrowed(_) => value, + Cow::Owned(normalized) => normalized, + } +} + +/// Fold an optional field, as media metadata columns are. +pub(crate) fn normalize_field(field: &mut Option) { + if let Some(value) = field.take() { + *field = Some(into_nfc(value)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const NFC: &str = "Füßen"; + const NFD: &str = "Fu\u{308}\u{df}en"; + + #[test] + fn the_two_spellings_differ_until_they_are_folded() { + // The premise of this module: equal to a reader, unequal to a comparison. + assert_ne!(NFC, NFD); + assert_eq!(to_nfc(NFC), to_nfc(NFD)); + assert_eq!(to_nfc(NFD), NFC); + } + + #[test] + fn text_already_in_nfc_is_not_copied() { + assert!(matches!(to_nfc("plain ascii"), Cow::Borrowed(_))); + assert!(matches!(to_nfc(NFC), Cow::Borrowed(_))); + assert!(matches!(to_nfc(NFD), Cow::Owned(_))); + } + + #[test] + fn folding_is_idempotent_across_the_corpus() { + for sample in crate::unicode_corpus::alignment_sweep() { + let once = to_nfc(&sample).into_owned(); + let twice = to_nfc(&once).into_owned(); + assert_eq!(once, twice, "folding {sample:?} was not idempotent"); + } + } + + #[test] + fn an_empty_or_absent_field_stays_that_way() { + let mut absent = None; + normalize_field(&mut absent); + assert_eq!(absent, None); + + let mut empty = Some(String::new()); + normalize_field(&mut empty); + assert_eq!(empty.as_deref(), Some("")); + } +} diff --git a/crates/vuio-core/src/tv_control.rs b/crates/vuio-core/src/tv_control.rs index 0d24804b..31c2f0b3 100644 --- a/crates/vuio-core/src/tv_control.rs +++ b/crates/vuio-core/src/tv_control.rs @@ -193,11 +193,10 @@ async fn fetch_tv_info( loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(e)) => { - current_element = String::from_utf8_lossy(e.name().as_ref()).to_string(); + current_element = e.name().as_ref().to_owned(); } Ok(Event::Text(e)) => { - let decoded = reader.decoder().decode(e.as_ref())?; - let text = quick_xml::escape::unescape(&decoded)?.into_owned(); + let text = quick_xml::escape::unescape(e.as_ref())?.into_owned(); match current_element.as_str() { "friendlyName" if friendly_name.is_empty() => { friendly_name = text; @@ -225,7 +224,7 @@ async fn fetch_tv_info( } } Ok(Event::End(e)) => { - let name = String::from_utf8_lossy(e.name().as_ref()).to_string(); + let name = e.name().as_ref().to_owned(); if name == "service" { if !current_service_type.contains("AVTransport") { in_av_transport_service = false; diff --git a/crates/vuio-core/src/unicode_corpus.rs b/crates/vuio-core/src/unicode_corpus.rs new file mode 100644 index 00000000..190bb119 --- /dev/null +++ b/crates/vuio-core/src/unicode_corpus.rs @@ -0,0 +1,67 @@ +//! Text that byte-oriented string handling has historically broken on, plus the +//! alignment sweep that turns offset bugs into deterministic test failures. +//! +//! Issue #51 was `&title[title.len() - 5..]` splitting a `ü` in half. A flat list +//! of awkward strings would not have caught it: the panic needs the slice point to +//! land *inside* a multi-byte character, which depends on the string's length +//! relative to that character's width. [`alignment_sweep`] therefore repeats every +//! sample at eight different paddings, so whatever index the code under test +//! computes eventually lands on every byte position of every sample — including +//! the continuation bytes that are never valid boundaries. + +/// Strings that stress encoding width, combining marks, direction and escaping. +pub(crate) const SAMPLES: &[&str] = &[ + "", + "plain ascii title", + // Issue #51, and the umlauts that reported it. + "Wie Felsenabgrund mir zu Füßen", + "Grüße aus München", + "Ärger mit Öl", + // The same word pre-composed (NFC) and decomposed (NFD). macOS hands back + // whichever form was written, so both reach us from the same directory. + "Füßen", + "Fu\u{308}\u{df}en", + // Scripts with no ASCII at all, including right-to-left and Indic clusters. + "Лунная соната", + "Ἀχιλλεύς", + "交響曲第八番 変ホ長調", + "안녕하세요", + "שלום עולם", + "مرحبا بالعالم", + "नमस्ते", + "สวัสดีชาวโลก", + // Four-byte scalars, and graphemes built from several of them. + "🎵🎶", + "👩‍👩‍👧‍👦", + "👋🏽", + "🇩🇪", + "𝄞 Clair de Lune", + "\u{10ffff}", + // Marks and invisibles that make character count disagree with byte count. + "e\u{301}\u{302}\u{303}", + "a\u{200b}b\u{200d}c", + "a\u{202e}reversed", + "\u{feff}leading bom", + // Case mapping that changes length, and the dotted/dotless Turkish pair. + "ß vs SS", + "Džunglica", + "filigree", + "İstanbul", + "ırmak", + // Characters an XML writer has to replace rather than pass through. + "A&B \"quoted\" 'single'", + "bell\u{7}vertical\u{b}form\u{c}feed", + "\u{fffe} noncharacter", + "…ellipsis…", +]; + +/// Every sample at eight paddings on each side, so a computed byte offset lands +/// on every position within every sample across the whole sweep. +pub(crate) fn alignment_sweep() -> impl Iterator { + SAMPLES.iter().flat_map(|sample| { + (0..8).flat_map(move |pad| { + let filler = "x".repeat(pad); + [format!("{filler}{sample}"), format!("{sample}{filler}")] + }) + }) +} diff --git a/crates/vuio-core/src/web/radio.rs b/crates/vuio-core/src/web/radio.rs index 743e55ab..89a7f073 100644 --- a/crates/vuio-core/src/web/radio.rs +++ b/crates/vuio-core/src/web/radio.rs @@ -614,10 +614,9 @@ mod tests { let payload = &out[17..17 + units * 16]; let text = String::from_utf8_lossy(payload); assert!(text.starts_with("StreamTitle='Artist - Song';"), "{text}"); + let (_, padding) = text.split_once(';').expect("metadata terminator"); assert!( - text[text.find(';').unwrap() + 1..] - .bytes() - .all(|byte| byte == 0), + padding.bytes().all(|byte| byte == 0), "the block must be padded with zeroes" ); diff --git a/crates/vuio-core/src/web/soap/common.rs b/crates/vuio-core/src/web/soap/common.rs index 353f4508..393ad8b0 100644 --- a/crates/vuio-core/src/web/soap/common.rs +++ b/crates/vuio-core/src/web/soap/common.rs @@ -14,11 +14,10 @@ pub(super) fn parse_dir_index_prefix(path_prefix_str: &str) -> (Option, & if !num_str.is_empty() { if let Ok(idx) = num_str.parse::() { let prefix_len = 1 + num_str.len(); - let rem = if path_prefix_str.len() > prefix_len { - path_prefix_str[prefix_len..].trim_start_matches('/') - } else { - "" - }; + let rem = path_prefix_str + .get(prefix_len..) + .unwrap_or_default() + .trim_start_matches('/'); (Some(idx), rem) } else { (None, path_prefix_str) diff --git a/crates/vuio-core/src/web/soap/parser.rs b/crates/vuio-core/src/web/soap/parser.rs index 766c6860..8c940a99 100644 --- a/crates/vuio-core/src/web/soap/parser.rs +++ b/crates/vuio-core/src/web/soap/parser.rs @@ -114,11 +114,7 @@ pub(super) fn xml_element_text(body: &str, expected_name: &str) -> Option { - return reader - .decoder() - .decode(text.as_ref()) - .ok() - .map(|value| value.into_owned()); + return Some(text.as_ref().to_owned()); } Event::End(_) => capture = false, Event::Eof => return None, @@ -128,13 +124,10 @@ pub(super) fn xml_element_text(body: &str, expected_name: &str) -> Option &str { - let local = name - .iter() - .rposition(|byte| *byte == b':') - .map(|position| &name[position + 1..]) - .unwrap_or(name); - std::str::from_utf8(local).unwrap_or_default() +fn local_xml_name(name: &str) -> &str { + name.rsplit_once(':') + .map(|(_, local)| local) + .unwrap_or(name) } fn invalid_soap_request(message: &'static str) -> Response { @@ -167,7 +160,7 @@ pub(super) fn parse_browse_params(body: &str) -> BrowseParams { current_element = local_xml_name(element.name().as_ref()).to_string(); } Ok(Event::Text(ref text)) => { - let text = reader.decoder().decode(text.as_ref()).unwrap_or_default(); + let text = text.as_ref(); match current_element.as_str() { "ObjectID" => { object_id = text.trim().to_string(); diff --git a/crates/vuio-core/src/web/subtitles.rs b/crates/vuio-core/src/web/subtitles.rs index 562d95a2..63a81b7c 100644 --- a/crates/vuio-core/src/web/subtitles.rs +++ b/crates/vuio-core/src/web/subtitles.rs @@ -94,7 +94,7 @@ fn normalize_timestamp(stamp: &str) -> String { parts[0], parts[1], parts[2], - &millis[..millis.len().min(3)] + millis.get(..millis.len().min(3)).unwrap_or(millis) ) } @@ -106,6 +106,18 @@ fn digits_only(value: &str) -> bool { mod tests { use super::*; + /// Cue text is author-supplied and arrives in every script there is; timing + /// lines are indexed by byte offset, so malformed ones are fed in too. + #[test] + fn srt_conversion_survives_every_script_and_alignment() { + for sample in crate::unicode_corpus::alignment_sweep() { + let srt = format!("1\n00:00:01,000 --> 00:00:04,500\n{sample}\n"); + assert!(srt_to_vtt(&srt).starts_with("WEBVTT")); + let malformed = format!("1\n{sample} --> {sample}\n{sample}\n"); + assert!(srt_to_vtt(&malformed).starts_with("WEBVTT")); + } + } + #[test] fn converts_a_basic_cue() { let vtt = srt_to_vtt("1\n00:00:01,000 --> 00:00:04,500\nHello\n"); diff --git a/crates/vuio-core/src/web/ui.rs b/crates/vuio-core/src/web/ui.rs index 6165c181..cdbc058e 100644 --- a/crates/vuio-core/src/web/ui.rs +++ b/crates/vuio-core/src/web/ui.rs @@ -722,7 +722,12 @@ mod tests { let sources = dashboard_sources(); let mut checked = 0; for (_, tail) in sources.match_indices("/assets/").map(|(index, matched)| { - (index, &sources[index + matched.len()..]) + ( + index, + sources + .get(index + matched.len()..) + .expect("match ends on a character boundary"), + ) }) { let name: String = tail .chars() diff --git a/crates/vuio-core/src/web/xml/rendering.rs b/crates/vuio-core/src/web/xml/rendering.rs index 4ed67776..e900c4bc 100644 --- a/crates/vuio-core/src/web/xml/rendering.rs +++ b/crates/vuio-core/src/web/xml/rendering.rs @@ -1,3 +1,5 @@ +#![deny(clippy::string_slice)] + use super::*; use std::fmt::Write as _; @@ -17,12 +19,20 @@ pub(super) fn write_xml_escaped( _ => None, }; if let Some(replacement) = replacement { - target.write_str(&value[unescaped_start..offset])?; + target.write_str( + value + .get(unescaped_start..offset) + .expect("char indices are UTF-8 boundaries"), + )?; target.write_str(replacement)?; unescaped_start = offset + character.len_utf8(); } } - target.write_str(&value[unescaped_start..]) + target.write_str( + value + .get(unescaped_start..) + .expect("char indices are UTF-8 boundaries"), + ) } pub(super) fn is_valid_xml_character(character: char) -> bool { @@ -110,11 +120,9 @@ pub(super) fn strip_matching_media_extension(base: &str, filename: &str) -> Stri else { return base.to_string(); }; - let suffix_len = ext.len() + 1; // '.' + ext - if base.len() > suffix_len { - let maybe = &base[base.len() - suffix_len..]; - if maybe.as_bytes()[0] == b'.' && maybe[1..].eq_ignore_ascii_case(ext) { - return base[..base.len() - suffix_len].to_string(); + if let Some((stem, base_ext)) = base.rsplit_once('.') { + if !stem.is_empty() && base_ext.eq_ignore_ascii_case(ext) { + return stem.to_string(); } } base.to_string() @@ -419,7 +427,10 @@ pub(super) fn directory_container_id(object_id: &str, path: &str, name: &str) -> || path == "radio" { path.to_owned() - } else if path.starts_with('d') && path[1..].chars().all(|c| c.is_ascii_digit()) { + } else if path + .strip_prefix('d') + .is_some_and(|suffix| suffix.chars().all(|c| c.is_ascii_digit())) + { format!("{}/{}", object_id.trim_end_matches('/'), path) } else { format!("{}/{}", object_id.trim_end_matches('/'), name) diff --git a/crates/vuio-core/src/web/xml/tests.rs b/crates/vuio-core/src/web/xml/tests.rs index 323347a0..d160ba34 100644 --- a/crates/vuio-core/src/web/xml/tests.rs +++ b/crates/vuio-core/src/web/xml/tests.rs @@ -12,6 +12,14 @@ fn xml_escape_handles_markup_unicode_and_invalid_controls() { ); } +#[test] +fn xml_escape_accepts_every_unicode_scalar_and_emits_valid_xml_characters() { + let all_scalars: String = (0..=char::MAX as u32).filter_map(char::from_u32).collect(); + let escaped = xml_escape(&all_scalars).to_string(); + + assert!(escaped.chars().all(is_valid_xml_character)); +} + #[test] fn soap_result_writer_applies_the_required_second_escape_layer() { let mut output = String::new(); @@ -43,6 +51,68 @@ fn samsung_keeps_titles_without_matching_extension() { ); } +#[test] +fn samsung_handles_unicode_near_the_extension_boundary() { + let title = "09 - Symphony No. 8 in E-Flat Major, Pt. 2 V. Wie Felsenabgrund mir zu Füßen"; + let filename = format!("{title}.flac"); + + assert_eq!( + didl_display_title(Some(title), &filename, DlnaClientProfile::SamsungTv), + title + ); + assert_eq!( + didl_display_title(None, &filename, DlnaClientProfile::SamsungTv), + title + ); +} + +#[test] +fn samsung_extension_handling_accepts_every_unicode_scalar() { + for code_point in 0..=char::MAX as u32 { + let Some(character) = char::from_u32(code_point) else { + continue; + }; + + // With a one-byte extension, the old byte-offset implementation tried to + // start its suffix one byte before `x`, bisecting every multibyte scalar. + let title = format!("title{character}x"); + assert_eq!( + didl_display_title(Some(&title), "file.z", DlnaClientProfile::SamsungTv), + title, + "metadata title containing U+{code_point:04X}" + ); + + // Exercise the filename-fallback and matching-extension path as well. + // Both separators are excluded so this assertion has the same meaning on + // Unix and Windows; they remain covered in the metadata-title assertion. + if !matches!(character, '/' | '\\') { + let stem = format!("title{character}"); + let filename = format!("{stem}.Z"); + assert_eq!( + didl_display_title(None, &filename, DlnaClientProfile::SamsungTvQ), + stem, + "filename containing U+{code_point:04X}" + ); + } + } +} + +#[test] +fn samsung_extension_handling_covers_suffix_edge_cases() { + for (base, filename, expected) in [ + ("", "file.flac", ""), + (".flac", "file.flac", ".flac"), + ("archive.part.FLAC", "file.flac", "archive.part"), + ("track.mp3", "file.flac", "track.mp3"), + ("track.flac", "file", "track.flac"), + ("track.flac", "file.", "track.flac"), + ("曲.音", "file.音", "曲"), + ("曲.音", "file.樂", "曲.音"), + ] { + assert_eq!(strip_matching_media_extension(base, filename), expected); + } +} + #[test] fn non_samsung_keeps_filename_when_title_missing() { assert_eq!( diff --git a/crates/vuio-core/tests/music_browse_integration_tests.rs b/crates/vuio-core/tests/music_browse_integration_tests.rs index 9e19be1b..bdd466f3 100644 --- a/crates/vuio-core/tests/music_browse_integration_tests.rs +++ b/crates/vuio-core/tests/music_browse_integration_tests.rs @@ -567,7 +567,9 @@ async fn child_counts_match_the_children_actually_returned() { let start = response .find(&anchor) .unwrap_or_else(|| panic!("{container_id} not in response")); - response[start..] + response + .get(start..) + .expect("find returns a character boundary") .split("childCount="") .nth(1) .and_then(|rest| rest.split(""").next()) diff --git a/crates/vuio-core/tests/regression_tests.rs b/crates/vuio-core/tests/regression_tests.rs index 53479612..3eaff3ba 100644 --- a/crates/vuio-core/tests/regression_tests.rs +++ b/crates/vuio-core/tests/regression_tests.rs @@ -237,7 +237,7 @@ mod critical_path_scenarios { "Testing long path ({} chars): {}", long_path.len(), if long_path.len() > 100 { - &long_path[..100] + long_path.get(..100).expect("test path is ASCII") } else { &long_path } diff --git a/crates/vuio-core/tests/web_ui_integration_tests.rs b/crates/vuio-core/tests/web_ui_integration_tests.rs index ad1b4e49..2c0ebd59 100644 --- a/crates/vuio-core/tests/web_ui_integration_tests.rs +++ b/crates/vuio-core/tests/web_ui_integration_tests.rs @@ -354,7 +354,7 @@ async fn the_two_surfaces_differ_only_at_the_root() { assert!( shell.contains("/_app/immutable/"), "the web UI surface should serve the Svelte shell, got: {}", - &shell[..shell.len().min(200)] + shell.chars().take(200).collect::() ); let (status, body, _) = get(&state, Surface::Primary, "/").await; @@ -452,7 +452,9 @@ async fn the_apps_bundles_are_served_with_immutable_caching() { let start = shell .find("/_app/immutable/") .expect("the shell loads a bundle"); - let bundle: String = shell[start..] + let bundle: String = shell + .get(start..) + .expect("find returns a character boundary") .chars() .take_while(|character| !"\"'".contains(*character)) .collect(); diff --git a/crates/vuio-web/Cargo.toml b/crates/vuio-web/Cargo.toml index 83a15784..0ca17d42 100644 --- a/crates/vuio-web/Cargo.toml +++ b/crates/vuio-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vuio-web" -version = "0.0.49" +version = "0.0.50" edition = "2021" rust-version = "1.98" authors = ["vyrti"] diff --git a/docs/api.md b/docs/api.md index b9f13682..f789155f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -98,7 +98,7 @@ configuration file actually writes. Backs the dashboard's Admin tab. "read_only_reason": null, "auth_enabled": false, "is_docker": false, - "version": "0.0.49", + "version": "0.0.50", // Where the server is actually accepting, which is what every advertised URL uses. "bound_addr": "0.0.0.0:8080", "desired_addr": null, diff --git a/docs/install.md b/docs/install.md index 31a274c6..c70d0101 100644 --- a/docs/install.md +++ b/docs/install.md @@ -242,7 +242,7 @@ Deploy VuIO to a Kubernetes cluster using the official Helm chart from GHCR. For ```bash # Install directly from GitHub Container Registry -helm install vuio oci://ghcr.io/vuiodev/charts/vuio --version 0.0.49 +helm install vuio oci://ghcr.io/vuiodev/charts/vuio --version 0.0.50 ``` Or install from local source: diff --git a/docs/kubernetes.md b/docs/kubernetes.md index c061af22..3e0ff8c3 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -11,7 +11,7 @@ VuIO provides an official Helm 3 chart to deploy the media server directly onto Install the chart directly from GitHub Container Registry without cloning the repository: ```bash -helm install vuio oci://ghcr.io/vuiodev/charts/vuio --version 0.0.49 +helm install vuio oci://ghcr.io/vuiodev/charts/vuio --version 0.0.50 ``` ### Local Installation diff --git a/packaging/docker/builddocker.sh b/packaging/docker/builddocker.sh index 1e238a9b..d55feb99 100755 --- a/packaging/docker/builddocker.sh +++ b/packaging/docker/builddocker.sh @@ -1,6 +1,6 @@ export GITHUB_ORG="vuiodev" export IMAGE_NAME="vuio" -export VERSION_TAG="v0.0.49" +export VERSION_TAG="v0.0.50" docker login ghcr.io diff --git a/packaging/linux/generate-repo.sh b/packaging/linux/generate-repo.sh index 9ff2ace7..741cff95 100755 --- a/packaging/linux/generate-repo.sh +++ b/packaging/linux/generate-repo.sh @@ -250,7 +250,7 @@ for arch in x86_64 aarch64; do zstd -d "$pkg" -o pkgtemp/pkg.tar --quiet 2>/dev/null && \ tar -xf pkgtemp/pkg.tar -C pkgtemp .PKGINFO 2>/dev/null || true pname="vuio" - pver="0.0.49-1" + pver="0.0.50-1" pdesc="Cross-platform DLNA media server" purl="https://github.com/vuiodev/vuio" psize="15000000"