diff options
Diffstat (limited to '')
-rw-r--r-- | Cargo.lock | 2461 | ||||
-rw-r--r-- | Cargo.toml | 20 | ||||
-rw-r--r-- | NEWS.md | 79 | ||||
-rw-r--r-- | crates/fmt/Cargo.toml | 2 | ||||
-rw-r--r-- | crates/libmpv2/examples/opengl.rs | 17 | ||||
-rw-r--r-- | crates/libmpv2/libmpv2-sys/Cargo.toml | 2 | ||||
-rw-r--r-- | crates/libmpv2/src/lib.rs | 2 | ||||
-rw-r--r-- | crates/libmpv2/src/mpv/events.rs | 50 | ||||
-rw-r--r-- | crates/libmpv2/src/mpv/protocol.rs | 127 | ||||
-rw-r--r-- | crates/libmpv2/src/mpv/render.rs | 48 | ||||
-rw-r--r-- | crates/libmpv2/src/tests.rs | 24 | ||||
-rw-r--r-- | crates/yt/Cargo.toml (renamed from yt/Cargo.toml) | 18 | ||||
-rw-r--r-- | crates/yt/src/ansi_escape_codes.rs | 26 | ||||
-rw-r--r-- | crates/yt/src/app.rs (renamed from yt/src/app.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/cache/mod.rs (renamed from yt/src/cache/mod.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/cli.rs (renamed from yt/src/cli.rs) | 20 | ||||
-rw-r--r-- | crates/yt/src/comments/comment.rs | 152 | ||||
-rw-r--r-- | crates/yt/src/comments/description.rs (renamed from yt/src/comments/description.rs) | 8 | ||||
-rw-r--r-- | crates/yt/src/comments/display.rs (renamed from yt/src/comments/display.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/comments/mod.rs (renamed from yt/src/comments/mod.rs) | 26 | ||||
-rw-r--r-- | crates/yt/src/comments/output.rs (renamed from yt/src/comments/output.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/config/default.rs (renamed from yt/src/config/default.rs) | 6 | ||||
-rw-r--r-- | crates/yt/src/config/definitions.rs (renamed from yt/src/config/definitions.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/config/file_system.rs (renamed from yt/src/config/file_system.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/config/mod.rs (renamed from yt/src/config/mod.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/constants.rs (renamed from yt/src/constants.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/download/download_options.rs | 118 | ||||
-rw-r--r-- | crates/yt/src/download/mod.rs (renamed from yt/src/download/mod.rs) | 31 | ||||
-rw-r--r-- | crates/yt/src/download/progress_hook.rs | 188 | ||||
-rw-r--r-- | crates/yt/src/main.rs (renamed from yt/src/main.rs) | 33 | ||||
-rw-r--r-- | crates/yt/src/select/cmds/add.rs (renamed from yt/src/select/cmds/add.rs) | 111 | ||||
-rw-r--r-- | crates/yt/src/select/cmds/mod.rs (renamed from yt/src/select/cmds/mod.rs) | 13 | ||||
-rw-r--r-- | crates/yt/src/select/mod.rs (renamed from yt/src/select/mod.rs) | 15 | ||||
-rw-r--r-- | crates/yt/src/select/selection_file/duration.rs (renamed from yt/src/select/selection_file/duration.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/select/selection_file/help.str (renamed from yt/src/select/selection_file/help.str) | 0 | ||||
-rw-r--r-- | crates/yt/src/select/selection_file/help.str.license (renamed from yt/src/select/selection_file/help.str.license) | 0 | ||||
-rw-r--r-- | crates/yt/src/select/selection_file/mod.rs (renamed from yt/src/select/selection_file/mod.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/status/mod.rs (renamed from yt/src/status/mod.rs) | 11 | ||||
-rw-r--r-- | crates/yt/src/storage/migrate/mod.rs (renamed from yt/src/storage/migrate/mod.rs) | 189 | ||||
-rw-r--r-- | crates/yt/src/storage/migrate/sql/0_Empty_to_Zero.sql (renamed from yt/src/storage/migrate/sql/00_empty_to_zero.sql) | 0 | ||||
-rw-r--r-- | crates/yt/src/storage/migrate/sql/1_Zero_to_One.sql (renamed from yt/src/storage/migrate/sql/01_zero_to_one.sql) | 0 | ||||
-rw-r--r-- | crates/yt/src/storage/migrate/sql/2_One_to_Two.sql (renamed from yt/src/storage/migrate/sql/02_one_to_two.sql) | 0 | ||||
-rw-r--r-- | crates/yt/src/storage/migrate/sql/3_Two_to_Three.sql | 85 | ||||
-rw-r--r-- | crates/yt/src/storage/mod.rs (renamed from yt/src/storage/mod.rs) | 2 | ||||
-rw-r--r-- | crates/yt/src/storage/subscriptions.rs (renamed from yt/src/storage/subscriptions.rs) | 25 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/downloader.rs (renamed from yt/src/storage/video_database/downloader.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/extractor_hash.rs (renamed from yt/src/storage/video_database/extractor_hash.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/get/mod.rs (renamed from yt/src/storage/video_database/get/mod.rs) | 8 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/get/playlist/iterator.rs (renamed from yt/src/storage/video_database/get/playlist/iterator.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/get/playlist/mod.rs (renamed from yt/src/storage/video_database/get/playlist/mod.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/mod.rs (renamed from yt/src/storage/video_database/mod.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/notify.rs (renamed from yt/src/storage/video_database/notify.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/set/mod.rs (renamed from yt/src/storage/video_database/set/mod.rs) | 60 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/set/playlist.rs (renamed from yt/src/storage/video_database/set/playlist.rs) | 36 | ||||
-rw-r--r-- | crates/yt/src/subscribe/mod.rs (renamed from yt/src/subscribe/mod.rs) | 31 | ||||
-rw-r--r-- | crates/yt/src/unreachable.rs (renamed from yt/src/unreachable.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/update/mod.rs (renamed from yt/src/update/mod.rs) | 69 | ||||
-rw-r--r-- | crates/yt/src/update/updater.rs | 167 | ||||
-rw-r--r-- | crates/yt/src/version/mod.rs (renamed from yt/src/version/mod.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/videos/display/format_video.rs (renamed from yt/src/videos/display/format_video.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/videos/display/mod.rs (renamed from yt/src/videos/display/mod.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/videos/mod.rs (renamed from yt/src/videos/mod.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/watch/mod.rs (renamed from yt/src/watch/mod.rs) | 18 | ||||
-rw-r--r-- | crates/yt/src/watch/playlist.rs (renamed from yt/src/watch/playlist.rs) | 12 | ||||
-rw-r--r-- | crates/yt/src/watch/playlist_handler/client_messages/mod.rs (renamed from yt/src/watch/playlist_handler/client_messages/mod.rs) | 0 | ||||
-rw-r--r-- | crates/yt/src/watch/playlist_handler/mod.rs (renamed from yt/src/watch/playlist_handler/mod.rs) | 29 | ||||
-rw-r--r-- | crates/yt_dlp/.cargo/config.toml | 12 | ||||
-rw-r--r-- | crates/yt_dlp/Cargo.toml | 13 | ||||
-rw-r--r-- | crates/yt_dlp/src/duration.rs | 78 | ||||
-rw-r--r-- | crates/yt_dlp/src/error.rs | 68 | ||||
-rw-r--r-- | crates/yt_dlp/src/lib.rs | 956 | ||||
-rw-r--r-- | crates/yt_dlp/src/logging.rs | 148 | ||||
-rw-r--r-- | crates/yt_dlp/src/progress_hook.rs | 41 | ||||
-rw-r--r-- | crates/yt_dlp/src/python_json_decode_failed.error_msg | 5 | ||||
-rw-r--r-- | crates/yt_dlp/src/python_json_decode_failed.error_msg.license | 9 | ||||
-rw-r--r-- | crates/yt_dlp/src/tests.rs | 89 | ||||
-rw-r--r-- | crates/yt_dlp/src/wrapper/info_json.rs | 824 | ||||
-rw-r--r-- | crates/yt_dlp/src/wrapper/mod.rs | 12 | ||||
-rw-r--r-- | crates/yt_dlp/src/wrapper/yt_dlp_options.rs | 62 | ||||
-rw-r--r-- | flake.lock | 48 | ||||
-rw-r--r-- | flake.nix | 31 | ||||
-rwxr-xr-x | scripts/mkdb.sh | 9 | ||||
-rw-r--r-- | yt/src/comments/comment.rs | 65 | ||||
-rw-r--r-- | yt/src/download/download_options.rs | 113 | ||||
-rw-r--r-- | yt/src/update/updater.rs | 171 |
85 files changed, 4058 insertions, 3035 deletions
diff --git a/Cargo.lock b/Cargo.lock index f12d722..4a17021 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -13,9 +13,28 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "adler32" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] [[package]] name = "aho-corasick" @@ -49,9 +68,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -64,44 +83,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.96" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arrayref" @@ -116,6 +135,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] name = "atoi" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -125,6 +150,15 @@ dependencies = [ ] [[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + +[[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -132,9 +166,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -153,20 +187,20 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -185,25 +219,33 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde", ] [[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] name = "blake3" -version = "1.6.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1230237285e3e10cde447185e8975408ae24deaa67205ce684805c25bc0c7937" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", - "constant_time_eq", - "memmap2", + "constant_time_eq 0.3.1", ] [[package]] @@ -216,10 +258,27 @@ dependencies = [ ] [[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" @@ -229,22 +288,60 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.1" +version = "1.5.0" dependencies = [ "serde", ] [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", + "libbz2-rs-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "caseless" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] [[package]] name = "cc" -version = "1.2.15" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "shlex", ] @@ -260,22 +357,28 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -300,9 +403,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.30" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -310,9 +413,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.30" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -322,9 +425,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", @@ -334,15 +437,38 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] [[package]] name = "concurrent-queue" @@ -366,6 +492,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -382,9 +524,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] @@ -396,6 +538,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] name = "crossbeam" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -410,9 +561,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -452,6 +603,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -462,10 +619,19 @@ dependencies = [ ] [[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + +[[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -485,6 +651,27 @@ dependencies = [ ] [[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -496,21 +683,74 @@ dependencies = [ ] [[package]] +name = "dns-lookup" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc" +dependencies = [ + "cfg-if", + "libc", + "socket2", + "windows-sys 0.48.0", +] + +[[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" dependencies = [ "serde", ] [[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -518,15 +758,21 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] name = "etcetera" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -549,12 +795,29 @@ dependencies = [ ] [[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" + +[[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] name = "filetime" version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -567,6 +830,17 @@ dependencies = [ ] [[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[package]] name = "flume" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -579,9 +853,24 @@ dependencies = [ [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" @@ -703,26 +992,45 @@ dependencies = [ ] [[package]] +name = "gethostname" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" +dependencies = [ + "rustix", + "windows-targets 0.52.6", +] + +[[package]] +name = "getopts" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +dependencies = [ + "unicode-width", +] + +[[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -738,10 +1046,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -765,9 +1083,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -776,6 +1094,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -804,16 +1128,17 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -827,21 +1152,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -851,30 +1177,10 @@ dependencies = [ ] [[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -882,68 +1188,55 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -956,9 +1249,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -966,27 +1259,21 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", ] [[package]] -name = "indoc" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" - -[[package]] name = "inotify" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "inotify-sys", "libc", ] @@ -1001,10 +1288,22 @@ dependencies = [ ] [[package]] +name = "is-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "is-terminal" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", @@ -1027,10 +1326,43 @@ dependencies = [ ] [[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "js-sys" @@ -1043,10 +1375,29 @@ dependencies = [ ] [[package]] +name = "junction" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72bbdfd737a243da3dfc1f99ee8d6e166480f17ab4ac84d7c34aacd73fc7bd16" +dependencies = [ + "scopeguard", + "windows-sys 0.52.0", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] name = "kqueue" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -1072,30 +1423,91 @@ dependencies = [ ] [[package]] +name = "lexical-parse-float" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de6f9cb01fb0b08060209a057c048fcbab8717b4c1ecd2eac66ebfe39a65b0f2" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72207aae22fc0a121ba7b6d479e42cbfea549af1479c3f3a4f12c70dd66df12e" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a82e24bf537fd24c177ffbbdc6ebcc8d54732c35b50a3f28cc3f4e4c949a0b3" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexopt" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa0e2a1fcbe2f6be6c42e342259976206b383122fc152e872795338b5a3f3a7" + +[[package]] +name = "libbz2-rs-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864a00c8d019e36216b69c2c4ce50b83b7bd966add3cf5ba554ec44f8bebcf5" + +[[package]] name = "libc" -version = "0.2.169" +version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" + +[[package]] +name = "libffi" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebfd30a67b482a08116e753d0656cb626548cf4242543e5cc005be7639d99838" +dependencies = [ + "libc", + "libffi-sys", +] + +[[package]] +name = "libffi-sys" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f003aa318c9f0ee69eb0ada7c78f5c9d2fedd2ceb274173b5c7ff475eee584a3" +dependencies = [ + "cc", +] [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.2", ] [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libmpv2" -version = "1.4.1" +version = "1.5.0" dependencies = [ "crossbeam", "libmpv2-sys", @@ -1105,7 +1517,7 @@ dependencies = [ [[package]] name = "libmpv2-sys" -version = "1.4.1" +version = "1.5.0" dependencies = [ "bindgen", ] @@ -1116,7 +1528,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "libc", "redox_syscall", ] @@ -1133,22 +1545,31 @@ dependencies = [ ] [[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + +[[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1156,9 +1577,99 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lz4_flex" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "malachite-base" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c738d3789301e957a8f7519318fcbb1b92bb95863b28f6938ae5a05be6259f34" +dependencies = [ + "hashbrown", + "itertools 0.14.0", + "libm", + "ryu", +] + +[[package]] +name = "malachite-bigint" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f46b904a4725706c5ad0133b662c20b388a3ffb04bda5154029dcb0cd28ae34" +dependencies = [ + "malachite-base", + "malachite-nz", + "num-integer", + "num-traits", + "paste", +] + +[[package]] +name = "malachite-nz" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1707c9a1fa36ce21749b35972bfad17bbf34cf5a7c96897c0491da321e387d3b" +dependencies = [ + "itertools 0.14.0", + "libm", + "malachite-base", + "wide", +] + +[[package]] +name = "malachite-q" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d764801aa4e96bbb69b389dcd03b50075345131cd63ca2e380bca71cc37a3675" +dependencies = [ + "itertools 0.14.0", + "malachite-base", + "malachite-nz", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "md-5" @@ -1172,15 +1683,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.5" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] @@ -1202,23 +1713,54 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "mt19937" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7151a832e54d2d6b2c827a20e5bcdd80359281cd2c354e725d4b82e7c471de" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", ] [[package]] @@ -1237,7 +1779,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "filetime", "inotify", "kqueue", @@ -1283,6 +1825,15 @@ dependencies = [ ] [[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1313,6 +1864,36 @@ dependencies = [ ] [[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1323,15 +1904,81 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "optional" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978aa494585d3ca4ad74929863093e87cac9790d81fe7aba2b3dc2890643a0fc" [[package]] name = "owo-colors" -version = "4.1.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" +checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "parking" @@ -1341,9 +1988,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1351,9 +1998,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -1363,6 +2010,12 @@ dependencies = [ ] [[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] name = "pem-rfc7468" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1379,20 +2032,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.12", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" dependencies = [ "pest", "pest_generator", @@ -1400,9 +2053,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" dependencies = [ "pest", "pest_meta", @@ -1413,9 +2066,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", @@ -1423,6 +2076,44 @@ dependencies = [ ] [[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1457,114 +2148,114 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pmutil" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "portable-atomic" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] -name = "ppv-lite86" -version = "0.2.20" +name = "portable-atomic-util" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ - "zerocopy", + "portable-atomic", ] [[package]] -name = "prettyplease" -version = "0.2.29" +name = "potential_utf" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ - "proc-macro2", - "syn", + "zerovec", ] [[package]] -name = "proc-macro2" -version = "1.0.93" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "unicode-ident", + "zerocopy", ] [[package]] -name = "pyo3" -version = "0.23.4" +name = "prettyplease" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", + "proc-macro2", + "syn", ] [[package]] -name = "pyo3-build-config" -version = "0.23.4" +name = "proc-macro2" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ - "once_cell", - "target-lexicon", + "unicode-ident", ] [[package]] -name = "pyo3-ffi" -version = "0.23.4" +name = "pymath" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d" +checksum = "5b66ab66a8610ce209d8b36cd0fecc3a15c494f715e0cb26f0586057f293abc9" dependencies = [ "libc", - "pyo3-build-config", ] [[package]] -name = "pyo3-macros" -version = "0.23.4" +name = "quote" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", ] [[package]] -name = "pyo3-macros-backend" -version = "0.23.4" +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "radium" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" +checksum = "db0b76288902db304c864a12046b73d2d895cc34a4bb8137baaeebe9978a072c" dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn", + "cfg-if", ] [[package]] -name = "quote" -version = "1.0.38" +name = "radix_trie" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" dependencies = [ - "proc-macro2", + "endian-type", + "nibble_vec", ] [[package]] @@ -1575,7 +2266,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1585,7 +2276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1594,16 +2285,36 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", ] [[package]] name = "redox_syscall" -version = "0.5.9" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "bitflags 2.8.0", + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", ] [[package]] @@ -1636,10 +2347,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] +name = "result-like" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf7172fef6a7d056b5c26bf6c826570267562d51697f4982ff3ba4aec68a9df" +dependencies = [ + "result-like-derive", +] + +[[package]] +name = "result-like-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d6574c02e894d66370cfc681e5d68fedbc9a548fb55b30a96b3f0ae22d0fe5" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "rsa" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest", @@ -1648,7 +2380,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1656,10 +2388,71 @@ dependencies = [ ] [[package]] +name = "ruff_python_ast" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.11.0#2cd25ef6410fb5fca96af1578728a3d828d2d53a" +dependencies = [ + "aho-corasick", + "bitflags 2.9.1", + "compact_str", + "is-macro", + "itertools 0.14.0", + "memchr", + "ruff_python_trivia", + "ruff_source_file", + "ruff_text_size", + "rustc-hash", +] + +[[package]] +name = "ruff_python_parser" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.11.0#2cd25ef6410fb5fca96af1578728a3d828d2d53a" +dependencies = [ + "bitflags 2.9.1", + "bstr", + "compact_str", + "memchr", + "ruff_python_ast", + "ruff_python_trivia", + "ruff_text_size", + "rustc-hash", + "static_assertions", + "unicode-ident", + "unicode-normalization", + "unicode_names2", +] + +[[package]] +name = "ruff_python_trivia" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.11.0#2cd25ef6410fb5fca96af1578728a3d828d2d53a" +dependencies = [ + "itertools 0.14.0", + "ruff_source_file", + "ruff_text_size", + "unicode-ident", +] + +[[package]] +name = "ruff_source_file" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.11.0#2cd25ef6410fb5fca96af1578728a3d828d2d53a" +dependencies = [ + "memchr", + "ruff_text_size", +] + +[[package]] +name = "ruff_text_size" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.11.0#2cd25ef6410fb5fca96af1578728a3d828d2d53a" + +[[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -1669,11 +2462,11 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", @@ -1681,16 +2474,386 @@ dependencies = [ ] [[package]] +name = "rustpython" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "cfg-if", + "dirs-next", + "env_logger", + "lexopt", + "libc", + "log", + "ruff_python_parser", + "rustpython-compiler", + "rustpython-pylib", + "rustpython-stdlib", + "rustpython-vm", + "rustyline", +] + +[[package]] +name = "rustpython-codegen" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "ahash", + "bitflags 2.9.1", + "indexmap", + "itertools 0.14.0", + "log", + "malachite-bigint", + "memchr", + "num-complex", + "num-traits", + "ruff_python_ast", + "ruff_source_file", + "ruff_text_size", + "rustpython-compiler-core", + "rustpython-compiler-source", + "rustpython-literal", + "rustpython-wtf8", + "thiserror 2.0.12", + "unicode_names2", +] + +[[package]] +name = "rustpython-common" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "ascii", + "bitflags 2.9.1", + "bstr", + "cfg-if", + "getrandom 0.3.3", + "itertools 0.14.0", + "libc", + "lock_api", + "malachite-base", + "malachite-bigint", + "malachite-q", + "memchr", + "num-traits", + "once_cell", + "parking_lot", + "radium", + "rustpython-literal", + "rustpython-wtf8", + "siphasher", + "unicode_names2", + "widestring", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustpython-compiler" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "ruff_python_ast", + "ruff_python_parser", + "ruff_source_file", + "ruff_text_size", + "rustpython-codegen", + "rustpython-compiler-core", + "rustpython-compiler-source", + "thiserror 2.0.12", +] + +[[package]] +name = "rustpython-compiler-core" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "bitflags 2.9.1", + "itertools 0.14.0", + "lz4_flex", + "malachite-bigint", + "num-complex", + "ruff_source_file", + "rustpython-wtf8", +] + +[[package]] +name = "rustpython-compiler-source" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "ruff_source_file", + "ruff_text_size", +] + +[[package]] +name = "rustpython-derive" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "proc-macro2", + "rustpython-compiler", + "rustpython-derive-impl", + "syn", +] + +[[package]] +name = "rustpython-derive-impl" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "itertools 0.14.0", + "maplit", + "proc-macro2", + "quote", + "rustpython-compiler-core", + "rustpython-doc", + "syn", + "syn-ext", + "textwrap", +] + +[[package]] +name = "rustpython-doc" +version = "0.3.0" +source = "git+https://github.com/RustPython/__doc__?tag=0.3.0#8b62ce5d796d68a091969c9fa5406276cb483f79" +dependencies = [ + "once_cell", +] + +[[package]] +name = "rustpython-literal" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "hexf-parse", + "is-macro", + "lexical-parse-float", + "num-traits", + "rustpython-wtf8", + "unic-ucd-category", +] + +[[package]] +name = "rustpython-pylib" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "glob", +] + +[[package]] +name = "rustpython-sre_engine" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "bitflags 2.9.1", + "num_enum", + "optional", + "rustpython-wtf8", +] + +[[package]] +name = "rustpython-stdlib" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "adler32", + "ahash", + "ascii", + "base64", + "blake2", + "bzip2", + "cfg-if", + "crc32fast", + "crossbeam-utils", + "csv-core", + "digest", + "dns-lookup", + "dyn-clone", + "flate2", + "foreign-types-shared", + "gethostname", + "hex", + "indexmap", + "itertools 0.14.0", + "junction", + "libc", + "libz-rs-sys", + "lzma-sys", + "mac_address", + "malachite-bigint", + "md-5", + "memchr", + "memmap2", + "mt19937", + "nix", + "num-complex", + "num-integer", + "num-traits", + "num_enum", + "openssl", + "openssl-probe", + "openssl-sys", + "page_size", + "parking_lot", + "paste", + "pymath", + "rand_core 0.9.3", + "rustix", + "rustpython-common", + "rustpython-derive", + "rustpython-vm", + "schannel", + "sha-1", + "sha2", + "sha3", + "socket2", + "system-configuration", + "termios", + "ucd", + "unic-char-property", + "unic-normal", + "unic-ucd-age", + "unic-ucd-bidi", + "unic-ucd-category", + "unic-ucd-ident", + "unicode-casing", + "unicode_names2", + "uuid", + "widestring", + "windows-sys 0.59.0", + "xml-rs", + "xz2", +] + +[[package]] +name = "rustpython-vm" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "ahash", + "ascii", + "bitflags 2.9.1", + "bstr", + "caseless", + "cfg-if", + "chrono", + "constant_time_eq 0.4.2", + "crossbeam-utils", + "errno", + "exitcode", + "getrandom 0.3.3", + "glob", + "half", + "hex", + "indexmap", + "is-macro", + "itertools 0.14.0", + "junction", + "libc", + "libffi", + "libloading", + "log", + "malachite-bigint", + "memchr", + "memoffset", + "nix", + "num-complex", + "num-integer", + "num-traits", + "num_cpus", + "num_enum", + "once_cell", + "optional", + "parking_lot", + "paste", + "result-like", + "ruff_python_ast", + "ruff_python_parser", + "ruff_source_file", + "ruff_text_size", + "rustix", + "rustpython-codegen", + "rustpython-common", + "rustpython-compiler", + "rustpython-compiler-core", + "rustpython-compiler-source", + "rustpython-derive", + "rustpython-literal", + "rustpython-sre_engine", + "rustyline", + "schannel", + "static_assertions", + "strum", + "strum_macros", + "thiserror 2.0.12", + "thread_local", + "timsort", + "uname", + "unic-ucd-bidi", + "unic-ucd-category", + "unic-ucd-ident", + "unicode-casing", + "unicode_names2", + "which", + "widestring", + "windows", + "windows-sys 0.59.0", + "winreg", +] + +[[package]] +name = "rustpython-wtf8" +version = "0.4.0" +source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +dependencies = [ + "ascii", + "bstr", + "itertools 0.14.0", + "memchr", +] + +[[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "rustyline" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.59.0", +] [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] [[package]] name = "same-file" @@ -1702,6 +2865,15 @@ dependencies = [ ] [[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1732,18 +2904,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -1752,9 +2924,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.139" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -1764,9 +2936,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -1784,6 +2956,17 @@ dependencies = [ ] [[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1796,9 +2979,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -1806,6 +2989,16 @@ dependencies = [ ] [[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1813,9 +3006,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -1827,10 +3020,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1841,18 +3040,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1879,9 +3078,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -1892,11 +3091,12 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "bytes 1.10.0", + "base64", + "bytes 1.10.1", "crc", "crossbeam-queue", "either", @@ -1916,7 +3116,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror", + "thiserror 2.0.12", "tokio", "tokio-stream", "tracing", @@ -1925,9 +3125,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", @@ -1938,9 +3138,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", @@ -1957,22 +3157,21 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn", - "tempfile", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", - "bitflags 2.8.0", + "bitflags 2.9.1", "byteorder", - "bytes 1.10.0", + "bytes 1.10.1", "crc", "digest", "dotenvy", @@ -1999,20 +3198,20 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.12", "tracing", "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags 2.8.0", + "bitflags 2.9.1", "byteorder", "crc", "dotenvy", @@ -2036,16 +3235,16 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.12", "tracing", "whoami", ] [[package]] name = "sqlx-sqlite" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "flume", @@ -2060,6 +3259,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", + "thiserror 2.0.12", "tracing", "url", ] @@ -2071,6 +3271,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] name = "stderrlog" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2101,6 +3307,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2108,9 +3333,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.98" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -2118,10 +3343,21 @@ dependencies = [ ] [[package]] +name = "syn-ext" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b126de4ef6c2a628a68609dd00733766c3b015894698a438ebdf374933fc31d1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -2129,20 +3365,34 @@ dependencies = [ ] [[package]] -name = "target-lexicon" -version = "0.12.16" +name = "system-configuration" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "tempfile" -version = "3.17.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2158,27 +3408,62 @@ dependencies = [ ] [[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] name = "termsize" -version = "1.4.1" +version = "1.5.0" dependencies = [ "libc", "winapi", ] [[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -2187,19 +3472,24 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] +name = "timsort" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "639ce8ef6d2ba56be0383a94dd13b92138d58de44c62618303bb798fa92bdc00" + +[[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -2207,9 +3497,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -2222,12 +3512,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", - "bytes 1.10.0", + "bytes 1.10.1", "libc", "mio", "pin-project-lite", @@ -2261,9 +3551,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -2273,27 +3563,34 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] [[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2307,9 +3604,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", @@ -2318,9 +3615,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -2336,28 +3633,164 @@ dependencies = [ ] [[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] +name = "ucd" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4fa6e588762366f1eb4991ce59ad1b93651d0b769dfb4e4d1c5c4b943d1159" + +[[package]] name = "ucd-trie" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-normal" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09d64d33589a94628bc2aeb037f35c2e25f3f049c7348b5aa5580b48e6bba62" +dependencies = [ + "unic-ucd-normal", +] + +[[package]] +name = "unic-ucd-age" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8cfdfe71af46b871dc6af2c24fcd360e2f3392ee4c5111877f2947f311671c" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d568b51222484e1f8209ce48caa6b430bf352962b877d592c29ab31fb53d8c" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-category" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0" +dependencies = [ + "matches", + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-hangul" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1dc690e19010e1523edb9713224cba5ef55b54894fe33424439ec9a40c0054" +dependencies = [ + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-normal" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86aed873b8202d22b13859dda5fe7c001d271412c31d411fd9b827e030569410" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-hangul", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] +name = "unicode-casing" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "623f59e6af2a98bdafeb93fa277ac8e1e40440973001ca15cf4ae1541cd16d56" + +[[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" @@ -2382,15 +3815,31 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] -name = "unindent" -version = "0.2.3" +name = "unicode_names2" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd" +dependencies = [ + "phf", + "unicode_names2_generator", +] + +[[package]] +name = "unicode_names2_generator" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e" +dependencies = [ + "getopts", + "log", + "phf_codegen", + "rand", +] [[package]] name = "url" @@ -2405,12 +3854,6 @@ dependencies = [ ] [[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2424,12 +3867,23 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uu_fmt" -version = "1.4.1" +version = "1.5.0" dependencies = [ "unicode-width", ] [[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "atomic", + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2459,15 +3913,15 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -2537,16 +3991,44 @@ dependencies = [ ] [[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix", + "winsafe", +] + +[[package]] name = "whoami" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ "redox_syscall", "wasite", ] [[package]] +name = "wide" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b5576b9a81633f3e8df296ce0063042a73507636cbe956c61133dd7034ab22" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2578,6 +4060,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2587,6 +4079,65 @@ dependencies = [ ] [[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2637,7 +4188,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", @@ -2645,6 +4196,22 @@ dependencies = [ ] [[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2657,6 +4224,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2669,6 +4242,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2681,12 +4260,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2699,6 +4290,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2711,6 +4308,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2723,6 +4326,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2735,46 +4344,77 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] name = "winnow" -version = "0.7.3" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "winreg" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" dependencies = [ - "bitflags 2.8.0", + "cfg-if", + "windows-sys 0.59.0", ] [[package]] -name = "write16" -version = "1.0.0" +name = "winsafe" +version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "xdg" -version = "2.5.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + +[[package]] +name = "xml-rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -2784,9 +4424,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -2796,11 +4436,11 @@ dependencies = [ [[package]] name = "yt" -version = "1.4.1" +version = "1.5.0" dependencies = [ "anyhow", "blake3", - "bytes 1.4.1", + "bytes 1.5.0", "chrono", "chrono-humanize", "clap", @@ -2828,32 +4468,30 @@ dependencies = [ [[package]] name = "yt_dlp" -version = "1.4.1" +version = "1.5.0" dependencies = [ - "bytes 1.4.1", + "indexmap", "log", - "pyo3", - "serde", + "rustpython", "serde_json", - "tokio", + "thiserror 2.0.12", "url", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", @@ -2862,18 +4500,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", @@ -2888,10 +4526,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -2900,11 +4549,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" diff --git a/Cargo.toml b/Cargo.toml index eb3f735..470eb58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,13 +16,13 @@ members = [ "crates/libmpv2", "crates/libmpv2/libmpv2-sys", "crates/termsize", - "yt", + "crates/yt", ] [workspace.package] -edition = "2021" -version = "1.4.1" -rust-version = "1.80.0" +edition = "2024" +version = "1.5.0" +rust-version = "1.85.0" authors = ["Benedikt Peetz <benedikt.peetz@b-peetz.de>"] repository = "https://git.vhack.eu/soispha/clients/yt" license = "GPL-3.0-or-later" @@ -37,11 +37,11 @@ termsize = { path = "./crates/termsize" } uu_fmt = { path = "./crates/fmt" } # Shared -log = "0.4.26" -serde = { version = "1.0.218", features = ["derive"] } -serde_json = "1.0.139" +log = "0.4.27" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" url = { version = "2.5.4", features = ["serde"] } -tokio = { version = "1.43.0", features = [ +tokio = { version = "1.45.1", features = [ "rt-multi-thread", "macros", "process", @@ -59,6 +59,10 @@ codegen-units = 1 panic = "abort" split-debuginfo = "off" +[profile.dev] +# Otherwise, yt_dlp is just too slow +opt-level = 2 + [workspace.lints.rust] # rustc lint groups https://doc.rust-lang.org/rustc/lints/groups.html warnings = "warn" diff --git a/NEWS.md b/NEWS.md index 95ce885..4e57d7b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,6 +14,85 @@ If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## [v1.5.0](https://git.vhack.eu/soispha/clients/yt/compare/2146109725115a9d01cc08ebbe3ef9c533ef1a89..v1.5.0) - 2025-02-22 +#### Bug Fixes +- **(crates/libmpv2)** Improve the error message for the `RawError` - ([0bd13d5](https://git.vhack.eu/soispha/clients/yt/commit/0bd13d5c26495649dabc23a4fb6b37fe682e3aec)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/libmpv2/Mpv::command)** Correctly escape arguments - ([66c7392](https://git.vhack.eu/soispha/clients/yt/commit/66c739237cc352fedf6276a3163097c1f1f32bd4)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/libmpv2/mpv)** Log the setting of properties - ([d2081fb](https://git.vhack.eu/soispha/clients/yt/commit/d2081fbfed6b2bde727aaf766bf0435ec05a3574)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/termsize)** Remove all of `clippy`'s warnings - ([4b63c7b](https://git.vhack.eu/soispha/clients/yt/commit/4b63c7be4207bf2ff7884189a388e83633b20b26)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp)** Actually return errors instead of panicing - ([4d1b813](https://git.vhack.eu/soispha/clients/yt/commit/4d1b8136bb23d009ee04d863780225ad9d9f9eed)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp)** Avoid printing the file extension in the progress display - ([325b230](https://git.vhack.eu/soispha/clients/yt/commit/325b23039850953705a57c0a331045b169548751)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp/error::PythonError)** Add the python type as `kind` - ([c83dfe5](https://git.vhack.eu/soispha/clients/yt/commit/c83dfe5268e2db39fe731b2d38387b76d9586057)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp/lib)** Actually resolve the `entries` generator object - ([c7601c2](https://git.vhack.eu/soispha/clients/yt/commit/c7601c2e6cc86a3123d8c9dc13afce9430520583)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp/lib)** Swallow all error logs from yt_dlp - ([d1022c5](https://git.vhack.eu/soispha/clients/yt/commit/d1022c5c5c82557d8c2c45fad88b67cc3e6582e3)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp/progress_hook)** Print the progress to stderr - ([1786ba0](https://git.vhack.eu/soispha/clients/yt/commit/1786ba0b87d9883e4c75126e5d72af02134cc8b8)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp/wrappers/info_json)** Serialize the `InfoType`s with their correct name - ([dc19623](https://git.vhack.eu/soispha/clients/yt/commit/dc19623a18ac47d9c660d98db768c64d99decff9)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp/wrappers/info_json)** Don't serialize `None` values - ([fc5771e](https://git.vhack.eu/soispha/clients/yt/commit/fc5771e35b459af6210cbd9a2e7c33b6c462d337)) - [@soispha](https://git.vhack.eu/soispha) +- **(package)** Update to account for modifications in `mkdb.sh` - ([a1dbd45](https://git.vhack.eu/soispha/clients/yt/commit/a1dbd45cbe2b21aa50eefcb6c9f016a7aaa4863c)) - [@soispha](https://git.vhack.eu/soispha) +- **(package/blake3)** Migrate to the new `fetchCargoVendor` fetcher - ([c8ce2b7](https://git.vhack.eu/soispha/clients/yt/commit/c8ce2b7862eb273ed81cd399490e0536b2965a20)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt)** Remove most of the references to the zero version `Video` struct - ([74346a5](https://git.vhack.eu/soispha/clients/yt/commit/74346a5e43235be37bffca6dd0cb0ead66a529b5)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/)** Box large futures - ([335bfe9](https://git.vhack.eu/soispha/clients/yt/commit/335bfe91d7efdfd5a89eaf7728511f96dab3fef3)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/cli)** Make most of the arguments to `yt select <cmd> <hash>` optional - ([ceb0ff2](https://git.vhack.eu/soispha/clients/yt/commit/ceb0ff290707905af56106401b3f2a326971c505)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/download/download_options)** Stop trying to write annotations - ([2bcd30e](https://git.vhack.eu/soispha/clients/yt/commit/2bcd30e3f44fb7dbf859d72caf2a33a96ee5618b)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/main)** Call `watch` with the required `Arc<App>` - ([761a780](https://git.vhack.eu/soispha/clients/yt/commit/761a780499eeb6afe93b55b06d9df5c75aa9d7cc)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/main)** Actually remove the `yt check output-info-json` - ([e07db3a](https://git.vhack.eu/soispha/clients/yt/commit/e07db3a810c2e0f43b20d73ea4258f3ea8f240d4)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/select/cmds/add)** Don't try to add a video that is already added - ([69c94a3](https://git.vhack.eu/soispha/clients/yt/commit/69c94a3068689a575a21a0db6170bde9acad768d)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/select/selection_file/help.str)** Disable vim line wrapping - ([5cf5936](https://git.vhack.eu/soispha/clients/yt/commit/5cf5936c3f2fa0750a2e67d8ff4b6624c3141402)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/status)** Don't show the database version in `yt status` - ([cf8662c](https://git.vhack.eu/soispha/clients/yt/commit/cf8662ce03e5677ddb7de880ceb59d5d84a63259)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/status)** Show the current database version - ([8d7df29](https://git.vhack.eu/soispha/clients/yt/commit/8d7df29bbce0ceb258fa6c591003d379fcdb704f)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/storage/migrate)** Improve error reporting - ([e21c289](https://git.vhack.eu/soispha/clients/yt/commit/e21c289f8d21b802d2dc609233bc28cde65da224)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/storage/migrate/sql/01_zero_to_one.sql)** Account for duration being NULL - ([62cdd76](https://git.vhack.eu/soispha/clients/yt/commit/62cdd76443bbecfbdb70a82a7936a2e602338692)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/storage/notify)** Switch from a polling based system to inotify - ([dc8539e](https://git.vhack.eu/soispha/clients/yt/commit/dc8539e3707c1a281b3aef9c7a6e8f929845d965)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/watch)** Always open a `mpv` window - ([c662429](https://git.vhack.eu/soispha/clients/yt/commit/c6624299c45225ec3971e8119979d7421c56f70d)) - [@soispha](https://git.vhack.eu/soispha) +#### Build system +- **(.envrc)** Align with current state of the repository - ([6e35bf4](https://git.vhack.eu/soispha/clients/yt/commit/6e35bf41687f92a109ee36fa3169b9fb3f72b7f2)) - [@soispha](https://git.vhack.eu/soispha) +- **(.envrc)** Always save the `output.info.json` if in devshell - ([2146109](https://git.vhack.eu/soispha/clients/yt/commit/2146109725115a9d01cc08ebbe3ef9c533ef1a89)) - [@soispha](https://git.vhack.eu/soispha) +- **(flake)** Add `ffmpeg` to the devshell - ([7cc3e3c](https://git.vhack.eu/soispha/clients/yt/commit/7cc3e3cca0de6638625e2997002f78cfd8e03294)) - [@soispha](https://git.vhack.eu/soispha) +- **(rustfmt.toml)** Add - ([ae13afa](https://git.vhack.eu/soispha/clients/yt/commit/ae13afa1aeb8aaed94b1d72aa6207bbfe373dd52)) - [@soispha](https://git.vhack.eu/soispha) +- **(scripts/cprh)** Remove - ([3c91e60](https://git.vhack.eu/soispha/clients/yt/commit/3c91e6046b791664a6e7a0fdc41d369df0ee204a)) - [@soispha](https://git.vhack.eu/soispha) +- **(treewide)** Update - ([7387b68](https://git.vhack.eu/soispha/clients/yt/commit/7387b6853893b3b6a04edb95a830b810e3311ba0)) - [@soispha](https://git.vhack.eu/soispha) +- **(treewide)** Update - ([938b4f1](https://git.vhack.eu/soispha/clients/yt/commit/938b4f1b11dce643942d5e6cd505b206319709a2)) - [@soispha](https://git.vhack.eu/soispha) +- **({.envrc,scripts/mkdb})** Mark the `sqlx` database - ([a1b3f95](https://git.vhack.eu/soispha/clients/yt/commit/a1b3f95bd3f7b447e918eec5bd67d7b5e8333eb0)) - [@soispha](https://git.vhack.eu/soispha) +#### Documentation +- **(yt/cli)** Remove last references to the external update and status_change bits - ([6e9a03f](https://git.vhack.eu/soispha/clients/yt/commit/6e9a03f4e2c6b57ed569bf12ca5e2954149006fb)) - [@soispha](https://git.vhack.eu/soispha) +#### Features +- **(crates/yt_dlp/lib)** Wrap `process_ie_result` function - ([8a53fbd](https://git.vhack.eu/soispha/clients/yt/commit/8a53fbd8af842e2ca1bca0b352113ff7e965f51f)) - [@soispha](https://git.vhack.eu/soispha) +- **(version)** Include `yt-dlp` and `python` version in `--version` - ([431538e](https://git.vhack.eu/soispha/clients/yt/commit/431538e9c90090c8802460f3a625a0cd38a5e72a)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt)** Make colorization of the output configurable - ([e30b69d](https://git.vhack.eu/soispha/clients/yt/commit/e30b69dd4c2ebfb4ae77b38037b66f3e6fcb17bc)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/)** Use concrete types in the `Video` structure - ([9e8657c](https://git.vhack.eu/soispha/clients/yt/commit/9e8657c9762dbb66f3322976606a1b4334d45a6b)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/cli)** Make running the migrations of the database optional - ([31f15dc](https://git.vhack.eu/soispha/clients/yt/commit/31f15dc02bdbb815ce2d53f10d710a65b0b7bf0b)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/select/cmds/add)** Support `start` `stop` args - ([b3785ad](https://git.vhack.eu/soispha/clients/yt/commit/b3785ad44cb48143ed44cee48190b8646d668946)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/select/selection_file/duration)** Support durations up to days - ([14db4ea](https://git.vhack.eu/soispha/clients/yt/commit/14db4eadd57d0a3837227d9d74b84a133bacd434)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/status)** Include the approximate total watch time - ([e902437](https://git.vhack.eu/soispha/clients/yt/commit/e9024377cf5b16f9a81b0f750891307cd6acebe4)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/storage/migrate)** Add version two - ([8dbcf33](https://git.vhack.eu/soispha/clients/yt/commit/8dbcf33c8c6114e5699472d47c39f114103dc02e)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/storage/migrate)** Add db version One - ([93b7432](https://git.vhack.eu/soispha/clients/yt/commit/93b74321bf30ef33e82b0e9337d8cc3b6ca6e663)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/storage/migrate)** Init database migration system - ([9d6721b](https://git.vhack.eu/soispha/clients/yt/commit/9d6721bce1ed93e66c589d34e20393c78c7a423b)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/update)** Port the Python updater to rust - ([1d7bc17](https://git.vhack.eu/soispha/clients/yt/commit/1d7bc17e62a64ec213e43530b050bc41f978c610)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/version)** Show _current_ database version - ([e6bdcb4](https://git.vhack.eu/soispha/clients/yt/commit/e6bdcb4816cd54b7477f50cdebf06b07e7b9c58e)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/watch/playlist)** Init - ([686ea29](https://git.vhack.eu/soispha/clients/yt/commit/686ea29b06162b1a70dc473cea70b00e379c4f29)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/watch/playlist_handler)** Rewrite to use new db layout - ([4a008ef](https://git.vhack.eu/soispha/clients/yt/commit/4a008ef549f595af18f7cf2d0e9940d2627ae8c4)) - [@soispha](https://git.vhack.eu/soispha) +#### Miscellaneous Chores +- **(crates/libmpv2)** Make `cargo clippy` happy - ([b1474f9](https://git.vhack.eu/soispha/clients/yt/commit/b1474f9dc8dc1ed22c2a78680e40bd315cb82b0f)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/termsize)** Vendor - ([9da970f](https://git.vhack.eu/soispha/clients/yt/commit/9da970f1f44f19432680e255f91f73fbb8fbe3c8)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp/wrappers/info_json)** Add further fields - ([832ad82](https://git.vhack.eu/soispha/clients/yt/commit/832ad8265015284f1d95c3426f074aaeacd05864)) - [@soispha](https://git.vhack.eu/soispha) +- **(crates/yt_dlp/wrappers/info_json)** Add further fields - ([674e499](https://git.vhack.eu/soispha/clients/yt/commit/674e4992d320ca0057121eb4474c370abccee8ab)) - [@soispha](https://git.vhack.eu/soispha) +- **(old)** Remove - ([e2b90b4](https://git.vhack.eu/soispha/clients/yt/commit/e2b90b40333e35214f0b1c1e1f575bb688a99e74)) - [@soispha](https://git.vhack.eu/soispha) +- **(treewide)** Add/Update the license headers - ([4cbc424](https://git.vhack.eu/soispha/clients/yt/commit/4cbc424e0d1b51c03dc7c3e49dae361cbf4c4b77)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt)** Change the type of `max_backlog` to `usize` - ([b5145c5](https://git.vhack.eu/soispha/clients/yt/commit/b5145c5d4ef674016f4e4217f67c2969a8dee962)) - [@soispha](https://git.vhack.eu/soispha) +#### Refactoring +- **(crates/fmt)** Init forked `uu_fmt` library - ([7cfa693](https://git.vhack.eu/soispha/clients/yt/commit/7cfa6939deb5496a07313a2a34632da1a3fb1b89)) - [@soispha](https://git.vhack.eu/soispha) +- **(treewide)** Remove all references of the now obsolete update_raw.py - ([6a137c6](https://git.vhack.eu/soispha/clients/yt/commit/6a137c6ca968654810ccfddd90908a227287387f)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/)** Use the new `termsize` and `uu_fmt` crates - ([6da5602](https://git.vhack.eu/soispha/clients/yt/commit/6da5602d083bf26312071e68bfb1eb130da98934)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/description)** Move to the `comments` subdirectory - ([f98665d](https://git.vhack.eu/soispha/clients/yt/commit/f98665d992e3af91e52318e0c6e9334c891343bd)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/storage/video_database)** Move `getters,setters` to `get,set` - ([9496583](https://git.vhack.eu/soispha/clients/yt/commit/9496583cc76fbd7347384716f2898f870743f16d)) - [@soispha](https://git.vhack.eu/soispha) +- **(yt/videos/display)** Streamline video formatting - ([27fae0b](https://git.vhack.eu/soispha/clients/yt/commit/27fae0bcd380fdf7396c33678f4aa3fa2df192cf)) - [@soispha](https://git.vhack.eu/soispha) +#### Style +- **(treewide)** Re-format - ([55a9411](https://git.vhack.eu/soispha/clients/yt/commit/55a94110287ad2b1a55953febac48422a9d3ba89)) - [@soispha](https://git.vhack.eu/soispha) +#### Tests +- **(crates/yt_dlp)** Ignore tests that hang forever - ([405858e](https://git.vhack.eu/soispha/clients/yt/commit/405858e3e7d2e5c06e49f1c195c46d64916afb65)) - [@soispha](https://git.vhack.eu/soispha) + +- - - + ## [v1.4.1](https://git.vhack.eu/soispha/clients/yt/compare/72434a90d6a3dbba48d40a23b840befe7649b558..v1.4.1) - 2024-12-14 #### Bug Fixes - **(yt_dlp/wrappers/info_json)** Add further fields to `RequestedDownloads` - ([72434a9](https://git.vhack.eu/soispha/clients/yt/commit/72434a90d6a3dbba48d40a23b840befe7649b558)) - [@soispha](https://git.vhack.eu/soispha) diff --git a/crates/fmt/Cargo.toml b/crates/fmt/Cargo.toml index 7f82a09..f3cf4ad 100644 --- a/crates/fmt/Cargo.toml +++ b/crates/fmt/Cargo.toml @@ -24,7 +24,7 @@ publish = false path = "src/fmt.rs" [dependencies] -unicode-width = "0.2.0" +unicode-width = "0.2.1" [lints] workspace = true diff --git a/crates/libmpv2/examples/opengl.rs b/crates/libmpv2/examples/opengl.rs index 8eb9647..9f595aa 100644 --- a/crates/libmpv2/examples/opengl.rs +++ b/crates/libmpv2/examples/opengl.rs @@ -38,13 +38,16 @@ fn main() { Ok(()) }) .unwrap(); - let mut render_context = RenderContext::new(unsafe { mpv.ctx.as_mut() }, vec![ - RenderParam::ApiType(RenderParamApiType::OpenGl), - RenderParam::InitParams(OpenGLInitParams { - get_proc_address, - ctx: video, - }), - ]) + let mut render_context = RenderContext::new( + unsafe { mpv.ctx.as_mut() }, + vec![ + RenderParam::ApiType(RenderParamApiType::OpenGl), + RenderParam::InitParams(OpenGLInitParams { + get_proc_address, + ctx: video, + }), + ], + ) .expect("Failed creating render context"); event_subsystem diff --git a/crates/libmpv2/libmpv2-sys/Cargo.toml b/crates/libmpv2/libmpv2-sys/Cargo.toml index b0514b8..96141d3 100644 --- a/crates/libmpv2/libmpv2-sys/Cargo.toml +++ b/crates/libmpv2/libmpv2-sys/Cargo.toml @@ -23,4 +23,4 @@ rust-version.workspace = true publish = false [build-dependencies] -bindgen = { version = "0.71.1" } +bindgen = { version = "0.72.0" } diff --git a/crates/libmpv2/src/lib.rs b/crates/libmpv2/src/lib.rs index d47e620..f6c2103 100644 --- a/crates/libmpv2/src/lib.rs +++ b/crates/libmpv2/src/lib.rs @@ -35,7 +35,7 @@ use std::os::raw as ctype; pub const MPV_CLIENT_API_MAJOR: ctype::c_ulong = 2; pub const MPV_CLIENT_API_MINOR: ctype::c_ulong = 2; pub const MPV_CLIENT_API_VERSION: ctype::c_ulong = - MPV_CLIENT_API_MAJOR << 16 | MPV_CLIENT_API_MINOR; + (MPV_CLIENT_API_MAJOR << 16) | MPV_CLIENT_API_MINOR; mod mpv; #[cfg(test)] diff --git a/crates/libmpv2/src/mpv/events.rs b/crates/libmpv2/src/mpv/events.rs index e27da2c..f10ff6e 100644 --- a/crates/libmpv2/src/mpv/events.rs +++ b/crates/libmpv2/src/mpv/events.rs @@ -70,26 +70,28 @@ impl<'a> PropertyData<'a> { // SAFETY: meant to extract the data from an event property. See `mpv_event_property` in // `client.h` unsafe fn from_raw(format: MpvFormat, ptr: *mut ctype::c_void) -> Result<PropertyData<'a>> { - assert!(!ptr.is_null()); - match format { - mpv_format::Flag => Ok(PropertyData::Flag(*(ptr as *mut bool))), - mpv_format::String => { - let char_ptr = *(ptr as *mut *mut ctype::c_char); - Ok(PropertyData::Str(mpv_cstr_to_str!(char_ptr)?)) - } - mpv_format::OsdString => { - let char_ptr = *(ptr as *mut *mut ctype::c_char); - Ok(PropertyData::OsdStr(mpv_cstr_to_str!(char_ptr)?)) - } - mpv_format::Double => Ok(PropertyData::Double(*(ptr as *mut f64))), - mpv_format::Int64 => Ok(PropertyData::Int64(*(ptr as *mut i64))), - mpv_format::Node => { - let sys_node = *(ptr as *mut libmpv2_sys::mpv_node); - let node = SysMpvNode::new(sys_node, false); - Ok(PropertyData::Node(node.value().unwrap())) + unsafe { + assert!(!ptr.is_null()); + match format { + mpv_format::Flag => Ok(PropertyData::Flag(*(ptr as *mut bool))), + mpv_format::String => { + let char_ptr = *(ptr as *mut *mut ctype::c_char); + Ok(PropertyData::Str(mpv_cstr_to_str!(char_ptr)?)) + } + mpv_format::OsdString => { + let char_ptr = *(ptr as *mut *mut ctype::c_char); + Ok(PropertyData::OsdStr(mpv_cstr_to_str!(char_ptr)?)) + } + mpv_format::Double => Ok(PropertyData::Double(*(ptr as *mut f64))), + mpv_format::Int64 => Ok(PropertyData::Int64(*(ptr as *mut i64))), + mpv_format::Node => { + let sys_node = *(ptr as *mut libmpv2_sys::mpv_node); + let node = SysMpvNode::new(sys_node, false); + Ok(PropertyData::Node(node.value().unwrap())) + } + mpv_format::None => unreachable!(), + _ => unimplemented!(), } - mpv_format::None => unreachable!(), - _ => unimplemented!(), } } } @@ -146,11 +148,13 @@ pub enum Event<'a> { } unsafe extern "C" fn wu_wrapper<F: Fn() + Send + 'static>(ctx: *mut c_void) { - if ctx.is_null() { - panic!("ctx for wakeup wrapper is NULL"); - } + unsafe { + if ctx.is_null() { + panic!("ctx for wakeup wrapper is NULL"); + } - (*(ctx as *mut F))(); + (*(ctx as *mut F))(); + } } /// Context to listen to events. diff --git a/crates/libmpv2/src/mpv/protocol.rs b/crates/libmpv2/src/mpv/protocol.rs index ec840d8..ee33411 100644 --- a/crates/libmpv2/src/mpv/protocol.rs +++ b/crates/libmpv2/src/mpv/protocol.rs @@ -63,26 +63,28 @@ where T: RefUnwindSafe, U: RefUnwindSafe, { - let data = user_data as *mut ProtocolData<T, U>; + unsafe { + let data = user_data as *mut ProtocolData<T, U>; - (*info).cookie = user_data; - (*info).read_fn = Some(read_wrapper::<T, U>); - (*info).seek_fn = Some(seek_wrapper::<T, U>); - (*info).size_fn = Some(size_wrapper::<T, U>); - (*info).close_fn = Some(close_wrapper::<T, U>); + (*info).cookie = user_data; + (*info).read_fn = Some(read_wrapper::<T, U>); + (*info).seek_fn = Some(seek_wrapper::<T, U>); + (*info).size_fn = Some(size_wrapper::<T, U>); + (*info).close_fn = Some(close_wrapper::<T, U>); - let ret = panic::catch_unwind(|| { - let uri = mpv_cstr_to_str!(uri as *const _).unwrap(); - ptr::write( - (*data).cookie, - ((*data).open_fn)(&mut (*data).user_data, uri), - ); - }); + let ret = panic::catch_unwind(|| { + let uri = mpv_cstr_to_str!(uri as *const _).unwrap(); + ptr::write( + (*data).cookie, + ((*data).open_fn)(&mut (*data).user_data, uri), + ); + }); - if ret.is_ok() { - 0 - } else { - mpv_error::Generic as _ + if ret.is_ok() { + 0 + } else { + mpv_error::Generic as _ + } } } @@ -95,13 +97,15 @@ where T: RefUnwindSafe, U: RefUnwindSafe, { - let data = cookie as *mut ProtocolData<T, U>; + unsafe { + let data = cookie as *mut ProtocolData<T, U>; - let ret = panic::catch_unwind(|| { - let slice = slice::from_raw_parts_mut(buf, nbytes as _); - ((*data).read_fn)(&mut *(*data).cookie, slice) - }); - ret.unwrap_or(-1) + let ret = panic::catch_unwind(|| { + let slice = slice::from_raw_parts_mut(buf, nbytes as _); + ((*data).read_fn)(&mut *(*data).cookie, slice) + }); + ret.unwrap_or(-1) + } } unsafe extern "C" fn seek_wrapper<T, U>(cookie: *mut ctype::c_void, offset: i64) -> i64 @@ -109,18 +113,21 @@ where T: RefUnwindSafe, U: RefUnwindSafe, { - let data = cookie as *mut ProtocolData<T, U>; + unsafe { + let data = cookie as *mut ProtocolData<T, U>; - if (*data).seek_fn.is_none() { - return mpv_error::Unsupported as _; - } + if (*data).seek_fn.is_none() { + return mpv_error::Unsupported as _; + } - let ret = - panic::catch_unwind(|| (*(*data).seek_fn.as_ref().unwrap())(&mut *(*data).cookie, offset)); - if let Ok(ret) = ret { - ret - } else { - mpv_error::Generic as _ + let ret = panic::catch_unwind(|| { + (*(*data).seek_fn.as_ref().unwrap())(&mut *(*data).cookie, offset) + }); + if let Ok(ret) = ret { + ret + } else { + mpv_error::Generic as _ + } } } @@ -129,17 +136,20 @@ where T: RefUnwindSafe, U: RefUnwindSafe, { - let data = cookie as *mut ProtocolData<T, U>; + unsafe { + let data = cookie as *mut ProtocolData<T, U>; - if (*data).size_fn.is_none() { - return mpv_error::Unsupported as _; - } + if (*data).size_fn.is_none() { + return mpv_error::Unsupported as _; + } - let ret = panic::catch_unwind(|| (*(*data).size_fn.as_ref().unwrap())(&mut *(*data).cookie)); - if let Ok(ret) = ret { - ret - } else { - mpv_error::Unsupported as _ + let ret = + panic::catch_unwind(|| (*(*data).size_fn.as_ref().unwrap())(&mut *(*data).cookie)); + if let Ok(ret) = ret { + ret + } else { + mpv_error::Unsupported as _ + } } } @@ -149,9 +159,11 @@ where T: RefUnwindSafe, U: RefUnwindSafe, { - let data = Box::from_raw(cookie as *mut ProtocolData<T, U>); + unsafe { + let data = Box::from_raw(cookie as *mut ProtocolData<T, U>); - panic::catch_unwind(|| (data.close_fn)(Box::from_raw(data.cookie))); + panic::catch_unwind(|| (data.close_fn)(Box::from_raw(data.cookie))); + } } struct ProtocolData<T, U> { @@ -224,20 +236,23 @@ impl<T: RefUnwindSafe, U: RefUnwindSafe> Protocol<T, U> { seek_fn: Option<StreamSeek<T>>, size_fn: Option<StreamSize<T>>, ) -> Protocol<T, U> { - let c_layout = Layout::from_size_align(mem::size_of::<T>(), mem::align_of::<T>()).unwrap(); - let cookie = alloc::alloc(c_layout) as *mut T; - let data = Box::into_raw(Box::new(ProtocolData { - cookie, - user_data, + unsafe { + let c_layout = + Layout::from_size_align(mem::size_of::<T>(), mem::align_of::<T>()).unwrap(); + let cookie = alloc::alloc(c_layout) as *mut T; + let data = Box::into_raw(Box::new(ProtocolData { + cookie, + user_data, - open_fn, - close_fn, - read_fn, - seek_fn, - size_fn, - })); + open_fn, + close_fn, + read_fn, + seek_fn, + size_fn, + })); - Protocol { name, data } + Protocol { name, data } + } } fn register(&self, ctx: *mut libmpv2_sys::mpv_handle) -> Result<()> { diff --git a/crates/libmpv2/src/mpv/render.rs b/crates/libmpv2/src/mpv/render.rs index 6457048..02f70bb 100644 --- a/crates/libmpv2/src/mpv/render.rs +++ b/crates/libmpv2/src/mpv/render.rs @@ -125,26 +125,30 @@ impl<C> From<&RenderParam<C>> for u32 { } unsafe extern "C" fn gpa_wrapper<GLContext>(ctx: *mut c_void, name: *const i8) -> *mut c_void { - if ctx.is_null() { - panic!("ctx for get_proc_address wrapper is NULL"); - } + unsafe { + if ctx.is_null() { + panic!("ctx for get_proc_address wrapper is NULL"); + } - let params: *mut OpenGLInitParams<GLContext> = ctx as _; - let params = &*params; - (params.get_proc_address)( - ¶ms.ctx, - CStr::from_ptr(name) - .to_str() - .expect("Could not convert function name to str"), - ) + let params: *mut OpenGLInitParams<GLContext> = ctx as _; + let params = &*params; + (params.get_proc_address)( + ¶ms.ctx, + CStr::from_ptr(name) + .to_str() + .expect("Could not convert function name to str"), + ) + } } unsafe extern "C" fn ru_wrapper<F: Fn() + Send + 'static>(ctx: *mut c_void) { - if ctx.is_null() { - panic!("ctx for render_update wrapper is NULL"); - } + unsafe { + if ctx.is_null() { + panic!("ctx for render_update wrapper is NULL"); + } - (*(ctx as *mut F))(); + (*(ctx as *mut F))(); + } } impl<C> From<OpenGLInitParams<C>> for libmpv2_sys::mpv_opengl_init_params { @@ -197,14 +201,18 @@ impl<C> From<RenderParam<C>> for libmpv2_sys::mpv_render_param { } unsafe fn free_void_data<T>(ptr: *mut c_void) { - drop(Box::<T>::from_raw(ptr as *mut T)); + unsafe { + drop(Box::<T>::from_raw(ptr as *mut T)); + } } unsafe fn free_init_params<C>(ptr: *mut c_void) { - let params = Box::from_raw(ptr as *mut libmpv2_sys::mpv_opengl_init_params); - drop(Box::from_raw( - params.get_proc_address_ctx as *mut OpenGLInitParams<C>, - )); + unsafe { + let params = Box::from_raw(ptr as *mut libmpv2_sys::mpv_opengl_init_params); + drop(Box::from_raw( + params.get_proc_address_ctx as *mut OpenGLInitParams<C>, + )); + } } impl RenderContext { diff --git a/crates/libmpv2/src/tests.rs b/crates/libmpv2/src/tests.rs index 6106eb2..68753fc 100644 --- a/crates/libmpv2/src/tests.rs +++ b/crates/libmpv2/src/tests.rs @@ -54,10 +54,10 @@ fn properties() { 0.6, f64::round(subg * f64::powi(10.0, 4)) / f64::powi(10.0, 4) ); - mpv.command("loadfile", &[ - "test-data/speech_12kbps_mb.wav", - "append-play", - ]) + mpv.command( + "loadfile", + &["test-data/speech_12kbps_mb.wav", "append-play"], + ) .unwrap(); thread::sleep(Duration::from_millis(250)); @@ -185,10 +185,10 @@ fn events() { fn node_map() { let mpv = Mpv::new().unwrap(); - mpv.command("loadfile", &[ - "test-data/speech_12kbps_mb.wav", - "append-play", - ]) + mpv.command( + "loadfile", + &["test-data/speech_12kbps_mb.wav", "append-play"], + ) .unwrap(); thread::sleep(Duration::from_millis(250)); @@ -217,10 +217,10 @@ fn node_map() { fn node_array() -> Result<()> { let mpv = Mpv::new()?; - mpv.command("loadfile", &[ - "test-data/speech_12kbps_mb.wav", - "append-play", - ]) + mpv.command( + "loadfile", + &["test-data/speech_12kbps_mb.wav", "append-play"], + ) .unwrap(); thread::sleep(Duration::from_millis(250)); diff --git a/yt/Cargo.toml b/crates/yt/Cargo.toml index 6f6e470..17d4016 100644 --- a/yt/Cargo.toml +++ b/crates/yt/Cargo.toml @@ -24,21 +24,21 @@ rust-version.workspace = true publish = false [dependencies] -anyhow = "1.0.96" -blake3 = "1.6.0" -chrono = { version = "0.4.39", features = ["now"] } +anyhow = "1.0.98" +blake3 = "1.8.2" +chrono = { version = "0.4.41", features = ["now"] } chrono-humanize = "0.2.3" -clap = { version = "4.5.30", features = ["derive"] } +clap = { version = "4.5.40", features = ["derive"] } futures = "0.3.31" nucleo-matcher = "0.3.1" -owo-colors = "4.1.0" +owo-colors = "4.2.1" regex = "1.11.1" -sqlx = { version = "0.8.3", features = ["runtime-tokio", "sqlite"] } +sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] } stderrlog = "0.6.0" -tempfile = "3.17.1" -toml = "0.8.20" +tempfile = "3.20.0" +toml = "0.8.23" trinitry = { version = "0.2.2" } -xdg = "2.5.2" +xdg = "3.0.0" bytes.workspace = true libmpv2.workspace = true log.workspace = true diff --git a/crates/yt/src/ansi_escape_codes.rs b/crates/yt/src/ansi_escape_codes.rs new file mode 100644 index 0000000..ae1805d --- /dev/null +++ b/crates/yt/src/ansi_escape_codes.rs @@ -0,0 +1,26 @@ +// see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands +const CSI: &str = "\x1b["; +pub fn erase_in_display_from_cursor() { + print!("{CSI}0J"); +} +pub fn cursor_up(number: usize) { + // HACK(@bpeetz): The default is `1` and running this command with a + // number of `0` results in it using the default (i.e., `1`) <2025-03-25> + if number != 0 { + print!("{CSI}{number}A"); + } +} + +pub fn clear_whole_line() { + eprint!("{CSI}2K"); +} +pub fn move_to_col(x: usize) { + eprint!("{CSI}{x}G"); +} + +pub fn hide_cursor() { + eprint!("{CSI}?25l"); +} +pub fn show_cursor() { + eprint!("{CSI}?25h"); +} diff --git a/yt/src/app.rs b/crates/yt/src/app.rs index 15a9388..15a9388 100644 --- a/yt/src/app.rs +++ b/crates/yt/src/app.rs diff --git a/yt/src/cache/mod.rs b/crates/yt/src/cache/mod.rs index 83d5ee0..83d5ee0 100644 --- a/yt/src/cache/mod.rs +++ b/crates/yt/src/cache/mod.rs diff --git a/yt/src/cli.rs b/crates/yt/src/cli.rs index 037f45c..de7a5b8 100644 --- a/yt/src/cli.rs +++ b/crates/yt/src/cli.rs @@ -40,10 +40,6 @@ pub struct CliArgs { #[arg(long, short, action=ArgAction::SetTrue, default_value_t = false)] pub no_migrate_db: bool, - /// Increase message verbosity - #[arg(long="verbose", short = 'v', action = ArgAction::Count)] - pub verbosity: u8, - /// Display colors [defaults to true, if the config file has no value] #[arg(long, short = 'C')] pub color: Option<bool>, @@ -57,6 +53,10 @@ pub struct CliArgs { #[arg(long, short)] pub config_path: Option<PathBuf>, + /// Increase message verbosity + #[arg(long="verbose", short = 'v', action = ArgAction::Count)] + pub verbosity: u8, + /// Silence all output #[arg(long, short = 'q')] pub quiet: bool, @@ -103,12 +103,6 @@ pub enum Command { /// Show, the configuration options in effect Config {}, - /// Perform various tests - Check { - #[command(subcommand)] - command: CheckCommand, - }, - /// Display the comments of the currently playing video Comments {}, /// Display the description of the currently playing video @@ -355,12 +349,6 @@ impl Default for SelectCommand { } } -#[derive(Subcommand, Clone, Debug)] -pub enum CheckCommand { - /// Check if the given `*.info.json` file is deserializable. - InfoJson { path: PathBuf }, -} - #[derive(Subcommand, Clone, Copy, Debug)] pub enum CacheCommand { /// Invalidate all cache entries diff --git a/crates/yt/src/comments/comment.rs b/crates/yt/src/comments/comment.rs new file mode 100644 index 0000000..5bc939c --- /dev/null +++ b/crates/yt/src/comments/comment.rs @@ -0,0 +1,152 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use serde::{Deserialize, Deserializer, Serialize}; +use url::Url; + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)] +#[serde(from = "String")] +#[serde(deny_unknown_fields)] +pub enum Parent { + Root, + Id(String), +} + +impl Parent { + #[must_use] + pub fn id(&self) -> Option<&str> { + if let Self::Id(id) = self { + Some(id) + } else { + None + } + } +} + +impl From<String> for Parent { + fn from(value: String) -> Self { + if value == "root" { + Self::Root + } else { + Self::Id(value) + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)] +#[serde(from = "String")] +#[serde(deny_unknown_fields)] +pub struct Id { + pub id: String, +} +impl From<String> for Id { + fn from(value: String) -> Self { + Self { + // Take the last element if the string is split with dots, otherwise take the full id + id: value.split('.').last().unwrap_or(&value).to_owned(), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)] +#[allow(clippy::struct_excessive_bools)] +pub struct Comment { + pub id: Id, + pub text: String, + #[serde(default = "zero")] + pub like_count: u32, + pub is_pinned: bool, + pub author_id: String, + #[serde(default = "unknown")] + pub author: String, + pub author_is_verified: bool, + pub author_thumbnail: Url, + pub parent: Parent, + #[serde(deserialize_with = "edited_from_time_text", alias = "_time_text")] + pub edited: bool, + // Can't also be deserialized, as it's already used in 'edited' + // _time_text: String, + pub timestamp: i64, + pub author_url: Option<Url>, + pub author_is_uploader: bool, + pub is_favorited: bool, +} + +fn unknown() -> String { + "<Unknown>".to_string() +} +fn zero() -> u32 { + 0 +} +fn edited_from_time_text<'de, D>(d: D) -> Result<bool, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(d)?; + if s.contains(" (edited)") { + Ok(true) + } else { + Ok(false) + } +} + +#[derive(Debug, Clone)] +#[allow(clippy::module_name_repetitions)] +pub struct CommentExt { + pub value: Comment, + pub replies: Vec<CommentExt>, +} + +#[derive(Debug, Default)] +pub struct Comments { + pub(super) vec: Vec<CommentExt>, +} + +impl Comments { + pub fn new() -> Self { + Self::default() + } + pub fn push(&mut self, value: CommentExt) { + self.vec.push(value); + } + pub fn get_mut(&mut self, key: &str) -> Option<&mut CommentExt> { + self.vec.iter_mut().filter(|c| c.value.id.id == key).last() + } + pub fn insert(&mut self, key: &str, value: CommentExt) { + let parent = self + .vec + .iter_mut() + .filter(|c| c.value.id.id == key) + .last() + .expect("One of these should exist"); + parent.push_reply(value); + } +} +impl CommentExt { + pub fn push_reply(&mut self, value: CommentExt) { + self.replies.push(value); + } + pub fn get_mut_reply(&mut self, key: &str) -> Option<&mut CommentExt> { + self.replies + .iter_mut() + .filter(|c| c.value.id.id == key) + .last() + } +} + +impl From<Comment> for CommentExt { + fn from(value: Comment) -> Self { + Self { + replies: vec![], + value, + } + } +} diff --git a/yt/src/comments/description.rs b/crates/yt/src/comments/description.rs index d22a40f..e8cb29d 100644 --- a/yt/src/comments/description.rs +++ b/crates/yt/src/comments/description.rs @@ -17,7 +17,7 @@ use crate::{ }; use anyhow::{Result, bail}; -use yt_dlp::wrapper::info_json::InfoJson; +use yt_dlp::{InfoJson, json_cast}; pub async fn description(app: &App) -> Result<()> { let description = get(app).await?; @@ -39,6 +39,8 @@ pub async fn get(app: &App) -> Result<String> { ); Ok(info_json - .description - .unwrap_or("<No description>".to_owned())) + .get("description") + .map(|val| json_cast!(val, as_str)) + .unwrap_or("<No description>") + .to_owned()) } diff --git a/yt/src/comments/display.rs b/crates/yt/src/comments/display.rs index 6166b2b..6166b2b 100644 --- a/yt/src/comments/display.rs +++ b/crates/yt/src/comments/display.rs diff --git a/yt/src/comments/mod.rs b/crates/yt/src/comments/mod.rs index daecf8d..876146d 100644 --- a/yt/src/comments/mod.rs +++ b/crates/yt/src/comments/mod.rs @@ -11,11 +11,11 @@ use std::mem; -use anyhow::{Context, Result, bail}; -use comment::{CommentExt, Comments}; +use anyhow::{Result, bail}; +use comment::{Comment, CommentExt, Comments, Parent}; use output::display_fmt_and_less; use regex::Regex; -use yt_dlp::wrapper::info_json::{Comment, InfoJson, Parent}; +use yt_dlp::{InfoJson, json_cast}; use crate::{ app::App, @@ -39,23 +39,25 @@ pub async fn get(app: &App) -> Result<Comments> { bail!("Could not find a currently playing video!"); }; - let mut info_json: InfoJson = get::video_info_json(¤tly_playing_video)?.unreachable( - "A currently *playing* must be cached. And thus the info.json should be available", + let info_json: InfoJson = get::video_info_json(¤tly_playing_video)?.unreachable( + "A currently *playing* video must be cached. And thus the info.json should be available", ); - let base_comments = mem::take(&mut info_json.comments).with_context(|| { - format!( + let base_comments = if let Some(comments) = info_json.get("comments") { + json_cast!(comments, as_array) + } else { + bail!( "The video ('{}') does not have comments!", info_json - .title - .as_ref() - .unwrap_or(&("<No Title>".to_owned())) + .get("title") + .map(|val| json_cast!(val, as_str)) + .unwrap_or("<No Title>") ) - })?; - drop(info_json); + }; let mut comments = Comments::new(); for c in base_comments { + let c: Comment = serde_json::from_value(c.to_owned())?; if let Parent::Id(id) = &c.parent { comments.insert(&(id.clone()), CommentExt::from(c)); } else { diff --git a/yt/src/comments/output.rs b/crates/yt/src/comments/output.rs index cb3a9c4..cb3a9c4 100644 --- a/yt/src/comments/output.rs +++ b/crates/yt/src/comments/output.rs diff --git a/yt/src/config/default.rs b/crates/yt/src/config/default.rs index a1d327a..4ed643b 100644 --- a/yt/src/config/default.rs +++ b/crates/yt/src/config/default.rs @@ -14,19 +14,19 @@ use std::path::PathBuf; use anyhow::{Context, Result}; fn get_runtime_path(name: &'static str) -> Result<PathBuf> { - let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?; + let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX); xdg_dirs .place_runtime_file(name) .with_context(|| format!("Failed to place runtime file: '{name}'")) } fn get_data_path(name: &'static str) -> Result<PathBuf> { - let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?; + let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX); xdg_dirs .place_data_file(name) .with_context(|| format!("Failed to place data file: '{name}'")) } fn get_config_path(name: &'static str) -> Result<PathBuf> { - let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?; + let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX); xdg_dirs .place_config_file(name) .with_context(|| format!("Failed to place config file: '{name}'")) diff --git a/yt/src/config/definitions.rs b/crates/yt/src/config/definitions.rs index ce8c0d4..ce8c0d4 100644 --- a/yt/src/config/definitions.rs +++ b/crates/yt/src/config/definitions.rs diff --git a/yt/src/config/file_system.rs b/crates/yt/src/config/file_system.rs index 2463e9d..2463e9d 100644 --- a/yt/src/config/file_system.rs +++ b/crates/yt/src/config/file_system.rs diff --git a/yt/src/config/mod.rs b/crates/yt/src/config/mod.rs index a10f7c2..a10f7c2 100644 --- a/yt/src/config/mod.rs +++ b/crates/yt/src/config/mod.rs diff --git a/yt/src/constants.rs b/crates/yt/src/constants.rs index 0f5b918..0f5b918 100644 --- a/yt/src/constants.rs +++ b/crates/yt/src/constants.rs diff --git a/crates/yt/src/download/download_options.rs b/crates/yt/src/download/download_options.rs new file mode 100644 index 0000000..03c20ba --- /dev/null +++ b/crates/yt/src/download/download_options.rs @@ -0,0 +1,118 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use anyhow::Context; +use serde_json::{Value, json}; +use yt_dlp::{YoutubeDL, YoutubeDLOptions}; + +use crate::{app::App, storage::video_database::YtDlpOptions}; + +use super::progress_hook::wrapped_progress_hook; + +pub fn download_opts(app: &App, additional_opts: &YtDlpOptions) -> anyhow::Result<YoutubeDL> { + YoutubeDLOptions::new() + .with_progress_hook(wrapped_progress_hook) + .set("extract_flat", "in_playlist") + .set( + "extractor_args", + json! { + { + "youtube": { + "comment_sort": [ "top" ], + "max_comments": [ "150", "all", "100" ] + } + } + }, + ) + //.set("cookiesfrombrowser", json! {("firefox", "me.google", None::<String>, "youtube_dlp")}) + .set("prefer_free_formats", true) + .set("ffmpeg_location", env!("FFMPEG_LOCATION")) + .set("format", "bestvideo[height<=?1080]+bestaudio/best") + .set("fragment_retries", 10) + .set("getcomments", true) + .set("ignoreerrors", false) + .set("retries", 10) + .set("writeinfojson", true) + // NOTE: This results in a constant warning message. <2025-01-04> + //.set("writeannotations", true) + .set("writesubtitles", true) + .set("writeautomaticsub", true) + .set( + "outtmpl", + json! { + { + "default": app.config.paths.download_dir.join("%(channel)s/%(title)s.%(ext)s"), + "chapter": "%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s" + } + }, + ) + .set("compat_opts", json! {{}}) + .set("forceprint", json! {{}}) + .set("print_to_file", json! {{}}) + .set("windowsfilenames", false) + .set("restrictfilenames", false) + .set("trim_file_names", false) + .set( + "postprocessors", + json! { + [ + { + "api": "https://sponsor.ajay.app", + "categories": [ + "interaction", + "intro", + "music_offtopic", + "sponsor", + "outro", + "poi_highlight", + "preview", + "selfpromo", + "filler", + "chapter" + ], + "key": "SponsorBlock", + "when": "after_filter" + }, + { + "force_keyframes": false, + "key": "ModifyChapters", + "remove_chapters_patterns": [], + "remove_ranges": [], + "remove_sponsor_segments": [ "sponsor" ], + "sponsorblock_chapter_title": "[SponsorBlock]: %(category_names)l" + }, + { + "add_chapters": true, + "add_infojson": null, + "add_metadata": false, + "key": "FFmpegMetadata" + }, + { + "key": "FFmpegConcat", + "only_multi_video": true, + "when": "playlist" + } + ] + }, + ) + .set( + "subtitleslangs", + Value::Array( + additional_opts + .subtitle_langs + .split(',') + .map(|val| Value::String(val.to_owned())) + .collect::<Vec<_>>(), + ), + ) + .build() + .context("Failed to instanciate download yt_dlp") +} diff --git a/yt/src/download/mod.rs b/crates/yt/src/download/mod.rs index 984d400..110bf55 100644 --- a/yt/src/download/mod.rs +++ b/crates/yt/src/download/mod.rs @@ -29,9 +29,11 @@ use bytes::Bytes; use futures::{FutureExt, future::BoxFuture}; use log::{debug, error, info, warn}; use tokio::{fs, task::JoinHandle, time}; +use yt_dlp::{json_cast, json_get}; #[allow(clippy::module_name_repetitions)] pub mod download_options; +pub mod progress_hook; #[derive(Debug)] #[allow(clippy::module_name_repetitions)] @@ -109,7 +111,7 @@ impl Downloader { } } let cache_allocation = Self::get_current_cache_allocation(app).await?; - let video_size = self.get_approx_video_size(app, next_video).await?; + let video_size = self.get_approx_video_size(app, next_video)?; if video_size >= max_cache_size { error!( @@ -291,7 +293,7 @@ impl Downloader { dir_size(read_dir_result).await } - async fn get_approx_video_size(&mut self, app: &App, video: &Video) -> Result<u64> { + fn get_approx_video_size(&mut self, app: &App, video: &Video) -> Result<u64> { if let Some(value) = self.video_size_cache.get(&video.extractor_hash) { Ok(*value) } else { @@ -299,25 +301,25 @@ impl Downloader { let add_opts = YtDlpOptions { subtitle_langs: String::new(), }; - let opts = &download_opts(app, &add_opts); + let yt_dlp = download_opts(app, &add_opts)?; - let result = yt_dlp::extract_info(opts, &video.url, false, true) - .await + let result = yt_dlp + .extract_info(&video.url, false, true) .with_context(|| { format!("Failed to extract video information: '{}'", video.title) })?; - let size = if let Some(val) = result.filesize { - val - } else if let Some(val) = result.filesize_approx { - val - } else if result.duration.is_some() && result.tbr.is_some() { + let size = if let Some(val) = result.get("filesize") { + json_cast!(val, as_u64) + } else if let Some(val) = result.get("filesize_approx") { + json_cast!(val, as_u64) + } else if result.get("duration").is_some() && result.get("tbr").is_some() { #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] - let duration = result.duration.expect("Is some").ceil() as u64; + let duration = json_get!(result, "duration", as_f64).ceil() as u64; // TODO: yt_dlp gets this from the format #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] - let tbr = result.tbr.expect("Is Some").ceil() as u64; + let tbr = json_get!(result, "tbr", as_f64).ceil() as u64; duration * tbr * (1000 / 8) } else { @@ -342,9 +344,10 @@ impl Downloader { debug!("Download started: {}", &video.title); let addional_opts = get_video_yt_dlp_opts(app, &video.extractor_hash).await?; + let yt_dlp = download_opts(app, &addional_opts)?; - let result = yt_dlp::download(&[video.url.clone()], &download_opts(app, &addional_opts)) - .await + let result = yt_dlp + .download(&[video.url.to_owned()]) .with_context(|| format!("Failed to download video: '{}'", video.title))?; assert_eq!(result.len(), 1); diff --git a/crates/yt/src/download/progress_hook.rs b/crates/yt/src/download/progress_hook.rs new file mode 100644 index 0000000..b75ec00 --- /dev/null +++ b/crates/yt/src/download/progress_hook.rs @@ -0,0 +1,188 @@ +use std::{ + io::{Write, stderr}, + process, +}; + +use bytes::Bytes; +use log::{Level, log_enabled}; +use yt_dlp::mk_python_function; + +use crate::{ + ansi_escape_codes::{clear_whole_line, move_to_col}, + select::selection_file::duration::MaybeDuration, +}; + +/// # Panics +/// If expectations fail. +#[allow(clippy::too_many_lines, clippy::needless_pass_by_value)] +pub fn progress_hook( + input: serde_json::Map<String, serde_json::Value>, +) -> Result<(), std::io::Error> { + // Only add the handler, if the log-level is higher than Debug (this avoids covering debug + // messages). + if log_enabled!(Level::Debug) { + return Ok(()); + } + + macro_rules! get { + (@interrogate $item:ident, $type_fun:ident, $get_fun:ident, $name:expr) => {{ + let a = $item.get($name).expect(concat!( + "The field '", + stringify!($name), + "' should exist." + )); + + if a.$type_fun() { + a.$get_fun().expect( + "The should have been checked in the if guard, so unpacking here is fine", + ) + } else { + panic!( + "Value {} => \n{}\n is not of type: {}", + $name, + a, + stringify!($type_fun) + ); + } + }}; + + ($type_fun:ident, $get_fun:ident, $name1:expr, $name2:expr) => {{ + let a = get! {@interrogate input, is_object, as_object, $name1}; + let b = get! {@interrogate a, $type_fun, $get_fun, $name2}; + b + }}; + + ($type_fun:ident, $get_fun:ident, $name:expr) => {{ + get! {@interrogate input, $type_fun, $get_fun, $name} + }}; + } + + macro_rules! default_get { + (@interrogate $item:ident, $default:expr, $get_fun:ident, $name:expr) => {{ + let a = if let Some(field) = $item.get($name) { + field.$get_fun().unwrap_or($default) + } else { + $default + }; + a + }}; + + ($get_fun:ident, $default:expr, $name1:expr, $name2:expr) => {{ + let a = get! {@interrogate input, is_object, as_object, $name1}; + let b = default_get! {@interrogate a, $default, $get_fun, $name2}; + b + }}; + + ($get_fun:ident, $default:expr, $name:expr) => {{ + default_get! {@interrogate input, $default, $get_fun, $name} + }}; + } + + macro_rules! c { + ($color:expr, $format:expr) => { + format!("\x1b[{}m{}\x1b[0m", $color, $format) + }; + } + + #[allow(clippy::items_after_statements)] + fn format_bytes(bytes: u64) -> String { + let bytes = Bytes::new(bytes); + bytes.to_string() + } + + #[allow(clippy::items_after_statements)] + fn format_speed(speed: f64) -> String { + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let bytes = Bytes::new(speed.floor() as u64); + format!("{bytes}/s") + } + + let get_title = || -> String { + match get! {is_string, as_str, "info_dict", "ext"} { + "vtt" => { + format!( + "Subtitles ({})", + default_get! {as_str, "<No Subtitle Language>", "info_dict", "name"} + ) + } + "webm" | "mp4" | "mp3" | "m4a" => { + default_get! { as_str, "<No title>", "info_dict", "title"}.to_owned() + } + other => panic!("The extension '{other}' is not yet implemented"), + } + }; + + match get! {is_string, as_str, "status"} { + "downloading" => { + let elapsed = default_get! {as_f64, 0.0f64, "elapsed"}; + let eta = default_get! {as_f64, 0.0, "eta"}; + let speed = default_get! {as_f64, 0.0, "speed"}; + + let downloaded_bytes = get! {is_u64, as_u64, "downloaded_bytes"}; + let (total_bytes, bytes_is_estimate): (u64, &'static str) = { + let total_bytes = default_get!(as_u64, 0, "total_bytes"); + if total_bytes == 0 { + let maybe_estimate = default_get!(as_u64, 0, "total_bytes_estimate"); + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + if maybe_estimate == 0 { + // The download speed should be in bytes per second and the eta in seconds. + // Thus multiplying them gets us the raw bytes (which were estimated by `yt_dlp`, from their `info.json`) + let bytes_still_needed = (speed * eta).ceil() as u64; + + (downloaded_bytes + bytes_still_needed, "~") + } else { + (maybe_estimate, "~") + } + } else { + (total_bytes, "") + } + }; + + let percent: f64 = { + if total_bytes == 0 { + 100.0 + } else { + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss + )] + { + (downloaded_bytes as f64 / total_bytes as f64) * 100.0 + } + } + }; + + clear_whole_line(); + move_to_col(1); + + eprint!( + "'{}' [{}/{} at {}] -> [{} of {}{} {}] ", + c!("34;1", get_title()), + c!("33;1", MaybeDuration::from_secs_f64(elapsed)), + c!("33;1", MaybeDuration::from_secs_f64(eta)), + c!("32;1", format_speed(speed)), + c!("31;1", format_bytes(downloaded_bytes)), + c!("31;1", bytes_is_estimate), + c!("31;1", format_bytes(total_bytes)), + c!("36;1", format!("{:.02}%", percent)) + ); + stderr().flush()?; + } + "finished" => { + eprintln!("-> Finished downloading."); + } + "error" => { + // TODO: This should probably return an Err. But I'm not so sure where the error would + // bubble up to (i.e., who would catch it) <2025-01-21> + eprintln!("-> Error while downloading: {}", get_title()); + process::exit(1); + } + other => unreachable!("'{other}' should not be a valid state!"), + } + + Ok(()) +} + +mk_python_function!(progress_hook, wrapped_progress_hook); diff --git a/yt/src/main.rs b/crates/yt/src/main.rs index ffb3e14..39f52f4 100644 --- a/yt/src/main.rs +++ b/crates/yt/src/main.rs @@ -13,16 +13,16 @@ // to print it anyways. #![allow(clippy::missing_errors_doc)] -use std::{fs, sync::Arc}; +use std::sync::Arc; use anyhow::{Context, Result, bail}; use app::App; use bytes::Bytes; use cache::{invalidate, maintain}; use clap::Parser; -use cli::{CacheCommand, CheckCommand, SelectCommand, SubscriptionCommand, VideosCommand}; +use cli::{CacheCommand, SelectCommand, SubscriptionCommand, VideosCommand}; use config::Config; -use log::info; +use log::{error, info}; use select::cmds::handle_select_cmd; use storage::video_database::get::video_by_hash; use tokio::{ @@ -30,10 +30,10 @@ use tokio::{ io::{BufReader, stdin}, task::JoinHandle, }; -use yt_dlp::wrapper::info_json::InfoJson; use crate::{cli::Command, storage::subscriptions}; +pub mod ansi_escape_codes; pub mod app; pub mod cli; pub mod unreachable; @@ -200,7 +200,7 @@ async fn main() -> Result<()> { subscribe::import(&app, BufReader::new(f), force).await?; } else { subscribe::import(&app, BufReader::new(stdin()), force).await?; - }; + } } }, @@ -215,17 +215,6 @@ async fn main() -> Result<()> { CacheCommand::Maintain { all } => maintain(&app, all).await?, }, - Command::Check { command } => match command { - CheckCommand::InfoJson { path } => { - let string = fs::read_to_string(&path) - .with_context(|| format!("Failed to read '{}' to string!", path.display()))?; - - drop( - serde_json::from_str::<InfoJson>(&string) - .context("Failed to deserialize value")?, - ); - } - }, Command::Comments {} => { comments::comments(&app).await?; } @@ -242,15 +231,17 @@ async fn dowa(arc_app: Arc<App>) -> Result<()> { info!("Max cache size: '{}'", max_cache_size); let arc_app_clone = Arc::clone(&arc_app); - let download: JoinHandle<Result<()>> = tokio::spawn(async move { - download::Downloader::new() + let download: JoinHandle<()> = tokio::spawn(async move { + let result = download::Downloader::new() .consume(arc_app_clone, max_cache_size.as_u64()) - .await?; + .await; - Ok(()) + if let Err(err) = result { + error!("Error from downloader: {err:?}"); + } }); watch::watch(arc_app).await?; - download.await??; + download.await?; Ok(()) } diff --git a/yt/src/select/cmds/add.rs b/crates/yt/src/select/cmds/add.rs index da58ec2..387b3a1 100644 --- a/yt/src/select/cmds/add.rs +++ b/crates/yt/src/select/cmds/add.rs @@ -14,15 +14,13 @@ use crate::{ storage::video_database::{ self, extractor_hash::ExtractorHash, get::get_all_hashes, set::add_video, }, - unreachable::Unreachable, update::video_entry_to_video, }; use anyhow::{Context, Result, bail}; use log::{error, warn}; -use serde_json::{Map, Value}; use url::Url; -use yt_dlp::wrapper::info_json::InfoType; +use yt_dlp::{InfoJson, YoutubeDL, json_cast, json_get}; #[allow(clippy::too_many_lines)] pub(super) async fn add( @@ -32,17 +30,11 @@ pub(super) async fn add( stop: Option<usize>, ) -> Result<()> { for url in urls { - async fn process_and_add( - app: &App, - entry: yt_dlp::wrapper::info_json::InfoJson, - opts: &Map<String, Value>, - ) -> Result<()> { - let url = entry - .url - .unreachable("`yt_dlp` should guarantee that this is Some at this point"); - - let entry = yt_dlp::extract_info(opts, &url, false, true) - .await + async fn process_and_add(app: &App, entry: InfoJson, yt_dlp: &YoutubeDL) -> Result<()> { + let url = json_get!(entry, "url", as_str).parse()?; + + let entry = yt_dlp + .extract_info(&url, false, true) .with_context(|| format!("Failed to fetch entry for url: '{url}'"))?; add_entry(app, entry).await?; @@ -50,19 +42,13 @@ pub(super) async fn add( Ok(()) } - async fn add_entry(app: &App, entry: yt_dlp::wrapper::info_json::InfoJson) -> Result<()> { + async fn add_entry(app: &App, entry: InfoJson) -> Result<()> { // We have to re-fetch all hashes every time, because a user could try to add the same // URL twice (for whatever reason.) let hashes = get_all_hashes(app) .await .context("Failed to fetch all video hashes")?; - let extractor_hash = blake3::hash( - entry - .id - .as_ref() - .expect("This should be some at this point") - .as_bytes(), - ); + let extractor_hash = blake3::hash(json_get!(entry, "id", as_str).as_bytes()); if hashes.contains(&extractor_hash) { error!( "Video '{}'{} is already in the database. Skipped adding it", @@ -72,17 +58,17 @@ pub(super) async fn add( .with_context(|| format!( "Failed to format hash of video '{}' as short hash", entry - .url - .map_or("<Unknown video Url>".to_owned(), |url| url.to_string()) + .get("url") + .map_or("<Unknown video Url>".to_owned(), ToString::to_string) ))?, entry - .title + .get("title") .map_or(String::new(), |title| format!(" ('{title}')")) ); return Ok(()); } - let video = video_entry_to_video(entry, None)?; + let video = video_entry_to_video(&entry, None)?; add_video(app, video.clone()).await?; println!("{}", &video.to_line_display(app).await?); @@ -90,16 +76,19 @@ pub(super) async fn add( Ok(()) } - let opts = download_opts(app, &video_database::YtDlpOptions { - subtitle_langs: String::new(), - }); + let yt_dlp = download_opts( + app, + &video_database::YtDlpOptions { + subtitle_langs: String::new(), + }, + )?; - let entry = yt_dlp::extract_info(&opts, &url, false, true) - .await + let entry = yt_dlp + .extract_info(&url, false, true) .with_context(|| format!("Failed to fetch entry for url: '{url}'"))?; - match entry._type { - Some(InfoType::Video) => { + match entry.get("_type").map(|val| json_cast!(val, as_str)) { + Some("Video") => { add_entry(app, entry).await?; if start.is_some() || stop.is_some() { warn!( @@ -107,13 +96,14 @@ pub(super) async fn add( ); } } - Some(InfoType::Playlist) => { - if let Some(entries) = entry.entries { + Some("Playlist") => { + if let Some(entries) = entry.get("entries") { + let entries = json_cast!(entries, as_array); let start = start.unwrap_or(0); let stop = stop.unwrap_or(entries.len() - 1); - let mut respected_entries: Vec<_> = take_vector(entries, start, stop) - .with_context(|| { + let respected_entries = + take_vector(entries, start, stop).with_context(|| { format!( "Failed to take entries starting at: {start} and ending with {stop}" ) @@ -123,11 +113,23 @@ pub(super) async fn add( warn!("No entries found, after applying your start/stop limits."); } else { // Pre-warm the cache - process_and_add(app, respected_entries.remove(0), &opts).await?; + process_and_add( + app, + json_cast!(respected_entries[0], as_object).to_owned(), + &yt_dlp, + ) + .await?; + let respected_entries = &respected_entries[1..]; let futures: Vec<_> = respected_entries - .into_iter() - .map(|entry| process_and_add(app, entry, &opts)) + .iter() + .map(|entry| { + process_and_add( + app, + json_cast!(entry, as_object).to_owned(), + &yt_dlp, + ) + }) .collect(); for fut in futures { @@ -148,7 +150,7 @@ pub(super) async fn add( Ok(()) } -fn take_vector<T>(vector: Vec<T>, start: usize, stop: usize) -> Result<Vec<T>> { +fn take_vector<T>(vector: &[T], start: usize, stop: usize) -> Result<&[T]> { let length = vector.len(); if stop >= length { @@ -157,26 +159,7 @@ fn take_vector<T>(vector: Vec<T>, start: usize, stop: usize) -> Result<Vec<T>> { ); } - let end_skip = { - let base = length - .checked_sub(stop) - .unreachable("The check above should have caught this case."); - - base.checked_sub(1) - .unreachable("The check above should have caught this case.") - }; - - // NOTE: We're using this instead of the `vector[start..=stop]` notation, because I wanted to - // avoid the needed allocation to turn the slice into a vector. <2025-01-04> - - // TODO: This function could also just return a slice, but oh well.. <2025-01-04> - Ok(vector - .into_iter() - .skip(start) - .rev() - .skip(end_skip) - .rev() - .collect()) + Ok(&vector[start..=stop]) } #[cfg(test)] @@ -187,7 +170,7 @@ mod test { fn test_vector_take() { let vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - let new_vec = take_vector(vec, 2, 8).unwrap(); + let new_vec = take_vector(&vec, 2, 8).unwrap(); assert_eq!(new_vec, vec![2, 3, 4, 5, 6, 7, 8]); } @@ -196,13 +179,13 @@ mod test { fn test_vector_take_overflow() { let vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - assert!(take_vector(vec, 0, 12).is_err()); + assert!(take_vector(&vec, 0, 12).is_err()); } #[test] fn test_vector_take_equal() { let vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - assert!(take_vector(vec, 0, 11).is_err()); + assert!(take_vector(&vec, 0, 11).is_err()); } } diff --git a/yt/src/select/cmds/mod.rs b/crates/yt/src/select/cmds/mod.rs index ea41f99..aabcd3d 100644 --- a/yt/src/select/cmds/mod.rs +++ b/crates/yt/src/select/cmds/mod.rs @@ -51,10 +51,15 @@ pub async fn handle_select_cmd( is_focused, } = video.status { - handle_status_change(app, shared, line_number, VideoStatus::Cached { - cache_path, - is_focused, - }) + handle_status_change( + app, + shared, + line_number, + VideoStatus::Cached { + cache_path, + is_focused, + }, + ) .await?; } else { handle_status_change(app, shared, line_number, VideoStatus::Watch).await?; diff --git a/yt/src/select/mod.rs b/crates/yt/src/select/mod.rs index 54db65c..8db9ae3 100644 --- a/yt/src/select/mod.rs +++ b/crates/yt/src/select/mod.rs @@ -53,12 +53,15 @@ pub async fn select(app: &App, done: bool, use_last_selection: bool) -> Result<( let matching_videos = if done { get::videos(app, VideoStatusMarker::ALL).await? } else { - get::videos(app, &[ - VideoStatusMarker::Pick, - // - VideoStatusMarker::Watch, - VideoStatusMarker::Cached, - ]) + get::videos( + app, + &[ + VideoStatusMarker::Pick, + // + VideoStatusMarker::Watch, + VideoStatusMarker::Cached, + ], + ) .await? }; diff --git a/yt/src/select/selection_file/duration.rs b/crates/yt/src/select/selection_file/duration.rs index 77c4fc5..77c4fc5 100644 --- a/yt/src/select/selection_file/duration.rs +++ b/crates/yt/src/select/selection_file/duration.rs diff --git a/yt/src/select/selection_file/help.str b/crates/yt/src/select/selection_file/help.str index e3cc347..e3cc347 100644 --- a/yt/src/select/selection_file/help.str +++ b/crates/yt/src/select/selection_file/help.str diff --git a/yt/src/select/selection_file/help.str.license b/crates/yt/src/select/selection_file/help.str.license index a0e196c..a0e196c 100644 --- a/yt/src/select/selection_file/help.str.license +++ b/crates/yt/src/select/selection_file/help.str.license diff --git a/yt/src/select/selection_file/mod.rs b/crates/yt/src/select/selection_file/mod.rs index abd26c4..abd26c4 100644 --- a/yt/src/select/selection_file/mod.rs +++ b/crates/yt/src/select/selection_file/mod.rs diff --git a/yt/src/status/mod.rs b/crates/yt/src/status/mod.rs index bc45cfb..18bef7d 100644 --- a/yt/src/status/mod.rs +++ b/crates/yt/src/status/mod.rs @@ -87,6 +87,15 @@ pub async fn show(app: &App) -> Result<()> { } }; + let watch_rate: f64 = { + fn to_f64(input: usize) -> f64 { + f64::from(u32::try_from(input).expect("This should never exceed u32::MAX")) + } + + let count = to_f64(watched_videos_len) / (to_f64(drop_videos_len) + to_f64(dropped_videos_len)); + count * 100.0 + }; + let cache_usage_raw = Downloader::get_current_cache_allocation(app) .await .context("Failed to get current cache allocation")?; @@ -97,7 +106,7 @@ Picked Videos: {picked_videos_len} Watch Videos: {watch_videos_len} Cached Videos: {cached_videos_len} -Watched Videos: {watched_videos_len} +Watched Videos: {watched_videos_len} (watch rate: {watch_rate:.2} %) Drop Videos: {drop_videos_len} Dropped Videos: {dropped_videos_len} diff --git a/yt/src/storage/migrate/mod.rs b/crates/yt/src/storage/migrate/mod.rs index badeb6f..953d079 100644 --- a/yt/src/storage/migrate/mod.rs +++ b/crates/yt/src/storage/migrate/mod.rs @@ -21,6 +21,59 @@ use sqlx::{Sqlite, SqlitePool, Transaction, query}; use crate::app::App; +macro_rules! make_upgrade { + ($app:expr, $old_version:expr, $new_version:expr, $sql_name:expr) => { + add_error_context( + async { + let mut tx = $app + .database + .begin() + .await + .context("Failed to start the update transaction")?; + debug!("Migrating: {} -> {}", $old_version, $new_version); + + sqlx::raw_sql(include_str!($sql_name)) + .execute(&mut *tx) + .await + .context("Failed to run the update sql script")?; + + set_db_version( + &mut tx, + if $old_version == Self::Empty { + // There is no previous version we would need to remove + None + } else { + Some($old_version) + }, + $new_version, + ) + .await + .with_context(|| format!("Failed to set the new version ({})", $new_version))?; + + tx.commit() + .await + .context("Failed to commit the update transaction")?; + + // NOTE: This is needed, so that sqlite "sees" our changes to the table + // without having to reconnect. <2025-02-18> + query!("VACUUM") + .execute(&$app.database) + .await + .context("Failed to vacuum database")?; + + Ok(()) + }, + $new_version, + ) + .await?; + + Box::pin($new_version.update($app)).await.context(concat!( + "While updating to version: ", + stringify!($new_version) + )) + }; +} + #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub enum DbVersion { /// The database is not yet initialized. @@ -35,8 +88,11 @@ pub enum DbVersion { /// Introduced: 2025-02-18. Two, + + /// Introduced: 2025-03-21. + Three, } -const CURRENT_VERSION: DbVersion = DbVersion::Two; +const CURRENT_VERSION: DbVersion = DbVersion::Three; async fn add_error_context( function: impl Future<Output = Result<()>>, @@ -44,7 +100,7 @@ async fn add_error_context( ) -> Result<()> { function .await - .with_context(|| format!("Format failed to migrate database to version: {level}")) + .with_context(|| format!("Failed to migrate database to version: {level}")) } async fn set_db_version( @@ -83,21 +139,26 @@ async fn set_db_version( impl DbVersion { fn as_sql_integer(self) -> i32 { match self { - DbVersion::Empty => unreachable!("A empty version does not have an associated integer"), DbVersion::Zero => 0, DbVersion::One => 1, DbVersion::Two => 2, + DbVersion::Three => 3, + + DbVersion::Empty => unreachable!("A empty version does not have an associated integer"), } } + fn from_db(number: i64, namespace: &str) -> Result<Self> { match (number, namespace) { (0, "yt") => Ok(DbVersion::Zero), (1, "yt") => Ok(DbVersion::One), (2, "yt") => Ok(DbVersion::Two), + (3, "yt") => Ok(DbVersion::Three), (0, other) => bail!("Db version is Zero, but got unknown namespace: '{other}'"), (1, other) => bail!("Db version is One, but got unknown namespace: '{other}'"), (2, other) => bail!("Db version is Two, but got unknown namespace: '{other}'"), + (3, other) => bail!("Db version is Three, but got unknown namespace: '{other}'"), (other, "yt") => bail!("Got unkown version for 'yt' namespace: {other}"), (num, nasp) => bail!("Got unkown version number ({num}) and namespace ('{nasp}')"), @@ -111,126 +172,24 @@ impl DbVersion { #[allow(clippy::too_many_lines)] async fn update(self, app: &App) -> Result<()> { match self { - DbVersion::Empty => { - add_error_context( - async { - let mut tx = app - .database - .begin() - .await - .context("Failed to start transaction")?; - debug!("Migrate: Empty -> Zero"); - - sqlx::raw_sql(include_str!("./sql/00_empty_to_zero.sql")) - .execute(&mut *tx) - .await - .context("Failed to execute sql update script")?; - - set_db_version(&mut tx, None, DbVersion::Zero) - .await - .context("Failed to set new version")?; - - tx.commit() - .await - .context("Failed to commit changes")?; - - // NOTE: This is needed, so that sqlite "sees" our changes to the table - // without having to reconnect. <2025-02-18> - query!("VACUUM") - .execute(&app.database) - .await - .context("Failed to vacuum database")?; - - Ok(()) - }, - DbVersion::One, - ) - .await?; - Box::pin(Self::Zero.update(app)).await + Self::Empty => { + make_upgrade! {app, Self::Empty, Self::Zero, "./sql/0_Empty_to_Zero.sql"} } - DbVersion::Zero => { - add_error_context( - async { - let mut tx = app - .database - .begin() - .await - .context("Failed to start transaction")?; - debug!("Migrate: Zero -> One"); - - sqlx::raw_sql(include_str!("./sql/01_zero_to_one.sql")) - .execute(&mut *tx) - .await - .context("Failed to execute the update sql script")?; - - set_db_version(&mut tx, Some(DbVersion::Zero), DbVersion::One) - .await - .context("Failed to set the new version")?; - - tx.commit() - .await - .context("Failed to commit the update transaction")?; - - // NOTE: This is needed, so that sqlite "sees" our changes to the table - // without having to reconnect. <2025-02-18> - query!("VACUUM") - .execute(&app.database) - .await - .context("Failed to vacuum database")?; - - Ok(()) - }, - DbVersion::Zero, - ) - .await?; - - Box::pin(Self::One.update(app)).await + Self::Zero => { + make_upgrade! {app, Self::Zero, Self::One, "./sql/1_Zero_to_One.sql"} } - DbVersion::One => { - add_error_context( - async { - let mut tx = app - .database - .begin() - .await - .context("Failed to start the update transaction")?; - debug!("Migrate: One -> Two"); - - sqlx::raw_sql(include_str!("./sql/02_one_to_two.sql")) - .execute(&mut *tx) - .await - .context("Failed to run the update sql script")?; - - set_db_version(&mut tx, Some(DbVersion::One), DbVersion::Two) - .await - .context("Failed to set the new version")?; - - tx.commit() - .await - .context("Failed to commit the update transaction")?; - - // NOTE: This is needed, so that sqlite "sees" our changes to the table - // without having to reconnect. <2025-02-18> - query!("VACUUM") - .execute(&app.database) - .await - .context("Failed to vacuum database")?; - - Ok(()) - }, - DbVersion::One, - ) - .await?; + Self::One => { + make_upgrade! {app, Self::One, Self::Two, "./sql/2_One_to_Two.sql"} + } - Box::pin(Self::Two.update(app)) - .await - .context("Failed to update to version: Three") + Self::Two => { + make_upgrade! {app, Self::Two, Self::Three, "./sql/3_Two_to_Three.sql"} } // This is the current_version - DbVersion::Two => { + Self::Three => { assert_eq!(self, CURRENT_VERSION); assert_eq!(self, get_version(app).await?); Ok(()) diff --git a/yt/src/storage/migrate/sql/00_empty_to_zero.sql b/crates/yt/src/storage/migrate/sql/0_Empty_to_Zero.sql index d703bfc..d703bfc 100644 --- a/yt/src/storage/migrate/sql/00_empty_to_zero.sql +++ b/crates/yt/src/storage/migrate/sql/0_Empty_to_Zero.sql diff --git a/yt/src/storage/migrate/sql/01_zero_to_one.sql b/crates/yt/src/storage/migrate/sql/1_Zero_to_One.sql index da9315b..da9315b 100644 --- a/yt/src/storage/migrate/sql/01_zero_to_one.sql +++ b/crates/yt/src/storage/migrate/sql/1_Zero_to_One.sql diff --git a/yt/src/storage/migrate/sql/02_one_to_two.sql b/crates/yt/src/storage/migrate/sql/2_One_to_Two.sql index 806de07..806de07 100644 --- a/yt/src/storage/migrate/sql/02_one_to_two.sql +++ b/crates/yt/src/storage/migrate/sql/2_One_to_Two.sql diff --git a/crates/yt/src/storage/migrate/sql/3_Two_to_Three.sql b/crates/yt/src/storage/migrate/sql/3_Two_to_Three.sql new file mode 100644 index 0000000..b33f849 --- /dev/null +++ b/crates/yt/src/storage/migrate/sql/3_Two_to_Three.sql @@ -0,0 +1,85 @@ +-- yt - A fully featured command line YouTube client +-- +-- Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +-- SPDX-License-Identifier: GPL-3.0-or-later +-- +-- This file is part of Yt. +-- +-- You should have received a copy of the License along with this program. +-- If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + + +-- 1. Create new table +-- 2. Copy data +-- 3. Drop old table +-- 4. Rename new into old + +-- remove the original TRANSACTION +COMMIT TRANSACTION; + +-- tweak config +PRAGMA foreign_keys=OFF; + +-- start your own TRANSACTION +BEGIN TRANSACTION; + +CREATE TABLE videos_new ( + cache_path TEXT UNIQUE CHECK (CASE + WHEN cache_path IS NOT NULL THEN status == 2 + ELSE 1 + END), + description TEXT, + duration REAL, + extractor_hash TEXT UNIQUE NOT NULL PRIMARY KEY, + last_status_change INTEGER NOT NULL, + parent_subscription_name TEXT, + priority INTEGER NOT NULL DEFAULT 0, + publish_date INTEGER, + status INTEGER NOT NULL DEFAULT 0 CHECK (status IN (0, 1, 2, 3, 4, 5) AND + CASE + WHEN status == 2 THEN cache_path IS NOT NULL + WHEN status != 2 THEN cache_path IS NULL + ELSE 1 + END), + thumbnail_url TEXT, + title TEXT NOT NULL, + url TEXT UNIQUE NOT NULL, + is_focused INTEGER UNIQUE DEFAULT NULL CHECK (CASE + WHEN is_focused IS NOT NULL THEN is_focused == 1 + ELSE 1 + END), + watch_progress INTEGER NOT NULL DEFAULT 0 CHECK (watch_progress <= duration) +) STRICT; + +INSERT INTO videos_new SELECT + videos.cache_path, + videos.description, + videos.duration, + videos.extractor_hash, + videos.last_status_change, + videos.parent_subscription_name, + videos.priority, + videos.publish_date, + videos.status, + videos.thumbnail_url, + videos.title, + videos.url, + dummy.is_focused, + videos.watch_progress +FROM videos, (SELECT NULL AS is_focused) AS dummy; + +DROP TABLE videos; + +ALTER TABLE videos_new RENAME TO videos; + +-- check foreign key constraint still upholding. +PRAGMA foreign_key_check; + +-- commit your own TRANSACTION +COMMIT TRANSACTION; + +-- rollback all config you setup before. +PRAGMA foreign_keys=ON; + +-- start a new TRANSACTION to let migrator commit it. +BEGIN TRANSACTION; diff --git a/yt/src/storage/mod.rs b/crates/yt/src/storage/mod.rs index 8653eb3..d352b41 100644 --- a/yt/src/storage/mod.rs +++ b/crates/yt/src/storage/mod.rs @@ -9,6 +9,6 @@ // You should have received a copy of the License along with this program. // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +pub mod migrate; pub mod subscriptions; pub mod video_database; -pub mod migrate; diff --git a/yt/src/storage/subscriptions.rs b/crates/yt/src/storage/subscriptions.rs index 3673eee..6c0d08a 100644 --- a/yt/src/storage/subscriptions.rs +++ b/crates/yt/src/storage/subscriptions.rs @@ -15,10 +15,9 @@ use std::collections::HashMap; use anyhow::Result; use log::debug; -use serde_json::{Value, json}; use sqlx::query; use url::Url; -use yt_dlp::wrapper::info_json::InfoType; +use yt_dlp::YoutubeDLOptions; use crate::{app::App, unreachable::Unreachable}; @@ -39,21 +38,19 @@ impl Subscription { } /// Check whether an URL could be used as a subscription URL -pub async fn check_url(url: &Url) -> Result<bool> { - let Value::Object(yt_opts) = json!( { - "playliststart": 1, - "playlistend": 10, - "noplaylist": false, - "extract_flat": "in_playlist", - }) else { - unreachable!("This is hardcoded"); - }; - - let info = yt_dlp::extract_info(&yt_opts, url, false, false).await?; +pub async fn check_url(url: Url) -> Result<bool> { + let yt_dlp = YoutubeDLOptions::new() + .set("playliststart", 1) + .set("playlistend", 10) + .set("noplaylist", false) + .set("extract_flat", "in_playlist") + .build()?; + + let info = yt_dlp.extract_info(&url, false, false)?; debug!("{:#?}", info); - Ok(info._type == Some(InfoType::Playlist)) + Ok(info.get("_type") == Some(&serde_json::Value::String("Playlist".to_owned()))) } #[derive(Default, Debug)] diff --git a/yt/src/storage/video_database/downloader.rs b/crates/yt/src/storage/video_database/downloader.rs index a95081e..a95081e 100644 --- a/yt/src/storage/video_database/downloader.rs +++ b/crates/yt/src/storage/video_database/downloader.rs diff --git a/yt/src/storage/video_database/extractor_hash.rs b/crates/yt/src/storage/video_database/extractor_hash.rs index df545d7..df545d7 100644 --- a/yt/src/storage/video_database/extractor_hash.rs +++ b/crates/yt/src/storage/video_database/extractor_hash.rs diff --git a/yt/src/storage/video_database/get/mod.rs b/crates/yt/src/storage/video_database/get/mod.rs index 6a4220e..0456cd3 100644 --- a/yt/src/storage/video_database/get/mod.rs +++ b/crates/yt/src/storage/video_database/get/mod.rs @@ -18,7 +18,7 @@ use anyhow::{Context, Result, bail}; use blake3::Hash; use log::{debug, trace}; use sqlx::query; -use yt_dlp::wrapper::info_json::InfoJson; +use yt_dlp::InfoJson; use crate::{ app::App, @@ -64,7 +64,11 @@ macro_rules! video_from_record { let optional = if let Some(cache_path) = &$record.cache_path { Some(( PathBuf::from(cache_path), - if $record.is_focused == 1 { true } else { false }, + if $record.is_focused == Some(1) { + true + } else { + false + }, )) } else { None diff --git a/yt/src/storage/video_database/get/playlist/iterator.rs b/crates/yt/src/storage/video_database/get/playlist/iterator.rs index 4c45bf7..4c45bf7 100644 --- a/yt/src/storage/video_database/get/playlist/iterator.rs +++ b/crates/yt/src/storage/video_database/get/playlist/iterator.rs diff --git a/yt/src/storage/video_database/get/playlist/mod.rs b/crates/yt/src/storage/video_database/get/playlist/mod.rs index f6aadbf..f6aadbf 100644 --- a/yt/src/storage/video_database/get/playlist/mod.rs +++ b/crates/yt/src/storage/video_database/get/playlist/mod.rs diff --git a/yt/src/storage/video_database/mod.rs b/crates/yt/src/storage/video_database/mod.rs index 74d09f0..74d09f0 100644 --- a/yt/src/storage/video_database/mod.rs +++ b/crates/yt/src/storage/video_database/mod.rs diff --git a/yt/src/storage/video_database/notify.rs b/crates/yt/src/storage/video_database/notify.rs index b55c00a..b55c00a 100644 --- a/yt/src/storage/video_database/notify.rs +++ b/crates/yt/src/storage/video_database/notify.rs diff --git a/yt/src/storage/video_database/set/mod.rs b/crates/yt/src/storage/video_database/set/mod.rs index 4006fde..8c1be4a 100644 --- a/yt/src/storage/video_database/set/mod.rs +++ b/crates/yt/src/storage/video_database/set/mod.rs @@ -19,17 +19,17 @@ use log::{debug, info}; use sqlx::query; use tokio::fs; -use crate::{ - app::App, - storage::video_database::{VideoStatusMarker, extractor_hash::ExtractorHash}, - video_from_record, -}; +use crate::{app::App, storage::video_database::extractor_hash::ExtractorHash, video_from_record}; use super::{Priority, Video, VideoOptions, VideoStatus}; mod playlist; pub use playlist::*; +const fn is_focused_to_value(is_focused: bool) -> Option<i8> { + if is_focused { Some(1) } else { None } +} + /// Set a new status for a video. /// This will only update the status time stamp/priority when the status or the priority has changed . pub async fn video_status( @@ -56,7 +56,7 @@ pub async fn video_status( }; let old_marker = old.status.as_marker(); - let cache_path = { + let (cache_path, is_focused) = { fn cache_path_to_string(path: &Path) -> Result<String> { Ok(path .to_str() @@ -69,13 +69,17 @@ pub async fn video_status( .to_owned()) } - match (old_marker, &new_status) { - (VideoStatusMarker::Cached, VideoStatus::Cached { cache_path, .. }) => { - Some(cache_path_to_string(cache_path)?) - } - (_, VideoStatus::Cached { cache_path, .. }) => Some(cache_path_to_string(cache_path)?), - - (VideoStatusMarker::Cached | _, _) => None, + if let VideoStatus::Cached { + cache_path, + is_focused, + } = &new_status + { + ( + Some(cache_path_to_string(cache_path)?), + is_focused_to_value(*is_focused), + ) + } else { + (None, None) } }; @@ -98,13 +102,14 @@ pub async fn video_status( query!( r#" UPDATE videos - SET status = ?, last_status_change = ?, priority = ?, cache_path = ? + SET status = ?, last_status_change = ?, priority = ?, cache_path = ?, is_focused = ? WHERE extractor_hash = ?; "#, new_status, now, new_priority, cache_path, + is_focused, video_hash ) .execute(&app.database) @@ -125,12 +130,13 @@ pub async fn video_status( query!( r#" UPDATE videos - SET status = ?, last_status_change = ?, cache_path = ? + SET status = ?, last_status_change = ?, cache_path = ?, is_focused = ? WHERE extractor_hash = ?; "#, new_status, now, cache_path, + is_focused, video_hash ) .execute(&app.database) @@ -147,10 +153,9 @@ pub async fn video_status( /// # Panics /// Only if assertions fail. pub async fn video_watched(app: &App, video: &ExtractorHash) -> Result<()> { - let video_hash = video.hash().to_string(); - let new_status = VideoStatusMarker::Watched.as_db_integer(); - let old = { + let video_hash = video.hash().to_string(); + let base = query!( r#" SELECT * @@ -175,20 +180,7 @@ pub async fn video_watched(app: &App, video: &ExtractorHash) -> Result<()> { unreachable!("The video must be marked as Cached before it can be marked Watched"); } - let now = Utc::now().timestamp(); - - query!( - r#" - UPDATE videos - SET status = ?, last_status_change = ?, cache_path = NULL - WHERE extractor_hash = ?; - "#, - new_status, - now, - video_hash - ) - .execute(&app.database) - .await?; + video_status(app, video, VideoStatus::Watched, None).await?; Ok(()) } @@ -271,10 +263,10 @@ pub async fn add_video(app: &App, video: Video) -> Result<()> { })? .to_string(), ), - is_focused, + is_focused_to_value(is_focused), ) } else { - (None, false) + (None, None) }; let duration: Option<f64> = video.duration.as_secs_f64(); diff --git a/yt/src/storage/video_database/set/playlist.rs b/crates/yt/src/storage/video_database/set/playlist.rs index 7e97239..547df21 100644 --- a/yt/src/storage/video_database/set/playlist.rs +++ b/crates/yt/src/storage/video_database/set/playlist.rs @@ -28,12 +28,9 @@ pub async fn focused( new_video_hash: &ExtractorHash, old_video_hash: Option<&ExtractorHash>, ) -> Result<()> { - if let Some(old) = old_video_hash { - debug!("Unfocusing video: '{old}'"); - unfocused(app, old).await?; - } - debug!("Focusing video: '{new_video_hash}'"); + unfocused(app, old_video_hash).await?; + debug!("Focusing video: '{new_video_hash}'"); let new_hash = new_video_hash.hash().to_string(); query!( r#" @@ -57,15 +54,38 @@ pub async fn focused( } /// Set a video to be no longer focused. +/// This will use the supplied `video_hash` if it is [`Some`], otherwise it will simply un-focus +/// the currently focused video. /// /// # Panics /// Only if internal assertions fail. -pub async fn unfocused(app: &App, video_hash: &ExtractorHash) -> Result<()> { - let hash = video_hash.hash().to_string(); +pub async fn unfocused(app: &App, video_hash: Option<&ExtractorHash>) -> Result<()> { + let hash = if let Some(hash) = video_hash { + hash.hash().to_string() + } else { + let output = query!( + r#" + SELECT extractor_hash + FROM videos + WHERE is_focused = 1; + "#, + ) + .fetch_optional(&app.database) + .await?; + + if let Some(output) = output { + output.extractor_hash + } else { + // There is no unfocused video right now. + return Ok(()); + } + }; + debug!("Unfocusing video: '{hash}'"); + query!( r#" UPDATE videos - SET is_focused = 0 + SET is_focused = NULL WHERE extractor_hash = ?; "#, hash diff --git a/yt/src/subscribe/mod.rs b/crates/yt/src/subscribe/mod.rs index 455ccb1..7ac0be4 100644 --- a/yt/src/subscribe/mod.rs +++ b/crates/yt/src/subscribe/mod.rs @@ -14,10 +14,9 @@ use std::str::FromStr; use anyhow::{Context, Result, bail}; use futures::FutureExt; use log::warn; -use serde_json::{Value, json}; use tokio::io::{AsyncBufRead, AsyncBufReadExt}; use url::Url; -use yt_dlp::wrapper::info_json::InfoType; +use yt_dlp::{YoutubeDLOptions, json_get}; use crate::{ app::App, @@ -142,26 +141,24 @@ pub async fn subscribe(app: &App, name: Option<String>, url: Url) -> Result<()> } async fn actual_subscribe(app: &App, name: Option<String>, url: Url) -> Result<()> { - if !check_url(&url).await? { + if !check_url(url.clone()).await? { bail!("The url ('{}') does not represent a playlist!", &url) - }; + } let name = if let Some(name) = name { name } else { - let Value::Object(yt_opts) = json!( { - "playliststart": 1, - "playlistend": 10, - "noplaylist": false, - "extract_flat": "in_playlist", - }) else { - unreachable!("This is hardcoded") - }; - - let info = yt_dlp::extract_info(&yt_opts, &url, false, false).await?; - - if info._type == Some(InfoType::Playlist) { - info.title.expect("This should be some for a playlist") + let yt_dlp = YoutubeDLOptions::new() + .set("playliststart", 1) + .set("playlistend", 10) + .set("noplaylist", false) + .set("extract_flat", "in_playlist") + .build()?; + + let info = yt_dlp.extract_info(&url, false, false)?; + + if info.get("_type") == Some(&serde_json::Value::String("Playlist".to_owned())) { + json_get!(info, "title", as_str).to_owned() } else { bail!("The url ('{}') does not represent a playlist!", &url) } diff --git a/yt/src/unreachable.rs b/crates/yt/src/unreachable.rs index 436fbb6..436fbb6 100644 --- a/yt/src/unreachable.rs +++ b/crates/yt/src/unreachable.rs diff --git a/yt/src/update/mod.rs b/crates/yt/src/update/mod.rs index 7efe0da..f0b1e2c 100644 --- a/yt/src/update/mod.rs +++ b/crates/yt/src/update/mod.rs @@ -15,7 +15,7 @@ use anyhow::{Context, Ok, Result}; use chrono::{DateTime, Utc}; use log::{info, warn}; use url::Url; -use yt_dlp::{unsmuggle_url, wrapper::info_json::InfoJson}; +use yt_dlp::{InfoJson, json_cast, json_get}; use crate::{ app::App, @@ -72,19 +72,7 @@ pub async fn update( } #[allow(clippy::too_many_lines)] -pub fn video_entry_to_video(entry: InfoJson, sub: Option<&Subscription>) -> Result<Video> { - macro_rules! unwrap_option { - ($option:expr) => { - match $option { - Some(x) => x, - None => anyhow::bail!(concat!( - "Expected a value, but '", - stringify!($option), - "' is None!" - )), - } - }; - } +pub fn video_entry_to_video(entry: &InfoJson, sub: Option<&Subscription>) -> Result<Video> { fn fmt_context(date: &str, extended: Option<&str>) -> String { let f = format!( "Failed to parse the `upload_date` of the entry ('{date}'). \ @@ -97,7 +85,9 @@ pub fn video_entry_to_video(entry: InfoJson, sub: Option<&Subscription>) -> Resu } } - let publish_date = if let Some(date) = &entry.upload_date { + let publish_date = if let Some(date) = &entry.get("upload_date") { + let date = json_cast!(date, as_str); + let year: u32 = date .chars() .take(4) @@ -113,7 +103,7 @@ pub fn video_entry_to_video(entry: InfoJson, sub: Option<&Subscription>) -> Resu .with_context(|| fmt_context(date, None))?; let day: u32 = date .chars() - .skip(6) + .skip(4 + 2) .take(2) .collect::<String>() .parse() @@ -128,42 +118,59 @@ pub fn video_entry_to_video(entry: InfoJson, sub: Option<&Subscription>) -> Resu } else { warn!( "The video '{}' lacks it's upload date!", - unwrap_option!(&entry.title) + json_get!(entry, "title", as_str) ); None }; - let thumbnail_url = match (&entry.thumbnails, &entry.thumbnail) { + let thumbnail_url = match (&entry.get("thumbnails"), &entry.get("thumbnail")) { (None, None) => None, - (None, Some(thumbnail)) => Some(thumbnail.to_owned()), + (None, Some(thumbnail)) => Some(Url::from_str(json_cast!(thumbnail, as_str))?), // TODO: The algorithm is not exactly the best <2024-05-28> - (Some(thumbnails), None) => thumbnails.first().map(|thumbnail| thumbnail.url.clone()), - (Some(_), Some(thumnail)) => Some(thumnail.to_owned()), + (Some(thumbnails), None) => { + if let Some(thumbnail) = json_cast!(thumbnails, as_array).first() { + Some(Url::from_str(json_get!( + json_cast!(thumbnail, as_object), + "url", + as_str + ))?) + } else { + None + } + } + (Some(_), Some(thumnail)) => Some(Url::from_str(json_cast!(thumnail, as_str))?), }; let url = { - let smug_url: Url = unwrap_option!(entry.webpage_url.clone()); - unsmuggle_url(&smug_url)? + let smug_url: Url = json_get!(entry, "webpage_url", as_str).parse()?; + // unsmuggle_url(&smug_url)? + smug_url }; - let extractor_hash = blake3::hash(unwrap_option!(entry.id).as_bytes()); + let extractor_hash = blake3::hash(json_get!(entry, "id", as_str).as_bytes()); let subscription_name = if let Some(sub) = sub { Some(sub.name.clone()) - } else if let Some(uploader) = entry.uploader { - if entry.webpage_url_domain == Some("youtube.com".to_owned()) { + } else if let Some(uploader) = entry.get("uploader") { + if entry.get("webpage_url_domain") + == Some(&serde_json::Value::String("youtube.com".to_owned())) + { Some(format!("{uploader} - Videos")) } else { - Some(uploader.clone()) + Some(json_cast!(uploader, as_str).to_owned()) } } else { None }; let video = Video { - description: entry.description.clone(), - duration: MaybeDuration::from_maybe_secs_f64(entry.duration), + description: entry + .get("description") + .map(|val| json_cast!(val, as_str).to_owned()), + duration: MaybeDuration::from_maybe_secs_f64( + entry.get("duration").map(|val| json_cast!(val, as_f64)), + ), extractor_hash: ExtractorHash::from_hash(extractor_hash), last_status_change: TimeStamp::from_now(), parent_subscription_name: subscription_name, @@ -171,7 +178,7 @@ pub fn video_entry_to_video(entry: InfoJson, sub: Option<&Subscription>) -> Resu publish_date: publish_date.map(TimeStamp::from_secs), status: VideoStatus::Pick, thumbnail_url, - title: unwrap_option!(entry.title.clone()), + title: json_get!(entry, "title", as_str).to_owned(), url, watch_progress: Duration::default(), }; @@ -180,7 +187,7 @@ pub fn video_entry_to_video(entry: InfoJson, sub: Option<&Subscription>) -> Resu async fn process_subscription(app: &App, sub: &Subscription, entry: InfoJson) -> Result<()> { let video = - video_entry_to_video(entry, Some(sub)).context("Failed to parse search entry as Video")?; + video_entry_to_video(&entry, Some(sub)).context("Failed to parse search entry as Video")?; add_video(app, video.clone()) .await diff --git a/crates/yt/src/update/updater.rs b/crates/yt/src/update/updater.rs new file mode 100644 index 0000000..8da654b --- /dev/null +++ b/crates/yt/src/update/updater.rs @@ -0,0 +1,167 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::io::{Write, stderr}; + +use anyhow::{Context, Result}; +use blake3::Hash; +use futures::{ + StreamExt, TryStreamExt, + stream::{self}, +}; +use log::{Level, debug, error, log_enabled}; +use serde_json::json; +use yt_dlp::{InfoJson, YoutubeDLOptions, json_cast, json_get}; + +use crate::{ + ansi_escape_codes::{clear_whole_line, move_to_col}, + app::App, + storage::subscriptions::Subscription, +}; + +use super::process_subscription; + +pub(super) struct Updater<'a> { + max_backlog: usize, + hashes: &'a [Hash], +} + +impl<'a> Updater<'a> { + pub(super) fn new(max_backlog: usize, hashes: &'a [Hash]) -> Self { + Self { + max_backlog, + hashes, + } + } + + pub(super) async fn update( + &mut self, + app: &App, + subscriptions: &[&Subscription], + ) -> Result<()> { + let mut stream = stream::iter(subscriptions) + .map(|sub| self.get_new_entries(sub)) + .buffer_unordered(100); + + while let Some(output) = stream.next().await { + let mut entries = output?; + + if entries.is_empty() { + continue; + } + + let (sub, entry) = entries.remove(0); + process_subscription(app, sub, entry).await?; + + let entry_stream: Result<()> = stream::iter(entries) + .map(|(sub, entry)| process_subscription(app, sub, entry)) + .buffer_unordered(100) + .try_collect() + .await; + entry_stream?; + } + + Ok(()) + } + + async fn get_new_entries( + &self, + sub: &'a Subscription, + ) -> Result<Vec<(&'a Subscription, InfoJson)>> { + let yt_dlp = YoutubeDLOptions::new() + .set("playliststart", 1) + .set("playlistend", self.max_backlog) + .set("noplaylist", false) + .set( + "extractor_args", + json! {{"youtubetab": {"approximate_date": [""]}}}, + ) + // TODO: This also removes unlisted and other stuff. Find a good way to remove the + // members-only videos from the feed. <2025-04-17> + .set("match-filter", "availability=public") + .build()?; + + if !log_enabled!(Level::Debug) { + clear_whole_line(); + move_to_col(1); + eprint!("Checking playlist {}...", sub.name); + move_to_col(1); + stderr().flush()?; + } + + let info = yt_dlp + .extract_info(&sub.url, false, false) + .with_context(|| format!("Failed to get playlist '{}'.", sub.name))?; + + let empty = vec![]; + let entries = info + .get("entries") + .map_or(&empty, |val| json_cast!(val, as_array)); + + let valid_entries: Vec<(&Subscription, InfoJson)> = entries + .iter() + .take(self.max_backlog) + .filter_map(|entry| -> Option<(&Subscription, InfoJson)> { + let id = json_get!(entry, "id", as_str); + let extractor_hash = blake3::hash(id.as_bytes()); + if self.hashes.contains(&extractor_hash) { + debug!("Skipping entry, as it is already present: '{extractor_hash}'",); + None + } else { + Some((sub, json_cast!(entry, as_object).to_owned())) + } + }) + .collect(); + + let processed_entries: Vec<(&Subscription, InfoJson)> = stream::iter(valid_entries) + .map( + async |(sub, entry)| match yt_dlp.process_ie_result(entry, false) { + Ok(output) => Ok((sub, output)), + Err(err) => Err(err), + }, + ) + .buffer_unordered(100) + .collect::<Vec<_>>() + .await + .into_iter() + // Don't fail the whole update, if one of the entries fails to fetch. + .filter_map(|base| match base { + Ok(ok) => Some(ok), + Err(err) => { + // TODO(@bpeetz): Add this <2025-06-13> + // if let YtDlpError::PythonError { error, kind } = &err { + // if kind.as_str() == "<class 'yt_dlp.utils.DownloadError'>" + // && error.to_string().as_str().contains( + // "Join this channel to get access to members-only content ", + // ) + // { + // // Hide this error + // } else { + // let error_string = error.to_string(); + // let error = error_string + // .strip_prefix("DownloadError: \u{1b}[0;31mERROR:\u{1b}[0m ") + // .expect("This prefix should exists"); + // error!("{error}"); + // } + // return None; + // } + + // TODO(@bpeetz): Ideally, we _would_ actually exit on unexpected errors, but + // this is fine for now. <2025-06-13> + // Some(Err(err).context("Failed to process new entries.")) + error!("While processing entry: {err}"); + None + } + }) + .collect(); + + Ok(processed_entries) + } +} diff --git a/yt/src/version/mod.rs b/crates/yt/src/version/mod.rs index 05d85e0..05d85e0 100644 --- a/yt/src/version/mod.rs +++ b/crates/yt/src/version/mod.rs diff --git a/yt/src/videos/display/format_video.rs b/crates/yt/src/videos/display/format_video.rs index b97acb1..b97acb1 100644 --- a/yt/src/videos/display/format_video.rs +++ b/crates/yt/src/videos/display/format_video.rs diff --git a/yt/src/videos/display/mod.rs b/crates/yt/src/videos/display/mod.rs index 1188569..1188569 100644 --- a/yt/src/videos/display/mod.rs +++ b/crates/yt/src/videos/display/mod.rs diff --git a/yt/src/videos/mod.rs b/crates/yt/src/videos/mod.rs index e821772..e821772 100644 --- a/yt/src/videos/mod.rs +++ b/crates/yt/src/videos/mod.rs diff --git a/yt/src/watch/mod.rs b/crates/yt/src/watch/mod.rs index 6827b2c..c32a76f 100644 --- a/yt/src/watch/mod.rs +++ b/crates/yt/src/watch/mod.rs @@ -58,9 +58,12 @@ fn init_mpv(app: &App) -> Result<(Mpv, EventContext)> { let config_path = &app.config.paths.mpv_config_path; if config_path.try_exists()? { info!("Found mpv.conf at '{}'!", config_path.display()); - mpv.command("load-config-file", &[config_path - .to_str() - .context("Failed to parse the config path is utf8-stringt")?])?; + mpv.command( + "load-config-file", + &[config_path + .to_str() + .context("Failed to parse the config path is utf8-stringt")?], + )?; } else { warn!( "Did not find a mpv.conf file at '{}'", @@ -71,9 +74,12 @@ fn init_mpv(app: &App) -> Result<(Mpv, EventContext)> { let input_path = &app.config.paths.mpv_input_path; if input_path.try_exists()? { info!("Found mpv.input.conf at '{}'!", input_path.display()); - mpv.command("load-input-conf", &[input_path - .to_str() - .context("Failed to parse the input path as utf8 string")?])?; + mpv.command( + "load-input-conf", + &[input_path + .to_str() + .context("Failed to parse the input path as utf8 string")?], + )?; } else { warn!( "Did not find a mpv.input.conf file at '{}'", diff --git a/yt/src/watch/playlist.rs b/crates/yt/src/watch/playlist.rs index 6ac8b12..ff383d0 100644 --- a/yt/src/watch/playlist.rs +++ b/crates/yt/src/watch/playlist.rs @@ -11,6 +11,7 @@ use std::path::Path; use crate::{ + ansi_escape_codes::{cursor_up, erase_in_display_from_cursor}, app::App, storage::video_database::{Video, VideoStatus, get, notify::wait_for_db_write}, }; @@ -31,17 +32,6 @@ fn cache_values(video: &Video) -> (&Path, bool) { } } -// ANSI ESCAPE CODES Wrappers {{{ -// see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands -const CSI: &str = "\x1b["; -fn erase_in_display_from_cursor() { - print!("{CSI}0J"); -} -fn cursor_up(number: usize) { - print!("{CSI}{number}A"); -} -// }}} - /// # Panics /// Only if internal assertions fail. pub async fn playlist(app: &App, watch: bool) -> Result<()> { diff --git a/yt/src/watch/playlist_handler/client_messages/mod.rs b/crates/yt/src/watch/playlist_handler/client_messages/mod.rs index 6f7a59e..6f7a59e 100644 --- a/yt/src/watch/playlist_handler/client_messages/mod.rs +++ b/crates/yt/src/watch/playlist_handler/client_messages/mod.rs diff --git a/yt/src/watch/playlist_handler/mod.rs b/crates/yt/src/watch/playlist_handler/mod.rs index 2672ff5..29b8f39 100644 --- a/yt/src/watch/playlist_handler/mod.rs +++ b/crates/yt/src/watch/playlist_handler/mod.rs @@ -41,10 +41,10 @@ pub enum Status { } fn mpv_message(mpv: &Mpv, message: &str, time: Duration) -> Result<()> { - mpv.command("show-text", &[ - message, - time.as_millis().to_string().as_str(), - ])?; + mpv.command( + "show-text", + &[message, time.as_millis().to_string().as_str()], + )?; Ok(()) } @@ -139,15 +139,18 @@ pub(super) async fn reload_mpv_playlist( debug!("Will add {} videos to playlist.", playlist.len()); playlist.into_iter().try_for_each(|cache_path| { - mpv.command("loadfile", &[ - cache_path.to_str().with_context(|| { - format!( - "Failed to parse the video cache path ('{}') as valid utf8", - cache_path.display() - ) - })?, - "append-play", - ])?; + mpv.command( + "loadfile", + &[ + cache_path.to_str().with_context(|| { + format!( + "Failed to parse the video cache path ('{}') as valid utf8", + cache_path.display() + ) + })?, + "append-play", + ], + )?; Ok::<(), anyhow::Error>(()) })?; diff --git a/crates/yt_dlp/.cargo/config.toml b/crates/yt_dlp/.cargo/config.toml deleted file mode 100644 index d84f14d..0000000 --- a/crates/yt_dlp/.cargo/config.toml +++ /dev/null @@ -1,12 +0,0 @@ -# yt - A fully featured command line YouTube client -# -# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This file is part of Yt. -# -# You should have received a copy of the License along with this program. -# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -[env] -PYO3_PYTHON = "/nix/store/7xzk119acyws2c4ysygdv66l0grxkr39-python3-3.11.9-env/bin/python3" diff --git a/crates/yt_dlp/Cargo.toml b/crates/yt_dlp/Cargo.toml index a948a34..ddd5f9b 100644 --- a/crates/yt_dlp/Cargo.toml +++ b/crates/yt_dlp/Cargo.toml @@ -10,7 +10,7 @@ [package] name = "yt_dlp" -description = "A wrapper around the python yt_dlp library" +description = "A rust fii wrapper library for the python yt_dlp library" keywords = [] categories = [] version.workspace = true @@ -19,19 +19,16 @@ authors.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true -publish = false +publish = true [dependencies] -pyo3 = { version = "0.23.4", features = ["auto-initialize"] } -bytes.workspace = true +indexmap = { version = "2.9.0", default-features = false } log.workspace = true -serde.workspace = true +rustpython = { git = "https://github.com/RustPython/RustPython.git", features = ["threading", "stdlib", "stdio", "importlib", "ssl"], default-features = false } serde_json.workspace = true +thiserror = "2.0.12" url.workspace = true -[dev-dependencies] -tokio.workspace = true - [lints] workspace = true diff --git a/crates/yt_dlp/src/duration.rs b/crates/yt_dlp/src/duration.rs deleted file mode 100644 index 19181a5..0000000 --- a/crates/yt_dlp/src/duration.rs +++ /dev/null @@ -1,78 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -// TODO: This file should be de-duplicated with the same file in the 'yt' crate <2024-06-25> - -#[derive(Debug, Clone, Copy)] -pub struct Duration { - time: u32, -} - -impl From<&str> for Duration { - fn from(v: &str) -> Self { - let buf: Vec<_> = v.split(':').take(2).collect(); - Self { - time: (buf[0].parse::<u32>().expect("Should be a number") * 60) - + buf[1].parse::<u32>().expect("Should be a number"), - } - } -} - -impl From<Option<f64>> for Duration { - fn from(value: Option<f64>) -> Self { - Self { - #[allow( - clippy::cast_possible_truncation, - clippy::cast_precision_loss, - clippy::cast_sign_loss - )] - time: value.unwrap_or(0.0).ceil() as u32, - } - } -} - -impl std::fmt::Display for Duration { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - const SECOND: u32 = 1; - const MINUTE: u32 = 60 * SECOND; - const HOUR: u32 = 60 * MINUTE; - - let base_hour = self.time - (self.time % HOUR); - let base_min = (self.time % HOUR) - ((self.time % HOUR) % MINUTE); - let base_sec = (self.time % HOUR) % MINUTE; - - let h = base_hour / HOUR; - let m = base_min / MINUTE; - let s = base_sec / SECOND; - - if self.time == 0 { - write!(f, "0s") - } else if h > 0 { - write!(f, "{h}h {m}m") - } else { - write!(f, "{m}m {s}s") - } - } -} -#[cfg(test)] -mod test { - use super::Duration; - - #[test] - fn test_display_duration_1h() { - let dur = Duration { time: 60 * 60 }; - assert_eq!("1h 0m".to_owned(), dur.to_string()); - } - #[test] - fn test_display_duration_30min() { - let dur = Duration { time: 60 * 30 }; - assert_eq!("30m 0s".to_owned(), dur.to_string()); - } -} diff --git a/crates/yt_dlp/src/error.rs b/crates/yt_dlp/src/error.rs deleted file mode 100644 index 3881f0b..0000000 --- a/crates/yt_dlp/src/error.rs +++ /dev/null @@ -1,68 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -use std::{fmt::Display, io}; - -use pyo3::Python; - -#[derive(Debug)] -#[allow(clippy::module_name_repetitions)] -pub enum YtDlpError { - ResponseParseError { - error: serde_json::error::Error, - }, - PythonError { - error: Box<pyo3::PyErr>, - kind: String, - }, - IoError { - error: io::Error, - }, -} - -impl std::error::Error for YtDlpError {} - -impl Display for YtDlpError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - YtDlpError::ResponseParseError { error } => write!( - f, - include_str!("./python_json_decode_failed.error_msg"), - error - ), - YtDlpError::PythonError { error, kind: _ } => write!(f, "Python error: {error}"), - YtDlpError::IoError { error } => write!(f, "Io error: {error}"), - } - } -} - -impl From<serde_json::error::Error> for YtDlpError { - fn from(value: serde_json::error::Error) -> Self { - Self::ResponseParseError { error: value } - } -} - -impl From<pyo3::PyErr> for YtDlpError { - fn from(value: pyo3::PyErr) -> Self { - Python::with_gil(|py| { - let kind = value.get_type(py).to_string(); - Self::PythonError { - error: Box::new(value), - kind, - } - }) - } -} - -impl From<io::Error> for YtDlpError { - fn from(value: io::Error) -> Self { - Self::IoError { error: value } - } -} diff --git a/crates/yt_dlp/src/lib.rs b/crates/yt_dlp/src/lib.rs index 40610c2..34b8a5d 100644 --- a/crates/yt_dlp/src/lib.rs +++ b/crates/yt_dlp/src/lib.rs @@ -1,551 +1,541 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -// The pyo3 `pyfunction` proc-macros call unsafe functions internally, which trigger this lint. -#![allow(unsafe_op_in_unsafe_fn)] -#![allow(clippy::missing_errors_doc)] - -use std::io::stderr; -use std::{env, process}; -use std::{fs::File, io::Write}; - -use std::{path::PathBuf, sync::Once}; - -use crate::{duration::Duration, logging::setup_logging, wrapper::info_json::InfoJson}; - -use bytes::Bytes; -use error::YtDlpError; -use log::{Level, debug, info, log_enabled}; -use pyo3::types::{PyString, PyTuple, PyTupleMethods}; -use pyo3::{ - Bound, PyAny, PyResult, Python, pyfunction, - types::{PyAnyMethods, PyDict, PyDictMethods, PyList, PyListMethods, PyModule}, - wrap_pyfunction, +//! The `yt_dlp` interface is completely contained in the [`YoutubeDL`] structure. + +use std::io::Write; +use std::mem; +use std::{env, fs::File, path::PathBuf}; + +use indexmap::IndexMap; +use log::{Level, debug, error, info, log_enabled}; +use logging::setup_logging; +use rustpython::vm::builtins::PyList; +use rustpython::{ + InterpreterConfig, + vm::{ + self, Interpreter, PyObjectRef, PyRef, VirtualMachine, + builtins::{PyBaseException, PyDict, PyStr}, + function::{FuncArgs, KwArgs, PosArgs}, + }, }; -use serde::Serialize; -use serde_json::{Map, Value}; use url::Url; -pub mod duration; -pub mod error; -pub mod logging; -pub mod wrapper; +mod logging; +pub mod progress_hook; -#[cfg(test)] -mod tests; - -/// Synchronisation helper, to ensure that we don't setup the logger multiple times -static SYNC_OBJ: Once = Once::new(); +#[macro_export] +macro_rules! json_get { + ($value:expr, $name:literal, $into:ident) => { + $crate::json_cast!($value.get($name).expect("Should exist"), $into) + }; +} -/// Add a logger to the yt-dlp options. -/// If you have an logger set (i.e. for rust), than this will log to rust -/// -/// # Panics -/// This should never panic. -pub fn add_logger_and_sig_handler<'a>( - opts: Bound<'a, PyDict>, - py: Python<'_>, -) -> PyResult<Bound<'a, PyDict>> { - /// Is the specified record to be logged? Returns false for no, - /// true for yes. Filters can either modify log records in-place or - /// return a completely different record instance which will replace - /// the original log record in any future processing of the event. - #[pyfunction] - fn filter_error_log(_py: Python<'_>, record: &Bound<'_, PyAny>) -> bool { - // Filter out all error logs (they are propagated as rust errors) - let levelname: String = record - .getattr("levelname") - .expect("This should exist") - .extract() - .expect("This should be a String"); - - let return_value = levelname.as_str() != "ERROR"; - - if log_enabled!(Level::Debug) && !return_value { - let message: String = record - .call_method0("getMessage") - .expect("This method exists") - .extract() - .expect("The message is a string"); - - debug!("Swollowed error message: '{message}'"); - } - return_value - } +#[macro_export] +macro_rules! json_cast { + ($value:expr, $into:ident) => { + $value.$into().expect(concat!( + "Should be able to cast value into ", + stringify!($into) + )) + }; +} - setup_logging(py, "yt_dlp")?; - - let logging = PyModule::import(py, "logging")?; - let ytdl_logger = logging.call_method1("getLogger", ("yt_dlp",))?; - - // Ensure that all events are logged by setting the log level to NOTSET (we filter on rust's side) - // Also use this static, to ensure that we don't configure the logger every time - SYNC_OBJ.call_once(|| { - // Disable the SIGINT (Ctrl+C) handler, python installs. - // This allows the user to actually stop the application with Ctrl+C. - // This is here because it can only be run in the main thread and this was here already. - py.run( - c"\ -import signal -signal.signal(signal.SIGINT, signal.SIG_DFL)", - None, - None, - ) - .expect("This code should always work"); - - let config_opts = PyDict::new(py); - config_opts - .set_item("level", 0) - .expect("Setting this item should always work"); - - logging - .call_method("basicConfig", (), Some(&config_opts)) - .expect("This method exists"); - }); - - ytdl_logger.call_method1( - "addFilter", - (wrap_pyfunction!(filter_error_log, py).expect("This function can be wrapped"),), - )?; - - // This was taken from `ytcc`, I don't think it is still applicable - // ytdl_logger.setattr("propagate", false)?; - // let logging_null_handler = logging.call_method0("NullHandler")?; - // ytdl_logger.setattr("addHandler", logging_null_handler)?; - - opts.set_item("logger", ytdl_logger).expect("Should work"); - - Ok(opts) +/// The core of the `yt_dlp` interface. +pub struct YoutubeDL { + interpreter: Interpreter, + youtube_dl_class: PyObjectRef, + yt_dlp_module: PyObjectRef, + options: serde_json::Map<String, serde_json::Value>, } -#[pyfunction] -#[allow(clippy::too_many_lines)] -#[allow(clippy::missing_panics_doc)] -#[allow(clippy::items_after_statements)] -#[allow( - clippy::cast_possible_truncation, - clippy::cast_sign_loss, - clippy::cast_precision_loss -)] -pub fn progress_hook(py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<()> { - // Only add the handler, if the log-level is higher than Debug (this avoids covering debug - // messages). - if log_enabled!(Level::Debug) { - return Ok(()); +impl std::fmt::Debug for YoutubeDL { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // TODO(@bpeetz): Use something useful here. <2025-06-13> + f.write_str("YoutubeDL") } +} - // ANSI ESCAPE CODES Wrappers {{{ - // see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands - const CSI: &str = "\x1b["; - fn clear_whole_line() { - eprint!("{CSI}2K"); - } - fn move_to_col(x: usize) { - eprint!("{CSI}{x}G"); - } - // }}} - - let input: Map<String, Value> = serde_json::from_str(&json_dumps( - py, - input - .downcast::<PyAny>() - .expect("Will always work") - .to_owned(), - )?) - .expect("python's json is valid"); - - macro_rules! get { - (@interrogate $item:ident, $type_fun:ident, $get_fun:ident, $name:expr) => {{ - let a = $item.get($name).expect(concat!( - "The field '", - stringify!($name), - "' should exist." - )); - - if a.$type_fun() { - a.$get_fun().expect( - "The should have been checked in the if guard, so unpacking here is fine", - ) - } else { - panic!( - "Value {} => \n{}\n is not of type: {}", - $name, - a, - stringify!($type_fun) - ); +impl YoutubeDL { + /// Construct this instance from options. + /// + /// # Panics + /// If `yt_dlp` changed their interface. + /// + /// # Errors + /// If a python call fails. + pub fn from_options(mut options: YoutubeDLOptions) -> Result<Self, build::Error> { + let mut settings = vm::Settings::default(); + if let Ok(python_path) = env::var("PYTHONPATH") { + for path in python_path.split(':') { + settings.path_list.push(path.to_owned()); } - }}; + } else { + error!( + "No PYTHONPATH found or invalid utf8. \ + This means, that you probably did not \ + supply the yt_dlp!" + ); + } - ($type_fun:ident, $get_fun:ident, $name1:expr, $name2:expr) => {{ - let a = get! {@interrogate input, is_object, as_object, $name1}; - let b = get! {@interrogate a, $type_fun, $get_fun, $name2}; - b - }}; + settings.install_signal_handlers = false; - ($type_fun:ident, $get_fun:ident, $name:expr) => {{ - get! {@interrogate input, $type_fun, $get_fun, $name} - }}; - } + // NOTE(@bpeetz): Another value leads to an internal codegen error. <2025-06-13> + settings.optimize = 0; - macro_rules! default_get { - (@interrogate $item:ident, $default:expr, $get_fun:ident, $name:expr) => {{ - let a = if let Some(field) = $item.get($name) { - field.$get_fun().unwrap_or($default) - } else { - $default - }; - a - }}; - - ($get_fun:ident, $default:expr, $name1:expr, $name2:expr) => {{ - let a = get! {@interrogate input, is_object, as_object, $name1}; - let b = default_get! {@interrogate a, $default, $get_fun, $name2}; - b - }}; - - ($get_fun:ident, $default:expr, $name:expr) => {{ - default_get! {@interrogate input, $default, $get_fun, $name} - }}; - } + settings.isolated = true; - macro_rules! c { - ($color:expr, $format:expr) => { - format!("\x1b[{}m{}\x1b[0m", $color, $format) - }; - } + let interpreter = InterpreterConfig::new() + .init_stdlib() + .settings(settings) + .interpreter(); - fn format_bytes(bytes: u64) -> String { - let bytes = Bytes::new(bytes); - bytes.to_string() - } + let output_options = options.options.clone(); - fn format_speed(speed: f64) -> String { - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let bytes = Bytes::new(speed.floor() as u64); - format!("{bytes}/s") - } + let (yt_dlp_module, youtube_dl_class) = match interpreter.enter(|vm| { + let yt_dlp_module = vm.import("yt_dlp", 0)?; + let class = yt_dlp_module.get_attr("YoutubeDL", vm)?; - let get_title = || -> String { - match get! {is_string, as_str, "info_dict", "ext"} { - "vtt" => { - format!( - "Subtitles ({})", - default_get! {as_str, "<No Subtitle Language>", "info_dict", "name"} - ) + let maybe_hook = mem::take(&mut options.progress_hook); + let opts = options.into_py_dict(vm); + if let Some(function) = maybe_hook { + opts.get_or_insert(vm, vm.new_pyobj("progress_hooks"), || { + let hook: PyObjectRef = vm.new_function("progress_hook", function).into(); + vm.new_pyobj(vec![hook]) + }) + .expect("Should work?"); } - "webm" | "mp4" | "mp3" | "m4a" => { - default_get! { as_str, "<No title>", "info_dict", "title"}.to_owned() - } - other => panic!("The extension '{other}' is not yet implemented"), - } - }; - match get! {is_string, as_str, "status"} { - "downloading" => { - let elapsed = default_get! {as_f64, 0.0f64, "elapsed"}; - let eta = default_get! {as_f64, 0.0, "eta"}; - let speed = default_get! {as_f64, 0.0, "speed"}; - - let downloaded_bytes = get! {is_u64, as_u64, "downloaded_bytes"}; - let (total_bytes, bytes_is_estimate): (u64, &'static str) = { - let total_bytes = default_get!(as_u64, 0, "total_bytes"); - if total_bytes == 0 { - let maybe_estimate = default_get!(as_u64, 0, "total_bytes_estimate"); - - if maybe_estimate == 0 { - // The download speed should be in bytes per second and the eta in seconds. - // Thus multiplying them gets us the raw bytes (which were estimated by `yt_dlp`, from their `info.json`) - let bytes_still_needed = (speed * eta).ceil() as u64; - - (downloaded_bytes + bytes_still_needed, "~") - } else { - (maybe_estimate, "~") + { + // Unconditionally set a logger. + // Otherwise, yt_dlp will log to stderr. + + /// Is the specified record to be logged? Returns false for no, + /// true for yes. Filters can either modify log records in-place or + /// return a completely different record instance which will replace + /// the original log record in any future processing of the event. + fn filter_error_log(mut input: FuncArgs, vm: &VirtualMachine) -> bool { + let record = input.args.remove(0); + + // Filter out all error logs (they are propagated as rust errors) + let levelname: PyRef<PyStr> = record + .get_attr("levelname", vm) + .expect("This should exist") + .downcast() + .expect("This should be a String"); + + let return_value = levelname.as_str() != "ERROR"; + + if log_enabled!(Level::Debug) && !return_value { + let message: String = { + let get_message = record.get_attr("getMessage", vm).expect("Is set"); + let message: PyRef<PyStr> = get_message + .call((), vm) + .expect("Can be called") + .downcast() + .expect("Downcasting works"); + + message.as_str().to_owned() + }; + + debug!("Swollowed error message: '{message}'"); } - } else { - (total_bytes, "") + return_value } - }; - let percent: f64 = { - if total_bytes == 0 { - 100.0 - } else { - (downloaded_bytes as f64 / total_bytes as f64) * 100.0 + + let logging = setup_logging(vm, "yt_dlp")?; + let ytdl_logger = { + let get_logger = logging.get_item("getLogger", vm)?; + get_logger.call(("yt_dlp",), vm)? + }; + + { + let args = FuncArgs::new( + PosArgs::new(vec![]), + KwArgs::new({ + let mut map = IndexMap::new(); + // Ensure that all events are logged by setting + // the log level to NOTSET (we filter on rust's side) + map.insert("level".to_owned(), vm.new_pyobj(0)); + map + }), + ); + + let basic_config = logging.get_item("basicConfig", vm)?; + basic_config.call(args, vm)?; } - }; - clear_whole_line(); - move_to_col(1); - - eprint!( - "'{}' [{}/{} at {}] -> [{} of {}{} {}] ", - c!("34;1", get_title()), - c!("33;1", Duration::from(Some(elapsed))), - c!("33;1", Duration::from(Some(eta))), - c!("32;1", format_speed(speed)), - c!("31;1", format_bytes(downloaded_bytes)), - c!("31;1", bytes_is_estimate), - c!("31;1", format_bytes(total_bytes)), - c!("36;1", format!("{:.02}%", percent)) - ); - stderr().flush()?; - } - "finished" => { - eprintln!("-> Finished downloading."); - } - "error" => { - // TODO: This should probably return an Err. But I'm not so sure where the error would - // bubble up to (i.e., who would catch it) <2025-01-21> - eprintln!("-> Error while downloading: {}", get_title()); - process::exit(1); - } - other => unreachable!("'{other}' should not be a valid state!"), - }; + { + let add_filter = ytdl_logger.get_attr("addFilter", vm)?; + add_filter.call( + (vm.new_function("yt_dlp_error_filter", filter_error_log),), + vm, + )?; + } - Ok(()) -} + opts.set_item("logger", ytdl_logger, vm)?; + } -pub fn add_hooks<'a>(opts: Bound<'a, PyDict>, py: Python<'_>) -> PyResult<Bound<'a, PyDict>> { - if let Some(hooks) = opts.get_item("progress_hooks")? { - let hooks = hooks.downcast::<PyList>()?; - hooks.append(wrap_pyfunction!(progress_hook, py)?)?; + let youtube_dl_class = class.call((opts,), vm)?; - opts.set_item("progress_hooks", hooks)?; - } else { - // No hooks are set yet - let hooks_list = PyList::new(py, &[wrap_pyfunction!(progress_hook, py)?])?; + Ok::<_, PyRef<PyBaseException>>((yt_dlp_module, youtube_dl_class)) + }) { + Ok(ok) => ok, + Err(err) => { + interpreter.finalize(Some(err)); + return Err(build::Error::Python); + } + }; - opts.set_item("progress_hooks", hooks_list)?; + Ok(Self { + interpreter, + youtube_dl_class, + yt_dlp_module, + options: output_options, + }) } - Ok(opts) -} - -/// Take the result of the ie (may be modified) and resolve all unresolved -/// references (URLs, playlist items). -/// -/// It will also download the videos if 'download'. -/// Returns the resolved `ie_result`. -#[allow(clippy::unused_async)] -#[allow(clippy::missing_panics_doc)] -pub async fn process_ie_result( - yt_dlp_opts: &Map<String, Value>, - ie_result: InfoJson, - download: bool, -) -> Result<InfoJson, YtDlpError> { - Python::with_gil(|py| -> Result<InfoJson, YtDlpError> { - let opts = json_map_to_py_dict(yt_dlp_opts, py)?; - - let instance = get_yt_dlp(py, opts)?; - - let args = { - let ie_result = json_loads_str(py, ie_result)?; - (ie_result,) - }; + /// # Panics + /// + /// If `yt_dlp` changed their location or type of `__version__`. + pub fn version(&self) -> String { + let str_ref: PyRef<PyStr> = self.interpreter.enter_and_expect( + |vm| { + let version_module = self.yt_dlp_module.get_attr("version", vm)?; + let version = version_module.get_attr("__version__", vm)?; + let version = version.downcast().expect("This should always be a string"); + Ok(version) + }, + "yt_dlp version location has changed", + ); + str_ref.to_string() + } - let kwargs = PyDict::new(py); - kwargs.set_item("download", download)?; + /// Download a given list of URLs. + /// Returns the paths they were downloaded to. + /// + /// # Errors + /// If one of the downloads error. + pub fn download(&self, urls: &[Url]) -> Result<Vec<PathBuf>, extract_info::Error> { + let mut out_paths = Vec::with_capacity(urls.len()); + + for url in urls { + info!("Started downloading url: '{url}'"); + let info_json = self.extract_info(url, true, true)?; + + // Try to work around yt-dlp type weirdness + let result_string = if let Some(filename) = info_json.get("filename") { + PathBuf::from(json_cast!(filename, as_str)) + } else { + PathBuf::from(json_get!( + json_cast!( + json_get!(info_json, "requested_downloads", as_array)[0], + as_object + ), + "filename", + as_str + )) + }; - let result = instance - .call_method("process_ie_result", args, Some(&kwargs))? - .downcast_into::<PyDict>() - .expect("This is a dict"); + out_paths.push(result_string); + info!("Finished downloading url"); + } - let result_str = json_dumps(py, result.into_any())?; + Ok(out_paths) + } - serde_json::from_str(&result_str).map_err(Into::into) - }) -} + /// `extract_info(self, url, download=True, ie_key=None, extra_info=None, process=True, force_generic_extractor=False)` + /// + /// Extract and return the information dictionary of the URL + /// + /// Arguments: + /// - `url` URL to extract + /// + /// Keyword arguments: + /// :`download` Whether to download videos + /// :`process` Whether to resolve all unresolved references (URLs, playlist items). + /// Must be True for download to work + /// + /// # Panics + /// If expectations about python fail to hold. + /// + /// # Errors + /// If python operations fail. + pub fn extract_info( + &self, + url: &Url, + download: bool, + process: bool, + ) -> Result<InfoJson, extract_info::Error> { + match self.interpreter.enter(|vm| { + let pos_args = PosArgs::new(vec![vm.new_pyobj(url.to_string())]); + + let kw_args = KwArgs::new({ + let mut map = IndexMap::new(); + map.insert("download".to_owned(), vm.new_pyobj(download)); + map.insert("process".to_owned(), vm.new_pyobj(process)); + map + }); + + let fun_args = FuncArgs::new(pos_args, kw_args); + + let inner = self.youtube_dl_class.get_attr("extract_info", vm)?; + let result = inner + .call_with_args(fun_args, vm)? + .downcast::<PyDict>() + .expect("This is a dict"); + + // Resolve the generator object + if let Ok(generator) = result.get_item("entries", vm) { + if generator.payload_is::<PyList>() { + // already resolved. Do nothing + } else { + let max_backlog = self.options.get("playlistend").map_or(10000, |value| { + usize::try_from(value.as_u64().expect("Works")).expect("Should work") + }); + + let mut out = vec![]; + let next = generator.get_attr("__next__", vm)?; + while let Ok(output) = next.call((), vm) { + out.push(output); + + if out.len() == max_backlog { + break; + } + } + result.set_item("entries", vm.new_pyobj(out), vm)?; + } + } -/// `extract_info(self, url, download=True, ie_key=None, extra_info=None, process=True, force_generic_extractor=False)` -/// -/// Extract and return the information dictionary of the URL -/// -/// Arguments: -/// @param url URL to extract -/// -/// Keyword arguments: -/// @param download Whether to download videos -/// @param process Whether to resolve all unresolved references (URLs, playlist items). -/// Must be True for download to work -/// @param `ie_key` Use only the extractor with this key -/// -/// @param `extra_info` Dictionary containing the extra values to add to the info (For internal use only) -/// @`force_generic_extractor` Force using the generic extractor (Deprecated; use `ie_key`='Generic') -#[allow(clippy::unused_async)] -#[allow(clippy::missing_panics_doc)] -pub async fn extract_info( - yt_dlp_opts: &Map<String, Value>, - url: &Url, - download: bool, - process: bool, -) -> Result<InfoJson, YtDlpError> { - Python::with_gil(|py| -> Result<InfoJson, YtDlpError> { - let opts = json_map_to_py_dict(yt_dlp_opts, py)?; - - let instance = get_yt_dlp(py, opts)?; - let args = (url.as_str(),); - - let kwargs = PyDict::new(py); - kwargs.set_item("download", download)?; - kwargs.set_item("process", process)?; - - let result = instance - .call_method("extract_info", args, Some(&kwargs))? - .downcast_into::<PyDict>() - .expect("This is a dict"); - - // Resolve the generator object - if let Some(generator) = result.get_item("entries")? { - if generator.is_instance_of::<PyList>() { - // already resolved. Do nothing - } else { - let max_backlog = yt_dlp_opts.get("playlistend").map_or(10000, |value| { - usize::try_from(value.as_u64().expect("Works")).expect("Should work") - }); + let result = { + let sanitize = self.youtube_dl_class.get_attr("sanitize_info", vm)?; + let value = sanitize.call((result,), vm)?; - let mut out = vec![]; - while let Ok(output) = generator.call_method0("__next__") { - out.push(output); + value.downcast::<PyDict>().expect("This should stay a dict") + }; - if out.len() == max_backlog { - break; - } + let result_json = json_dumps(result, vm); + + if let Ok(confirm) = env::var("YT_STORE_INFO_JSON") { + if confirm == "yes" { + let mut file = File::create("output.info.json").unwrap(); + write!( + file, + "{}", + serde_json::to_string_pretty(&serde_json::Value::Object( + result_json.clone() + )) + .expect("Valid json") + ) + .unwrap(); } - result.set_item("entries", out)?; + } + + Ok::<_, PyRef<PyBaseException>>(result_json) + }) { + Ok(ok) => Ok(ok), + Err(err) => { + self.interpreter.enter(|vm| { + vm.print_exception(err); + }); + Err(extract_info::Error::Python) } } + } + + /// Take the (potentially modified) result of the information extractor (i.e., + /// [`Self::extract_info`] with `process` and `download` set to false) + /// and resolve all unresolved references (URLs, + /// playlist items). + /// + /// It will also download the videos if 'download' is true. + /// Returns the resolved `ie_result`. + /// + /// # Panics + /// If expectations about python fail to hold. + /// + /// # Errors + /// If python operations fail. + pub fn process_ie_result( + &self, + ie_result: InfoJson, + download: bool, + ) -> Result<InfoJson, process_ie_result::Error> { + match self.interpreter.enter(|vm| { + let pos_args = PosArgs::new(vec![vm.new_pyobj(json_loads(ie_result, vm))]); + + let kw_args = KwArgs::new({ + let mut map = IndexMap::new(); + map.insert("download".to_owned(), vm.new_pyobj(download)); + map + }); + + let fun_args = FuncArgs::new(pos_args, kw_args); + + let inner = self.youtube_dl_class.get_attr("process_ie_result", vm)?; + let result = inner + .call_with_args(fun_args, vm)? + .downcast::<PyDict>() + .expect("This is a dict"); + + let result = { + let sanitize = self.youtube_dl_class.get_attr("sanitize_info", vm)?; + let value = sanitize.call((result,), vm)?; + + value.downcast::<PyDict>().expect("This should stay a dict") + }; - let result_str = json_dumps(py, result.into_any())?; + let result_json = json_dumps(result, vm); - if let Ok(confirm) = env::var("YT_STORE_INFO_JSON") { - if confirm == "yes" { - let mut file = File::create("output.info.json")?; - write!(file, "{result_str}").unwrap(); + Ok::<_, PyRef<PyBaseException>>(result_json) + }) { + Ok(ok) => Ok(ok), + Err(err) => { + self.interpreter.enter(|vm| { + vm.print_exception(err); + }); + Err(process_ie_result::Error::Python) } } - - serde_json::from_str(&result_str).map_err(Into::into) - }) + } } -/// # Panics -/// Only if python fails to return a valid URL. -pub fn unsmuggle_url(smug_url: &Url) -> PyResult<Url> { - Python::with_gil(|py| { - let utils = get_yt_dlp_utils(py)?; - let url = utils - .call_method1("unsmuggle_url", (smug_url.as_str(),))? - .downcast::<PyTuple>()? - .get_item(0)?; - - let url: Url = url - .downcast::<PyString>()? - .to_string() - .parse() - .expect("Python should be able to return a valid url"); - - Ok(url) - }) +#[allow(missing_docs)] +pub mod process_ie_result { + #[derive(Debug, thiserror::Error, Clone, Copy)] + pub enum Error { + #[error("Python threw an exception")] + Python, + } } - -/// Download a given list of URLs. -/// Returns the paths they were downloaded to. -/// -/// # Panics -/// Only if `yt_dlp` changes their `info_json` schema. -pub async fn download( - urls: &[Url], - download_options: &Map<String, Value>, -) -> Result<Vec<PathBuf>, YtDlpError> { - let mut out_paths = Vec::with_capacity(urls.len()); - - for url in urls { - info!("Started downloading url: '{}'", url); - let info_json = extract_info(download_options, url, true, true).await?; - - // Try to work around yt-dlp type weirdness - let result_string = if let Some(filename) = info_json.filename { - filename - } else { - info_json.requested_downloads.expect("This must exist")[0] - .filename - .clone() - }; - - out_paths.push(result_string); - info!("Finished downloading url: '{}'", url); +#[allow(missing_docs)] +pub mod extract_info { + #[derive(Debug, thiserror::Error, Clone, Copy)] + pub enum Error { + #[error("Python threw an exception")] + Python, } - - Ok(out_paths) } -fn json_map_to_py_dict<'a>( - map: &Map<String, Value>, - py: Python<'a>, -) -> PyResult<Bound<'a, PyDict>> { - let json_string = serde_json::to_string(&map).expect("This must always work"); +pub type InfoJson = serde_json::Map<String, serde_json::Value>; +pub type ProgressHookFunction = fn(input: FuncArgs, vm: &VirtualMachine); - let python_dict = json_loads(py, json_string)?; - - Ok(python_dict) +/// Options, that are used to customize the download behaviour. +/// +/// In the future, this might get a Builder api. +/// +/// See `help(yt_dlp.YoutubeDL())` from python for a full list of available options. +#[derive(Default, Debug)] +pub struct YoutubeDLOptions { + options: serde_json::Map<String, serde_json::Value>, + progress_hook: Option<ProgressHookFunction>, } -fn json_dumps(py: Python<'_>, input: Bound<'_, PyAny>) -> PyResult<String> { - // json.dumps(yt_dlp.sanitize_info(input)) +impl YoutubeDLOptions { + #[must_use] + pub fn new() -> Self { + Self { + options: serde_json::Map::new(), + progress_hook: None, + } + } - let yt_dlp = get_yt_dlp(py, PyDict::new(py))?; - let sanitized_result = yt_dlp.call_method1("sanitize_info", (input,))?; + #[must_use] + pub fn set(self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self { + let mut options = self.options; + options.insert(key.into(), value.into()); - let json = PyModule::import(py, "json")?; - let dumps = json.getattr("dumps")?; + Self { + options, + progress_hook: self.progress_hook, + } + } - let output = dumps.call1((sanitized_result,))?; + #[must_use] + pub fn with_progress_hook(self, progress_hook: ProgressHookFunction) -> Self { + if let Some(_previous_hook) = self.progress_hook { + todo!() + } else { + Self { + options: self.options, + progress_hook: Some(progress_hook), + } + } + } - let output_str = output.extract::<String>()?; + /// # Errors + /// If the underlying [`YoutubeDL::from_options`] errors. + pub fn build(self) -> Result<YoutubeDL, build::Error> { + YoutubeDL::from_options(self) + } - Ok(output_str) -} + #[must_use] + pub fn from_json_options(options: serde_json::Map<String, serde_json::Value>) -> Self { + Self { + options, + progress_hook: None, + } + } -fn json_loads_str<T: Serialize>(py: Python<'_>, input: T) -> PyResult<Bound<'_, PyDict>> { - let string = serde_json::to_string(&input).expect("Correct json must be pased"); + #[must_use] + pub fn get(&self, key: &str) -> Option<&serde_json::Value> { + self.options.get(key) + } - json_loads(py, string) + fn into_py_dict(self, vm: &VirtualMachine) -> PyRef<PyDict> { + json_loads(self.options, vm) + } } -fn json_loads(py: Python<'_>, input: String) -> PyResult<Bound<'_, PyDict>> { - // json.loads(input) - - let json = PyModule::import(py, "json")?; - let dumps = json.getattr("loads")?; +#[allow(missing_docs)] +pub mod build { + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error("Python threw an exception")] + Python, - let output = dumps.call1((input,))?; - - Ok(output - .downcast::<PyDict>() - .expect("This should always be a PyDict") - .clone()) + #[error("Io error: {0}")] + Io(#[from] std::io::Error), + } } -fn get_yt_dlp_utils(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> { - let yt_dlp = PyModule::import(py, "yt_dlp")?; - let utils = yt_dlp.getattr("utils")?; - - Ok(utils) +fn json_loads( + input: serde_json::Map<String, serde_json::Value>, + vm: &VirtualMachine, +) -> PyRef<PyDict> { + let json = vm.import("json", 0).expect("Module exists"); + let loads = json.get_attr("loads", vm).expect("Method exists"); + let self_str = serde_json::to_string(&serde_json::Value::Object(input)).expect("Vaild json"); + let dict = loads + .call((self_str,), vm) + .expect("Vaild json is always a valid dict"); + + dict.downcast().expect("Should always be a dict") } -fn get_yt_dlp<'a>(py: Python<'a>, opts: Bound<'a, PyDict>) -> PyResult<Bound<'a, PyAny>> { - // Unconditionally set a logger - let opts = add_logger_and_sig_handler(opts, py)?; - let opts = add_hooks(opts, py)?; - let yt_dlp = PyModule::import(py, "yt_dlp")?; - let youtube_dl = yt_dlp.call_method1("YoutubeDL", (opts,))?; - - Ok(youtube_dl) +/// # Panics +/// If expectation about python operations fail. +pub fn json_dumps( + input: PyRef<PyDict>, + vm: &VirtualMachine, +) -> serde_json::Map<String, serde_json::Value> { + let json = vm.import("json", 0).expect("Module exists"); + let dumps = json.get_attr("dumps", vm).expect("Method exists"); + let dict = dumps + .call((input,), vm) + .map_err(|err| vm.print_exception(err)) + .expect("Might not always work, but for our dicts it works"); + + let string: PyRef<PyStr> = dict.downcast().expect("Should always be a string"); + + let real_string = string.to_str().expect("Should be valid utf8"); + + // { + // let mut file = File::create("debug.dump.json").unwrap(); + // write!(file, "{}", real_string).unwrap(); + // } + + let value: serde_json::Value = serde_json::from_str(real_string).expect("Should be valid json"); + + match value { + serde_json::Value::Object(map) => map, + _ => unreachable!("These should not be json.dumps output"), + } } diff --git a/crates/yt_dlp/src/logging.rs b/crates/yt_dlp/src/logging.rs index e731502..5cb4c1d 100644 --- a/crates/yt_dlp/src/logging.rs +++ b/crates/yt_dlp/src/logging.rs @@ -10,34 +10,66 @@ // This file is taken from: https://github.com/dylanbstorey/pyo3-pylogger/blob/d89e0d6820ebc4f067647e3b74af59dbc4941dd5/src/lib.rs // It is licensed under the Apache 2.0 License, copyright up to 2024 by Dylan Storey -// It was modified by Benedikt Peetz 2024 - -// The pyo3 `pyfunction` proc-macros call unsafe functions internally, which trigger this lint. -#![allow(unsafe_op_in_unsafe_fn)] - -use std::ffi::CString; +// It was modified by Benedikt Peetz 2024, 2025 use log::{Level, MetadataBuilder, Record, logger}; -use pyo3::{ - Bound, PyAny, PyResult, Python, - prelude::{PyAnyMethods, PyListMethods, PyModuleMethods}, - pyfunction, wrap_pyfunction, +use rustpython::vm::{ + PyObjectRef, PyRef, PyResult, VirtualMachine, + builtins::{PyInt, PyList, PyStr}, + convert::ToPyObject, + function::FuncArgs, }; /// Consume a Python `logging.LogRecord` and emit a Rust `Log` instead. -#[allow(clippy::needless_pass_by_value)] -#[pyfunction] -fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> { - let level = record.getattr("levelno")?; - let message = record.getattr("getMessage")?.call0()?.to_string(); - let pathname = record.getattr("pathname")?.to_string(); - let lineno = record - .getattr("lineno")? - .to_string() - .parse::<u32>() - .expect("This should always be a u32"); - - let logger_name = record.getattr("name")?.to_string(); +fn host_log(mut input: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let record = input.args.remove(0); + let rust_target = { + let base: PyRef<PyStr> = input.args.remove(0).downcast().expect("Should be a string"); + base.as_str().to_owned() + }; + + let level = { + let level: PyRef<PyInt> = record + .get_attr("levelno", vm)? + .downcast() + .expect("Should always be an int"); + level.as_u32_mask() + }; + let message = { + let get_message = record.get_attr("getMessage", vm)?; + let message: PyRef<PyStr> = get_message + .call((), vm)? + .downcast() + .expect("Downcasting works"); + + message.as_str().to_owned() + }; + + let pathname = { + let pathname: PyRef<PyStr> = record + .get_attr("pathname", vm)? + .downcast() + .expect("Is a string"); + + pathname.as_str().to_owned() + }; + + let lineno = { + let lineno: PyRef<PyInt> = record + .get_attr("lineno", vm)? + .downcast() + .expect("Is a number"); + + lineno.as_u32_mask() + }; + + let logger_name = { + let name: PyRef<PyStr> = record + .get_attr("name", vm)? + .downcast() + .expect("Should be a string"); + name.as_str().to_owned() + }; let full_target: Option<String> = if logger_name.trim().is_empty() || logger_name == "root" { None @@ -48,25 +80,25 @@ fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> { Some(format!("{rust_target}::{logger_name}")) }; - let target = full_target.as_deref().unwrap_or(rust_target); + let target = full_target.as_deref().unwrap_or(&rust_target); // error - let error_metadata = if level.ge(40u8)? { + let error_metadata = if level >= 40 { MetadataBuilder::new() .target(target) .level(Level::Error) .build() - } else if level.ge(30u8)? { + } else if level >= 30 { MetadataBuilder::new() .target(target) .level(Level::Warn) .build() - } else if level.ge(20u8)? { + } else if level >= 20 { MetadataBuilder::new() .target(target) .level(Level::Info) .build() - } else if level.ge(10u8)? { + } else if level >= 10 { MetadataBuilder::new() .target(target) .level(Level::Debug) @@ -98,13 +130,24 @@ fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> { /// # Panics /// Only if internal assertions fail. #[allow(clippy::module_name_repetitions)] -pub fn setup_logging(py: Python<'_>, target: &str) -> PyResult<()> { - let logging = py.import("logging")?; +pub(super) fn setup_logging(vm: &VirtualMachine, target: &str) -> PyResult<PyObjectRef> { + let logging = vm.import("logging", 0)?; - logging.setattr("host_log", wrap_pyfunction!(host_log, &logging)?)?; + let scope = vm.new_scope_with_builtins(); - py.run( - CString::new(format!( + for (key, value) in logging.dict().expect("Should be a dict") { + let key: PyRef<PyStr> = key.downcast().expect("Is a string"); + + scope.globals.set_item(key.as_str(), value, vm)?; + } + scope + .globals + .set_item("host_log", vm.new_function("host_log", host_log).into(), vm)?; + + let local_scope = scope.clone(); + vm.run_code_string( + local_scope, + format!( r#" class HostHandler(Handler): def __init__(self, level=0): @@ -119,15 +162,36 @@ def basicConfig(*pargs, **kwargs): kwargs["handlers"] = [HostHandler()] return oldBasicConfig(*pargs, **kwargs) "# - )) - .expect("This is hardcoded") - .as_c_str(), - Some(&logging.dict()), - None, + ) + .as_str(), + "<embedded logging inintializing code>".to_owned(), )?; - let all = logging.index()?; - all.append("HostHandler")?; - - Ok(()) + let all: PyRef<PyList> = logging + .get_attr("__all__", vm)? + .downcast() + .expect("Is a list"); + all.borrow_vec_mut().push(vm.new_pyobj("HostHandler")); + + // { + // let logging_dict = logging.dict().expect("Exists"); + // + // for (key, val) in scope.globals { + // let key: PyRef<PyStr> = key.downcast().expect("Is a string"); + // + // if !logging_dict.contains_key(key.as_str(), vm) { + // logging_dict.set_item(key.as_str(), val, vm)?; + // } + // } + // + // for (key, val) in scope.locals { + // let key: PyRef<PyStr> = key.downcast().expect("Is a string"); + // + // if !logging_dict.contains_key(key.as_str(), vm) { + // logging_dict.set_item(key.as_str(), val, vm)?; + // } + // } + // } + + Ok(scope.globals.to_pyobject(vm)) } diff --git a/crates/yt_dlp/src/progress_hook.rs b/crates/yt_dlp/src/progress_hook.rs new file mode 100644 index 0000000..7a7628a --- /dev/null +++ b/crates/yt_dlp/src/progress_hook.rs @@ -0,0 +1,41 @@ +#[macro_export] +macro_rules! mk_python_function { + ($name:ident, $new_name:ident) => { + pub fn $new_name( + mut args: $crate::progress_hook::rustpython::vm::function::FuncArgs, + vm: &$crate::progress_hook::rustpython::vm::VirtualMachine, + ) { + use $crate::progress_hook::rustpython; + + let input = { + let dict: rustpython::vm::PyRef<rustpython::vm::builtins::PyDict> = args + .args + .remove(0) + .downcast() + .expect("The progress hook is always called with these args"); + let new_dict = rustpython::vm::builtins::PyDict::new_ref(&vm.ctx); + dict.into_iter() + .filter_map(|(name, value)| { + let real_name: rustpython::vm::PyRefExact<rustpython::vm::builtins::PyStr> = + name.downcast_exact(vm).expect("Is a string"); + let name_str = real_name.to_str().expect("Is a string"); + if name_str.starts_with('_') { + None + } else { + Some((name_str.to_owned(), value)) + } + }) + .for_each(|(key, value)| { + new_dict + .set_item(&key, value, vm) + .expect("This is a transpositions, should always be valid"); + }); + + $crate::json_dumps(new_dict, vm) + }; + $name(input).expect("Shall not fail!"); + } + }; +} + +pub use rustpython; diff --git a/crates/yt_dlp/src/python_json_decode_failed.error_msg b/crates/yt_dlp/src/python_json_decode_failed.error_msg deleted file mode 100644 index d10688e..0000000 --- a/crates/yt_dlp/src/python_json_decode_failed.error_msg +++ /dev/null @@ -1,5 +0,0 @@ -Failed to decode yt-dlp's response: {} - -This is probably a bug. -Try running the command again with the `YT_STORE_INFO_JSON=yes` environment variable set -and maybe debug it further via `yt check info-json output.info.json`. diff --git a/crates/yt_dlp/src/python_json_decode_failed.error_msg.license b/crates/yt_dlp/src/python_json_decode_failed.error_msg.license deleted file mode 100644 index 7813eb6..0000000 --- a/crates/yt_dlp/src/python_json_decode_failed.error_msg.license +++ /dev/null @@ -1,9 +0,0 @@ -yt - A fully featured command line YouTube client - -Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -SPDX-License-Identifier: GPL-3.0-or-later - -This file is part of Yt. - -You should have received a copy of the License along with this program. -If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/crates/yt_dlp/src/tests.rs b/crates/yt_dlp/src/tests.rs deleted file mode 100644 index 91b6626..0000000 --- a/crates/yt_dlp/src/tests.rs +++ /dev/null @@ -1,89 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -use std::sync::LazyLock; - -use serde_json::{Value, json}; -use url::Url; - -static YT_OPTS: LazyLock<serde_json::Map<String, Value>> = LazyLock::new(|| { - match json!({ - "playliststart": 1, - "playlistend": 10, - "noplaylist": false, - "extract_flat": false, - }) { - Value::Object(obj) => obj, - _ => unreachable!("This json is hardcoded"), - } -}); - -#[tokio::test] -#[ignore = "This test hangs forever"] -async fn test_extract_info_video() { - let info = crate::extract_info( - &YT_OPTS, - &Url::parse("https://www.youtube.com/watch?v=dbjPnXaacAU").expect("Is valid."), - false, - false, - ) - .await - .map_err(|err| format!("Encountered error: '{err}'")) - .unwrap(); - - println!("{info:#?}"); -} - -#[tokio::test] -#[ignore = "This test hangs forever"] -async fn test_extract_info_url() { - let err = crate::extract_info( - &YT_OPTS, - &Url::parse("https://google.com").expect("Is valid."), - false, - false, - ) - .await - .map_err(|err| format!("Encountered error: '{err}'")) - .unwrap(); - - println!("{err:#?}"); -} - -#[tokio::test] -#[ignore = "This test hangs forever"] -async fn test_extract_info_playlist() { - let err = crate::extract_info( - &YT_OPTS, - &Url::parse("https://www.youtube.com/@TheGarriFrischer/videos").expect("Is valid."), - false, - true, - ) - .await - .map_err(|err| format!("Encountered error: '{err}'")) - .unwrap(); - - println!("{err:#?}"); -} -#[tokio::test] -#[ignore = "This test hangs forever"] -async fn test_extract_info_playlist_full() { - let err = crate::extract_info( - &YT_OPTS, - &Url::parse("https://www.youtube.com/@NixOS-Foundation/videos").expect("Is valid."), - false, - true, - ) - .await - .map_err(|err| format!("Encountered error: '{err}'")) - .unwrap(); - - println!("{err:#?}"); -} diff --git a/crates/yt_dlp/src/wrapper/info_json.rs b/crates/yt_dlp/src/wrapper/info_json.rs deleted file mode 100644 index a2c00df..0000000 --- a/crates/yt_dlp/src/wrapper/info_json.rs +++ /dev/null @@ -1,824 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -// `yt_dlp` named them like this. -#![allow(clippy::pub_underscore_fields)] - -use std::{collections::HashMap, path::PathBuf}; - -use pyo3::{Bound, PyResult, Python, types::PyDict}; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; -use url::Url; - -use crate::json_loads_str; - -type Todo = String; -type Extractor = String; -type ExtractorKey = String; - -// TODO: Change this to map `_type` to a structure of values, instead of the options <2024-05-27> -// And replace all the strings with better types (enums or urls) -#[derive(Debug, Deserialize, Serialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct InfoJson { - #[serde(skip_serializing_if = "Option::is_none")] - pub __files_to_move: Option<FilesToMove>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub __last_playlist_index: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub __post_extractor: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub __x_forwarded_for_ip: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub _filename: Option<PathBuf>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub _format_sort_fields: Option<Vec<String>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub _has_drm: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub _type: Option<InfoType>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub _version: Option<Version>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub abr: Option<f64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub acodec: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub age_limit: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub artists: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub aspect_ratio: Option<f64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub asr: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub audio_channels: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub audio_ext: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub automatic_captions: Option<HashMap<String, Vec<Caption>>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub availability: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub average_rating: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub categories: Option<Vec<String>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub channel: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_follower_count: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_id: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_is_verified: Option<bool>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_url: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub chapters: Option<Vec<Chapter>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub comment_count: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub comments: Option<Vec<Comment>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub concurrent_view_count: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub container: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub direct: Option<bool>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub display_id: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub downloader_options: Option<DownloaderOptions>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub duration: Option<f64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub duration_string: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub dynamic_range: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub entries: Option<Vec<InfoJson>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub episode: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub episode_number: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub epoch: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ext: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub extractor: Option<Extractor>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub extractor_key: Option<ExtractorKey>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub filename: Option<PathBuf>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub filesize: Option<u64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub filesize_approx: Option<u64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub format_id: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub format_index: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub format_note: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub formats: Option<Vec<Format>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub fps: Option<f64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub fulltitle: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub genre: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub genres: Option<Vec<String>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub has_drm: Option<bool>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub heatmap: Option<Vec<HeatMapEntry>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub height: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub hls_aes: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub http_headers: Option<HttpHeader>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ie_key: Option<ExtractorKey>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub is_live: Option<bool>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub language: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub language_preference: Option<i32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub license: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub like_count: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub live_status: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub location: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub manifest_url: Option<Url>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub media_type: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub modified_date: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub n_entries: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub original_url: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playable_in_embed: Option<bool>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_autonumber: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_channel: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_channel_id: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_count: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_id: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_index: Option<u64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_title: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_uploader: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_uploader_id: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub playlist_webpage_url: Option<Url>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub preference: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub protocol: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub quality: Option<f64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub release_date: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub release_timestamp: Option<u64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub release_year: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub repost_count: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub requested_downloads: Option<Vec<RequestedDownloads>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub requested_entries: Option<Vec<u32>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub requested_formats: Option<Vec<Format>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub requested_subtitles: Option<HashMap<String, Subtitle>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub resolution: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub season: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub season_number: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub series: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub source_preference: Option<i32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub sponsorblock_chapters: Option<Vec<SponsorblockChapter>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub stretched_ratio: Option<Todo>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub subtitles: Option<HashMap<String, Vec<Caption>>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub tags: Option<Vec<String>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub tbr: Option<f64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail: Option<Url>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnails: Option<Vec<ThumbNail>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub timestamp: Option<f64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub upload_date: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub uploader: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub uploader_id: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub uploader_url: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option<Url>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub vbr: Option<f64>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub vcodec: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub video_ext: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub view_count: Option<u32>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub was_live: Option<bool>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub webpage_url: Option<Url>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub webpage_url_basename: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub webpage_url_domain: Option<String>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub width: Option<u32>, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq)] -#[serde(deny_unknown_fields)] -#[allow(missing_copy_implementations)] -pub struct FilesToMove {} - -#[derive(Debug, Deserialize, Serialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct RequestedDownloads { - pub __files_to_merge: Option<Vec<Todo>>, - pub __finaldir: PathBuf, - pub __infojson_filename: PathBuf, - pub __postprocessors: Vec<Todo>, - pub __real_download: bool, - pub __write_download_archive: bool, - pub _filename: PathBuf, - pub _type: InfoType, - pub _version: Version, - pub abr: f64, - pub acodec: String, - pub aspect_ratio: Option<f64>, - pub asr: Option<u32>, - pub audio_channels: Option<u32>, - pub audio_ext: Option<String>, - pub chapters: Option<Vec<SponsorblockChapter>>, - pub duration: Option<f64>, - pub dynamic_range: Option<String>, - pub ext: String, - pub filename: PathBuf, - pub filepath: PathBuf, - pub filesize_approx: Option<u64>, - pub format: String, - pub format_id: String, - pub format_note: Option<String>, - pub fps: Option<f64>, - pub has_drm: Option<bool>, - pub height: Option<u32>, - pub http_headers: Option<HttpHeader>, - pub infojson_filename: PathBuf, - pub language: Option<String>, - pub manifest_url: Option<Url>, - pub protocol: String, - pub quality: Option<i64>, - pub requested_formats: Option<Vec<Format>>, - pub resolution: String, - pub tbr: f64, - pub url: Option<Url>, - pub vbr: f64, - pub vcodec: String, - pub video_ext: Option<String>, - pub width: Option<u32>, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] -#[serde(deny_unknown_fields)] -pub struct Subtitle { - pub ext: SubtitleExt, - pub filepath: PathBuf, - pub filesize: Option<u64>, - pub fragment_base_url: Option<Url>, - pub fragments: Option<Vec<Fragment>>, - pub manifest_url: Option<Url>, - pub name: Option<String>, - pub protocol: Option<Todo>, - pub url: Url, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)] -pub enum SubtitleExt { - #[serde(alias = "vtt")] - Vtt, - - #[serde(alias = "mp4")] - Mp4, - - #[serde(alias = "json")] - Json, - #[serde(alias = "json3")] - Json3, - - #[serde(alias = "ttml")] - Ttml, - - #[serde(alias = "srv1")] - Srv1, - #[serde(alias = "srv2")] - Srv2, - #[serde(alias = "srv3")] - Srv3, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] -#[serde(deny_unknown_fields)] -pub struct Caption { - pub ext: SubtitleExt, - pub filepath: Option<PathBuf>, - pub filesize: Option<u64>, - pub fragments: Option<Vec<SubtitleFragment>>, - pub fragment_base_url: Option<Url>, - pub manifest_url: Option<Url>, - pub name: Option<String>, - pub protocol: Option<String>, - pub url: String, - pub video_id: Option<String>, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] -#[serde(deny_unknown_fields)] -pub struct SubtitleFragment { - path: PathBuf, - duration: Option<f64>, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] -#[serde(deny_unknown_fields)] -pub struct Chapter { - pub end_time: f64, - pub start_time: f64, - pub title: String, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct SponsorblockChapter { - /// This is an utterly useless field, and should thus be ignored - pub _categories: Option<Vec<Vec<Value>>>, - - pub categories: Option<Vec<SponsorblockChapterCategory>>, - pub category: Option<SponsorblockChapterCategory>, - pub category_names: Option<Vec<String>>, - pub end_time: f64, - pub name: Option<String>, - pub r#type: Option<SponsorblockChapterType>, - pub start_time: f64, - pub title: String, -} - -pub fn get_none<'de, D, T>(_: D) -> Result<Option<T>, D::Error> -where - D: Deserializer<'de>, -{ - Ok(None) -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)] -#[serde(deny_unknown_fields)] -pub enum SponsorblockChapterType { - #[serde(alias = "skip")] - Skip, - - #[serde(alias = "chapter")] - Chapter, - - #[serde(alias = "poi")] - Poi, -} -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)] -#[serde(deny_unknown_fields)] -pub enum SponsorblockChapterCategory { - #[serde(alias = "filler")] - Filler, - - #[serde(alias = "interaction")] - Interaction, - - #[serde(alias = "music_offtopic")] - MusicOfftopic, - - #[serde(alias = "poi_highlight")] - PoiHighlight, - - #[serde(alias = "preview")] - Preview, - - #[serde(alias = "sponsor")] - Sponsor, - - #[serde(alias = "selfpromo")] - SelfPromo, - - #[serde(alias = "chapter")] - Chapter, - - #[serde(alias = "intro")] - Intro, - - #[serde(alias = "outro")] - Outro, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] -#[serde(deny_unknown_fields)] -#[allow(missing_copy_implementations)] -pub struct HeatMapEntry { - pub start_time: f64, - pub end_time: f64, - pub value: f64, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)] -#[serde(deny_unknown_fields)] -pub enum InfoType { - #[serde(alias = "playlist")] - #[serde(rename(serialize = "playlist"))] - Playlist, - - #[serde(alias = "url")] - #[serde(rename(serialize = "url"))] - Url, - - #[serde(alias = "video")] - #[serde(rename(serialize = "video"))] - Video, -} - -#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)] -#[serde(deny_unknown_fields)] -pub struct Version { - pub current_git_head: Option<String>, - pub release_git_head: String, - pub repository: String, - pub version: String, -} - -#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)] -#[serde(from = "String")] -#[serde(deny_unknown_fields)] -pub enum Parent { - Root, - Id(String), -} - -impl Parent { - #[must_use] - pub fn id(&self) -> Option<&str> { - if let Self::Id(id) = self { - Some(id) - } else { - None - } - } -} - -impl From<String> for Parent { - fn from(value: String) -> Self { - if value == "root" { - Self::Root - } else { - Self::Id(value) - } - } -} - -#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)] -#[serde(from = "String")] -#[serde(deny_unknown_fields)] -pub struct Id { - pub id: String, -} -impl From<String> for Id { - fn from(value: String) -> Self { - Self { - // Take the last element if the string is split with dots, otherwise take the full id - id: value.split('.').last().unwrap_or(&value).to_owned(), - } - } -} - -#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)] -#[serde(deny_unknown_fields)] -#[allow(clippy::struct_excessive_bools)] -pub struct Comment { - pub id: Id, - pub text: String, - #[serde(default = "zero")] - pub like_count: u32, - pub is_pinned: bool, - pub author_id: String, - #[serde(default = "unknown")] - pub author: String, - pub author_is_verified: bool, - pub author_thumbnail: Url, - pub parent: Parent, - #[serde(deserialize_with = "edited_from_time_text", alias = "_time_text")] - pub edited: bool, - // Can't also be deserialized, as it's already used in 'edited' - // _time_text: String, - pub timestamp: i64, - pub author_url: Option<Url>, - pub author_is_uploader: bool, - pub is_favorited: bool, -} -fn unknown() -> String { - "<Unknown>".to_string() -} -fn zero() -> u32 { - 0 -} -fn edited_from_time_text<'de, D>(d: D) -> Result<bool, D::Error> -where - D: Deserializer<'de>, -{ - let s = String::deserialize(d)?; - if s.contains(" (edited)") { - Ok(true) - } else { - Ok(false) - } -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] -#[serde(deny_unknown_fields)] -pub struct ThumbNail { - pub id: Option<String>, - pub preference: Option<i32>, - /// in the form of "[`height`]x[`width`]" - pub resolution: Option<String>, - pub url: Url, - pub width: Option<u32>, - pub height: Option<u32>, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] -#[serde(deny_unknown_fields)] -pub struct Format { - pub __needs_testing: Option<bool>, - pub __working: Option<bool>, - pub abr: Option<f64>, - pub acodec: Option<String>, - pub aspect_ratio: Option<f64>, - pub asr: Option<f64>, - pub audio_channels: Option<u32>, - pub audio_ext: Option<String>, - pub columns: Option<u32>, - pub container: Option<String>, - pub downloader_options: Option<DownloaderOptions>, - pub dynamic_range: Option<String>, - pub ext: String, - pub filepath: Option<PathBuf>, - pub filesize: Option<u64>, - pub filesize_approx: Option<u64>, - pub format: Option<String>, - pub format_id: String, - pub format_index: Option<String>, - pub format_note: Option<String>, - pub fps: Option<f64>, - pub fragment_base_url: Option<Todo>, - pub fragments: Option<Vec<Fragment>>, - pub has_drm: Option<bool>, - pub height: Option<u32>, - pub http_headers: Option<HttpHeader>, - pub is_dash_periods: Option<bool>, - pub is_live: Option<bool>, - pub language: Option<String>, - pub language_preference: Option<i32>, - pub manifest_stream_number: Option<u32>, - pub manifest_url: Option<Url>, - pub preference: Option<i32>, - pub protocol: Option<String>, - pub quality: Option<f64>, - pub resolution: Option<String>, - pub rows: Option<u32>, - pub source_preference: Option<i32>, - pub tbr: Option<f64>, - pub url: Url, - pub vbr: Option<f64>, - pub vcodec: String, - pub video_ext: Option<String>, - pub width: Option<u32>, -} - -#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)] -#[serde(deny_unknown_fields)] -#[allow(missing_copy_implementations)] -pub struct DownloaderOptions { - http_chunk_size: u64, -} - -#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)] -#[serde(deny_unknown_fields)] -pub struct HttpHeader { - #[serde(alias = "User-Agent")] - pub user_agent: Option<String>, - - #[serde(alias = "Accept")] - pub accept: Option<String>, - - #[serde(alias = "X-Forwarded-For")] - pub x_forwarded_for: Option<String>, - - #[serde(alias = "Accept-Language")] - pub accept_language: Option<String>, - - #[serde(alias = "Sec-Fetch-Mode")] - pub sec_fetch_mode: Option<String>, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] -#[serde(deny_unknown_fields)] -pub struct Fragment { - pub duration: Option<f64>, - pub fragment_count: Option<usize>, - pub path: Option<PathBuf>, - pub url: Option<Url>, -} - -impl InfoJson { - pub fn to_py_dict(self, py: Python<'_>) -> PyResult<Bound<'_, PyDict>> { - let output: Bound<'_, PyDict> = json_loads_str(py, self)?; - Ok(output) - } -} diff --git a/crates/yt_dlp/src/wrapper/mod.rs b/crates/yt_dlp/src/wrapper/mod.rs deleted file mode 100644 index 3fe3247..0000000 --- a/crates/yt_dlp/src/wrapper/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -pub mod info_json; -// pub mod yt_dlp_options; diff --git a/crates/yt_dlp/src/wrapper/yt_dlp_options.rs b/crates/yt_dlp/src/wrapper/yt_dlp_options.rs deleted file mode 100644 index 25595b5..0000000 --- a/crates/yt_dlp/src/wrapper/yt_dlp_options.rs +++ /dev/null @@ -1,62 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -use pyo3::{Bound, PyResult, Python, types::PyDict}; -use serde::Serialize; - -use crate::json_loads; - -#[derive(Serialize, Clone)] -pub struct YtDlpOptions { - pub playliststart: u32, - pub playlistend: u32, - pub noplaylist: bool, - pub extract_flat: ExtractFlat, - // pub extractor_args: ExtractorArgs, - // pub format: String, - // pub fragment_retries: u32, - // #[serde(rename(serialize = "getcomments"))] - // pub get_comments: bool, - // #[serde(rename(serialize = "ignoreerrors"))] - // pub ignore_errors: bool, - // pub retries: u32, - // #[serde(rename(serialize = "writeinfojson"))] - // pub write_info_json: bool, - // pub postprocessors: Vec<serde_json::Map<String, serde_json::Value>>, -} - -#[derive(Serialize, Copy, Clone)] -pub enum ExtractFlat { - #[serde(rename(serialize = "in_playlist"))] - InPlaylist, - - #[serde(rename(serialize = "discard_in_playlist"))] - DiscardInPlaylist, -} - -#[derive(Serialize, Clone)] -pub struct ExtractorArgs { - pub youtube: YoutubeExtractorArgs, -} - -#[derive(Serialize, Clone)] -pub struct YoutubeExtractorArgs { - comment_sort: Vec<String>, - max_comments: Vec<String>, -} - -impl YtDlpOptions { - pub fn to_py_dict(self, py: Python) -> PyResult<Bound<PyDict>> { - let string = serde_json::to_string(&self).expect("This should always work"); - - let output: Bound<PyDict> = json_loads(py, string)?; - Ok(output) - } -} diff --git a/flake.lock b/flake.lock index d5590d5..ba25c93 100644 --- a/flake.lock +++ b/flake.lock @@ -1,61 +1,27 @@ { "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1739866667, - "narHash": "sha256-EO1ygNKZlsAC9avfcwHkKGMsmipUk1Uc0TbrEZpkn64=", + "lastModified": 1749809936, + "narHash": "sha256-WPGRaj7CKfZukjcpxiacp29uYfMl3S9zFiEsVFv/HWM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "73cf49b8ad837ade2de76f87eb53fc85ed5d4680", + "rev": "ec4c48ddcd5718cc1312f432b800fbbfe63ee2fe", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "nixos-unstable-small", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "treefmt-nix": "treefmt-nix" } }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, "treefmt-nix": { "inputs": { "nixpkgs": [ @@ -63,11 +29,11 @@ ] }, "locked": { - "lastModified": 1739829690, - "narHash": "sha256-mL1szCeIsjh6Khn3nH2cYtwO5YXG6gBiTw1A30iGeDU=", + "lastModified": 1749194973, + "narHash": "sha256-eEy8cuS0mZ2j/r/FE0/LYBSBcIs/MKOIVakwHVuqTfk=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "3d0579f5cc93436052d94b73925b48973a104204", + "rev": "a05be418a1af1198ca0f63facb13c985db4cb3c5", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 18a5f62..1a6b43b 100644 --- a/flake.nix +++ b/flake.nix @@ -11,9 +11,8 @@ description = "yt"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small"; - flake-utils.url = "github:numtide/flake-utils"; treefmt-nix = { url = "github:numtide/treefmt-nix"; inputs = { @@ -25,21 +24,20 @@ outputs = { self, nixpkgs, - flake-utils, treefmt-nix, - }: (flake-utils.lib.eachDefaultSystem (system: let + }: let + system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages."${system}"; - python = pkgs.python3.withPackages (ps: [ - ps.yt-dlp - ]); - buildInputs = with pkgs; [ mpv-unwrapped.dev + libffi + openssl ]; nativeBuildInputs = with pkgs; [ llvmPackages_latest.clang-unwrapped.lib + pkg-config # Needed for the tests in `libmpv2` SDL2 @@ -50,25 +48,27 @@ treefmtEval = import ./treefmt.nix {inherit treefmt-nix pkgs;}; in { - packages = { + packages."${system}" = { inherit yt tree-sitter-yts; default = self.packages.${system}.yt; }; - checks = { + checks."${system}" = { inherit yt; formatting = treefmtEval.config.build.check self; }; - formatter = treefmtEval.config.build.wrapper; + formatter."${system}" = treefmtEval.config.build.wrapper; - devShells.default = pkgs.mkShell { + devShells."${system}".default = pkgs.mkShell { env = let clang_version = pkgs.lib.versions.major pkgs.llvmPackages_latest.clang-unwrapped.version; in { FFMPEG_LOCATION = "${pkgs.lib.getExe pkgs.ffmpeg}"; + + # These are needed for `libmpv` to compile. LIBCLANG_PATH = "${pkgs.llvmPackages_latest.clang-unwrapped.lib}/lib/libclang.so"; LIBCLANG_INCLUDE_PATH = "${pkgs.llvmPackages_latest.clang-unwrapped.lib}/lib/clang/${clang_version}/include"; C_INCLUDE_PATH = "${pkgs.glibc.dev}/include"; @@ -89,6 +89,7 @@ pkgs.cargo-flamegraph # Releng + pkgs.git-bug pkgs.reuse pkgs.cocogitto @@ -100,7 +101,9 @@ pkgs.sqlite-interactive # yt_dlp - python + pkgs.yt-dlp + pkgs.python3Packages.yt-dlp + pkgs.python3 pkgs.jq pkgs.ffmpeg @@ -109,5 +112,5 @@ pkgs.tree-sitter ]; }; - })); + }; } diff --git a/scripts/mkdb.sh b/scripts/mkdb.sh index 6bcebaf..f0c7740 100755 --- a/scripts/mkdb.sh +++ b/scripts/mkdb.sh @@ -16,9 +16,14 @@ db="$root/target/database.sqlx" [ -f "$db" ] && rm "$db" [ -d "$root/target" ] || mkdir "$root/target" -fd . "$root/yt/src/storage/migrate/sql" | while read -r sql_file; do +fd . "$root/crates/yt/src/storage/migrate/sql" | while read -r sql_file; do echo "Applying sql migration file: $(basename "$sql_file").." - sqlite3 "$db" <"$sql_file" + { + # NOTE(@bpeetz): The wrapping in a transaction is needed to simulate the rust code. <2025-05-07> + echo "BEGIN TRANSACTION;" + cat "$sql_file" + echo "COMMIT TRANSACTION;" + } | sqlite3 "$db" done # vim: ft=sh diff --git a/yt/src/comments/comment.rs b/yt/src/comments/comment.rs deleted file mode 100644 index 6b8cf73..0000000 --- a/yt/src/comments/comment.rs +++ /dev/null @@ -1,65 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -use yt_dlp::wrapper::info_json::Comment; - -#[derive(Debug, Clone)] -#[allow(clippy::module_name_repetitions)] -pub struct CommentExt { - pub value: Comment, - pub replies: Vec<CommentExt>, -} - -#[derive(Debug, Default)] -pub struct Comments { - pub(super) vec: Vec<CommentExt>, -} - -impl Comments { - pub fn new() -> Self { - Self::default() - } - pub fn push(&mut self, value: CommentExt) { - self.vec.push(value); - } - pub fn get_mut(&mut self, key: &str) -> Option<&mut CommentExt> { - self.vec.iter_mut().filter(|c| c.value.id.id == key).last() - } - pub fn insert(&mut self, key: &str, value: CommentExt) { - let parent = self - .vec - .iter_mut() - .filter(|c| c.value.id.id == key) - .last() - .expect("One of these should exist"); - parent.push_reply(value); - } -} -impl CommentExt { - pub fn push_reply(&mut self, value: CommentExt) { - self.replies.push(value); - } - pub fn get_mut_reply(&mut self, key: &str) -> Option<&mut CommentExt> { - self.replies - .iter_mut() - .filter(|c| c.value.id.id == key) - .last() - } -} - -impl From<Comment> for CommentExt { - fn from(value: Comment) -> Self { - Self { - replies: vec![], - value, - } - } -} diff --git a/yt/src/download/download_options.rs b/yt/src/download/download_options.rs deleted file mode 100644 index 8f5a609..0000000 --- a/yt/src/download/download_options.rs +++ /dev/null @@ -1,113 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -use serde_json::{Value, json}; - -use crate::{app::App, storage::video_database::YtDlpOptions}; - -#[must_use] -pub fn download_opts(app: &App, additional_opts: &YtDlpOptions) -> serde_json::Map<String, Value> { - match json!({ - "extract_flat": "in_playlist", - "extractor_args": { - "youtube": { - "comment_sort": [ - "top" - ], - "max_comments": [ - "150", - "all", - "100" - ] - } - }, - - "prefer_free_formats": true, - "ffmpeg_location": env!("FFMPEG_LOCATION"), - "format": "bestvideo[height<=?1080]+bestaudio/best", - "fragment_retries": 10, - "getcomments": true, - "ignoreerrors": false, - "retries": 10, - - "writeinfojson": true, - // NOTE: This results in a constant warning message. <2025-01-04> - // "writeannotations": true, - "writesubtitles": true, - "writeautomaticsub": true, - - "outtmpl": { - "default": app.config.paths.download_dir.join("%(channel)s/%(title)s.%(ext)s"), - "chapter": "%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s" - }, - "compat_opts": {}, - "forceprint": {}, - "print_to_file": {}, - "windowsfilenames": false, - "restrictfilenames": false, - "trim_file_names": false, - "postprocessors": [ - { - "api": "https://sponsor.ajay.app", - "categories": [ - "interaction", - "intro", - "music_offtopic", - "sponsor", - "outro", - "poi_highlight", - "preview", - "selfpromo", - "filler", - "chapter" - ], - "key": "SponsorBlock", - "when": "after_filter" - }, - { - "force_keyframes": false, - "key": "ModifyChapters", - "remove_chapters_patterns": [], - "remove_ranges": [], - "remove_sponsor_segments": [ - "sponsor" - ], - "sponsorblock_chapter_title": "[SponsorBlock]: %(category_names)l" - }, - { - "add_chapters": true, - "add_infojson": null, - "add_metadata": false, - "key": "FFmpegMetadata" - }, - { - "key": "FFmpegConcat", - "only_multi_video": true, - "when": "playlist" - } - ] - }) { - Value::Object(mut obj) => { - obj.insert( - "subtitleslangs".to_owned(), - Value::Array( - additional_opts - .subtitle_langs - .split(',') - .map(|val| Value::String(val.to_owned())) - .collect::<Vec<_>>(), - ), - ); - obj - } - _ => unreachable!("This is an object"), - } -} diff --git a/yt/src/update/updater.rs b/yt/src/update/updater.rs deleted file mode 100644 index 2b13378..0000000 --- a/yt/src/update/updater.rs +++ /dev/null @@ -1,171 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -use std::io::{Write, stderr}; - -use anyhow::{Context, Result}; -use blake3::Hash; -use futures::{ - StreamExt, TryStreamExt, - stream::{self}, -}; -use log::{Level, debug, error, log_enabled}; -use serde_json::json; -use yt_dlp::{error::YtDlpError, process_ie_result, wrapper::info_json::InfoJson}; - -use crate::{app::App, storage::subscriptions::Subscription}; - -use super::process_subscription; - -pub(super) struct Updater<'a> { - max_backlog: usize, - hashes: &'a [Hash], -} - -impl<'a> Updater<'a> { - pub(super) fn new(max_backlog: usize, hashes: &'a [Hash]) -> Self { - Self { - max_backlog, - hashes, - } - } - - pub(super) async fn update( - &mut self, - app: &App, - subscriptions: &[&Subscription], - ) -> Result<()> { - let mut stream = stream::iter(subscriptions) - .map(|sub| self.get_new_entries(sub)) - .buffer_unordered(100); - - while let Some(output) = stream.next().await { - let mut entries = output?; - - if entries.is_empty() { - continue; - } - - let (sub, entry) = entries.remove(0); - process_subscription(app, sub, entry).await?; - - let entry_stream: Result<()> = stream::iter(entries) - .map(|(sub, entry)| process_subscription(app, sub, entry)) - .buffer_unordered(100) - .try_collect() - .await; - entry_stream?; - } - - Ok(()) - } - - async fn get_new_entries( - &self, - sub: &'a Subscription, - ) -> Result<Vec<(&'a Subscription, InfoJson)>> { - // ANSI ESCAPE CODES Wrappers {{{ - // see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands - const CSI: &str = "\x1b["; - fn clear_whole_line() { - eprint!("{CSI}2K"); - } - fn move_to_col(x: usize) { - eprint!("{CSI}{x}G"); - } - // fn hide_cursor() { - // eprint!("{CSI}?25l"); - // } - // fn show_cursor() { - // eprint!("{CSI}?25h"); - // } - // }}} - - let json = json! { - { - "playliststart": 1, - "playlistend": self.max_backlog, - "noplaylist": false, - "extractor_args": {"youtubetab": {"approximate_date": [""]}}, - } - }; - let yt_dlp_opts = json.as_object().expect("This is hardcoded"); - - if !log_enabled!(Level::Debug) { - clear_whole_line(); - move_to_col(1); - eprint!("Checking playlist {}...", sub.name); - move_to_col(1); - stderr().flush()?; - } - - let info = yt_dlp::extract_info(yt_dlp_opts, &sub.url, false, false) - .await - .with_context(|| format!("Failed to get playlist '{}'.", sub.name))?; - - let entries = info.entries.unwrap_or(vec![]); - let valid_entries: Vec<(&Subscription, InfoJson)> = entries - .into_iter() - .take(self.max_backlog) - .filter_map(|entry| -> Option<(&Subscription, InfoJson)> { - let id = entry.id.as_ref().expect("Should exist?"); - let extractor_hash = blake3::hash(id.as_bytes()); - if self.hashes.contains(&extractor_hash) { - debug!( - "Skipping entry, as it is already present: '{}'", - extractor_hash - ); - None - } else { - Some((sub, entry)) - } - }) - .collect(); - - let processed_entries = { - let base: Result<Vec<(&Subscription, InfoJson)>, YtDlpError> = - stream::iter(valid_entries) - .map(|(sub, entry)| async move { - match process_ie_result(yt_dlp_opts, entry, false).await { - Ok(output) => Ok((sub, output)), - Err(err) => Err(err), - } - }) - .buffer_unordered(100) - .try_collect() - .await; - match base { - Ok(ok) => ok, - Err(err) => { - if let YtDlpError::PythonError { error, kind } = &err { - if kind.as_str() == "<class 'yt_dlp.utils.DownloadError'>" - && error.to_string().as_str().contains( - "Join this channel to get access to members-only content ", - ) - { - vec![] - } else { - let error_string = error.to_string(); - let error = error_string - .strip_prefix("DownloadError: \u{1b}[0;31mERROR:\u{1b}[0m ") - .expect("This prefix should exists"); - error!("{error}"); - vec![] - } - } else { - Err(err).context("Failed to process new entries.")? - } - } - } - }; - - Ok(processed_entries) - } -} |