diff options
28 files changed, 1212 insertions, 308 deletions
diff --git a/.envrc b/.envrc index 5537ab5..bac1203 100644 --- a/.envrc +++ b/.envrc @@ -20,5 +20,6 @@ PATH_add ./target/release PATH_add ./target/profiling export DATABASE_URL="sqlite://$root/target/database.sqlx" -export PYO3_PYTHON=python3 -export YT_STORE_INFO_JSON=yes + +# Plugins are not supported. +export YTDLP_NO_PLUGINS=1 diff --git a/Cargo.lock b/Cargo.lock index 4a17021..2e7b2cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,12 @@ dependencies = [ ] [[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -288,7 +294,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" dependencies = [ "serde", ] @@ -424,6 +430,18 @@ dependencies = [ ] [[package]] +name = "clap_complete" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677" +dependencies = [ + "clap", + "clap_lex", + "is_executable", + "shlex", +] + +[[package]] name = "clap_derive" version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -716,6 +734,15 @@ dependencies = [ ] [[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] name = "endian-type" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -852,6 +879,12 @@ dependencies = [ ] [[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1046,6 +1079,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes 1.10.1", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] name = "half" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1127,6 +1179,124 @@ dependencies = [ ] [[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes 1.10.1", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes 1.10.1", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes 1.10.1", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes 1.10.1", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes 1.10.1", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "base64", + "bytes 1.10.1", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] name = "iana-time-zone" version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1288,6 +1458,22 @@ dependencies = [ ] [[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] name = "is-macro" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1311,6 +1497,15 @@ dependencies = [ ] [[package]] +name = "is_executable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +dependencies = [ + "winapi", +] + +[[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1507,7 +1702,7 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libmpv2" -version = "1.5.0" +version = "1.6.0" dependencies = [ "crossbeam", "libmpv2-sys", @@ -1517,7 +1712,7 @@ dependencies = [ [[package]] name = "libmpv2-sys" -version = "1.5.0" +version = "1.6.0" dependencies = [ "bindgen", ] @@ -1583,9 +1778,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lz4_flex" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" +checksum = "2c592ad9fbc1b7838633b3ae55ce69b17d01150c72fcef229fbb819d39ee51ee" dependencies = [ "twox-hash", ] @@ -1706,6 +1901,12 @@ dependencies = [ ] [[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1742,6 +1943,23 @@ dependencies = [ ] [[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] name = "nibble_vec" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1798,16 +2016,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" [[package]] -name = "nucleo-matcher" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" -dependencies = [ - "memchr", - "unicode-segmentation", -] - -[[package]] name = "num-bigint-dig" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2031,51 +2239,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "pest" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" -dependencies = [ - "memchr", - "thiserror 2.0.12", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - -[[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2347,6 +2510,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] +name = "reqwest" +version = "0.12.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +dependencies = [ + "base64", + "bytes 1.10.1", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] name = "result-like" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2368,6 +2573,20 @@ dependencies = [ ] [[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] name = "rsa" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2474,9 +2693,42 @@ dependencies = [ ] [[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] name = "rustpython" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "cfg-if", "dirs-next", @@ -2495,7 +2747,7 @@ dependencies = [ [[package]] name = "rustpython-codegen" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "ahash", "bitflags 2.9.1", @@ -2520,7 +2772,7 @@ dependencies = [ [[package]] name = "rustpython-common" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "ascii", "bitflags 2.9.1", @@ -2549,7 +2801,7 @@ dependencies = [ [[package]] name = "rustpython-compiler" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -2564,7 +2816,7 @@ dependencies = [ [[package]] name = "rustpython-compiler-core" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "bitflags 2.9.1", "itertools 0.14.0", @@ -2578,7 +2830,7 @@ dependencies = [ [[package]] name = "rustpython-compiler-source" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "ruff_source_file", "ruff_text_size", @@ -2587,7 +2839,7 @@ dependencies = [ [[package]] name = "rustpython-derive" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "proc-macro2", "rustpython-compiler", @@ -2598,7 +2850,7 @@ dependencies = [ [[package]] name = "rustpython-derive-impl" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "itertools 0.14.0", "maplit", @@ -2622,7 +2874,7 @@ dependencies = [ [[package]] name = "rustpython-literal" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "hexf-parse", "is-macro", @@ -2635,7 +2887,7 @@ dependencies = [ [[package]] name = "rustpython-pylib" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "glob", ] @@ -2643,7 +2895,7 @@ dependencies = [ [[package]] name = "rustpython-sre_engine" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "bitflags 2.9.1", "num_enum", @@ -2654,7 +2906,7 @@ dependencies = [ [[package]] name = "rustpython-stdlib" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "adler32", "ahash", @@ -2728,7 +2980,7 @@ dependencies = [ [[package]] name = "rustpython-vm" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "ahash", "ascii", @@ -2804,7 +3056,7 @@ dependencies = [ [[package]] name = "rustpython-wtf8" version = "0.4.0" -source = "git+https://github.com/RustPython/RustPython.git#4e094eaa55d95e55b22cb0f579f9551d21d379cd" +source = "git+https://github.com/RustPython/RustPython.git#c968fe0fd9d7466dc9d5d97e973b82ba35e516d8" dependencies = [ "ascii", "bstr", @@ -2903,6 +3155,29 @@ dependencies = [ ] [[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3031,12 +3306,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" @@ -3354,6 +3626,15 @@ dependencies = [ ] [[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3418,7 +3699,7 @@ dependencies = [ [[package]] name = "termsize" -version = "1.5.0" +version = "1.6.0" dependencies = [ "libc", "winapi", @@ -3539,6 +3820,26 @@ dependencies = [ ] [[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] name = "tokio-stream" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3550,6 +3851,21 @@ dependencies = [ ] [[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes 1.10.1", + "futures-core", + "futures-sink", + "futures-util", + "hashbrown", + "pin-project-lite", + "tokio", +] + +[[package]] name = "toml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3591,6 +3907,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes 1.10.1", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3623,24 +3984,16 @@ dependencies = [ ] [[package]] -name = "trinitry" -version = "0.2.2" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f814008587cd653ef1f92f9caf321e86a6f53899ec118fd50eaed55974863a40" -dependencies = [ - "pest", - "pest_derive", -] +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "twox-hash" -version = "1.6.3" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "static_assertions", -] +checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56" [[package]] name = "typenum" @@ -3655,12 +4008,6 @@ 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" @@ -3842,6 +4189,12 @@ dependencies = [ ] [[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3867,7 +4220,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uu_fmt" -version = "1.5.0" +version = "1.6.0" dependencies = [ "unicode-width", ] @@ -3912,6 +4265,15 @@ dependencies = [ ] [[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3959,6 +4321,19 @@ dependencies = [ ] [[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3991,6 +4366,16 @@ dependencies = [ ] [[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "which" version = "7.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4120,6 +4505,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] +name = "windows-registry" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4436,30 +4832,31 @@ dependencies = [ [[package]] name = "yt" -version = "1.5.0" +version = "1.6.0" dependencies = [ "anyhow", "blake3", - "bytes 1.5.0", + "bytes 1.6.0", "chrono", "chrono-humanize", "clap", + "clap_complete", "futures", "libmpv2", "log", "notify", - "nucleo-matcher", "owo-colors", "regex", "serde", "serde_json", + "shlex", "sqlx", "stderrlog", "tempfile", "termsize", "tokio", + "tokio-util", "toml", - "trinitry", "url", "uu_fmt", "xdg", @@ -4468,11 +4865,13 @@ dependencies = [ [[package]] name = "yt_dlp" -version = "1.5.0" +version = "1.6.0" dependencies = [ "indexmap", "log", + "reqwest", "rustpython", + "serde", "serde_json", "thiserror 2.0.12", "url", diff --git a/Cargo.toml b/Cargo.toml index 470eb58..fa282b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ members = [ [workspace.package] edition = "2024" -version = "1.5.0" +version = "1.6.0" rust-version = "1.85.0" authors = ["Benedikt Peetz <benedikt.peetz@b-peetz.de>"] repository = "https://git.vhack.eu/soispha/clients/yt" diff --git a/NEWS.md b/NEWS.md index 4e57d7b..b1bc980 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,6 +14,88 @@ 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.6.0](https://git.foss-syndicate.org/soispha/clients/yt/compare/07db485f9c5206fbcfe2a5f9db28a9587edc6d2b..v1.6.0) - 2025-06-16 +#### Bug Fixes +- **(libmpv2-sys)** Avoid generating comments, that confuse rustdoc - ([0c0e00d](https://git.foss-syndicate.org/soispha/clients/yt/commit/0c0e00da2c21c4b8325fa6145c808e9df0df0834)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(scripts/mkdb.sh)** Also use the `$DATABASE_URL` variable as source source - ([45e5500](https://git.foss-syndicate.org/soispha/clients/yt/commit/45e55007aa13b1ec24af4c543bc3b8699710301c)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/cli)** Remove duplicated short flag key (help also uses 'h') - ([c1122d6](https://git.foss-syndicate.org/soispha/clients/yt/commit/c1122d6ab31548aff9bf8aaa4a855a771355c8e9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/download/get_file_size)** Correct deal with `filesize_approx` = Null - ([680f811](https://git.foss-syndicate.org/soispha/clients/yt/commit/680f811adc83554cfbaff56d8b50501786a949e2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/downloader/progress_hook)** Silence clippy warnings - ([65ba5d7](https://git.foss-syndicate.org/soispha/clients/yt/commit/65ba5d738dcfeaecb398e246e0db5d7c4bf04b99)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/select/selection_file/duration)** Improve the duration parser - ([9e1c1ae](https://git.foss-syndicate.org/soispha/clients/yt/commit/9e1c1aec0548a6482e23ceac4e1265ef8baf8023)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/storage/migrate)** Correct the two to three migration script - ([7694496](https://git.foss-syndicate.org/soispha/clients/yt/commit/7694496efa621466e327b9c00fe1c5cc092ccc1f)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/storage/migrate)** Correctly state the upgrade to the topmost version - ([449c4c2](https://git.foss-syndicate.org/soispha/clients/yt/commit/449c4c26c91400e56e0e685958b825b3f02f4e40)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/storage/migrate)** Improve error messages - ([3a16edd](https://git.foss-syndicate.org/soispha/clients/yt/commit/3a16edde524f881c8955026350243a1b4d54d89b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/storage/migrate)** Account for the fact that DbVersions::Empty means no Version - ([d1f004c](https://git.foss-syndicate.org/soispha/clients/yt/commit/d1f004ce48caf90ab4f3ec1d0bbb588c9cbf0fe9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/storage/video_database/set)** Reset the `is_focused` flag - ([07db485](https://git.foss-syndicate.org/soispha/clients/yt/commit/07db485f9c5206fbcfe2a5f9db28a9587edc6d2b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/subscribe)** Deal with moved url value - ([fb00ecf](https://git.foss-syndicate.org/soispha/clients/yt/commit/fb00ecf745c1bd12e026faabf235a75c2c775a3a)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/update)** Also handle the newly introduced error conditions - ([a7e1a2d](https://git.foss-syndicate.org/soispha/clients/yt/commit/a7e1a2d7475fc1304ef7b33aa2f170f8232bd1d8)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/update)** Correct the progress display in `--grouped` mode - ([35f400c](https://git.foss-syndicate.org/soispha/clients/yt/commit/35f400cebca70325e7e999f15dcaa562dbc78f25)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/update)** Avoid printing all the subscriptions that are not updated - ([810c0d3](https://git.foss-syndicate.org/soispha/clients/yt/commit/810c0d3e75287c15e8baf210f89c807a21d3acee)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/update/video_entry_to_video)** Cast the json objects - ([b6a57c5](https://git.foss-syndicate.org/soispha/clients/yt/commit/b6a57c5cad1ee7df56dad5ccb2317f936e682bbe)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/version)** Use yt_dlp's native python version imply - ([22f74fc](https://git.foss-syndicate.org/soispha/clients/yt/commit/22f74fc43b004045d13b0184ae075dea0ebc8eda)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/watch/playlist)** Workaround terminals, that treat 0 as 1 - ([b3be18a](https://git.foss-syndicate.org/soispha/clients/yt/commit/b3be18a0bfb55135135c9769ac531c098ca4d26c)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/{se,}dowa)** Don't exit completely, if the downloader fails - ([b70dd45](https://git.foss-syndicate.org/soispha/clients/yt/commit/b70dd458615bbad99cf05dbde3dc831a9922ba21)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt_dlp)** Avoid writing the json output to disk - ([c8601d6](https://git.foss-syndicate.org/soispha/clients/yt/commit/c8601d67c2dd67ed3ae4465fbf3906fa2cf15a98)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt_dlp/json_{cast,get})** Improve error reporting - ([c4f8c14](https://git.foss-syndicate.org/soispha/clients/yt/commit/c4f8c14b5636055a2973afe0d5ef6494d97a1a76)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +#### Build system +- **(.envrc)** Also disable ytdlp plugins by default - ([1b8113a](https://git.foss-syndicate.org/soispha/clients/yt/commit/1b8113a72161e5d5f1f7a8328265f8075fc3491a)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(.envrc)** Remove outdated env variables - ([e51139d](https://git.foss-syndicate.org/soispha/clients/yt/commit/e51139da51bbb8725614356bd173d1d66af7f74f)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(cog.toml)** Use the correct remote url - ([4e2aeec](https://git.foss-syndicate.org/soispha/clients/yt/commit/4e2aeec877ec9083de5116bcca8da039389b9f09)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(cog.toml)** Use correct username - ([b9957a2](https://git.foss-syndicate.org/soispha/clients/yt/commit/b9957a2dc50b02f1df8bcb2dc3ddcc3c081b94d3)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(flake)** Document for what the `CLANG_*` env vars are needed - ([d03e537](https://git.foss-syndicate.org/soispha/clients/yt/commit/d03e5374a238dc2b701c259bbd5ade91c6b4a9ff)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(flake)** Remove `flake-utils` - ([d21e1ac](https://git.foss-syndicate.org/soispha/clients/yt/commit/d21e1ac26c5e57f7e5f9cb2fea937b807118187b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(flake)** Switch to `nixpkgs-unstable-small` - ([1a807d2](https://git.foss-syndicate.org/soispha/clients/yt/commit/1a807d25bd1a47fb81b538a1638514cedb928148)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(flake)** Adapt the dev env to yt_dlp's new dependencies - ([b8682b4](https://git.foss-syndicate.org/soispha/clients/yt/commit/b8682b478a3a2322a370cc8eabf46d20d00e8c37)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(flake)** Add `git-bug` to the devshell - ([ebcd3e1](https://git.foss-syndicate.org/soispha/clients/yt/commit/ebcd3e153e01bd1432b583b2a09569ba2017b8ed)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(package/package.nix)** Update to the new build requirements - ([f590cef](https://git.foss-syndicate.org/soispha/clients/yt/commit/f590cef92da3931fae1607c6964b8125ab2f6307)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(treewide)** Update - ([2380d7d](https://git.foss-syndicate.org/soispha/clients/yt/commit/2380d7d7fdfdda91c26e8027f41aa6788f3590e0)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(treewide)** Update - ([0791777](https://git.foss-syndicate.org/soispha/clients/yt/commit/0791777665fe99d02b5e4aaaa43ca3483712dac9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(treewide)** Update - ([fa79bd7](https://git.foss-syndicate.org/soispha/clients/yt/commit/fa79bd7eef3824ad208984df9cc7784bdab5ba2b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(treewide)** Update - ([47754f5](https://git.foss-syndicate.org/soispha/clients/yt/commit/47754f54b978e7ed66ccd29c866fabe28607997e)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **({flake,Cargo}.lock)** Update - ([9c7dfa7](https://git.foss-syndicate.org/soispha/clients/yt/commit/9c7dfa7a8ca71bd5067741917a6f96061290976b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +#### Documentation +- **(yt/update)** Add comment about the `unsmuggle_url` invocation - ([6c47d93](https://git.foss-syndicate.org/soispha/clients/yt/commit/6c47d93c983b8807032220e107ac2f686abb14e2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/watch/playlist_handler/client_messages)** Add TODO about `current_exe` - ([13a0621](https://git.foss-syndicate.org/soispha/clients/yt/commit/13a062150e4efaf4b87d9213cf68b5a4eabb0235)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt_dlp)** Fix typo in `Cargo.toml`'s description - ([848270e](https://git.foss-syndicate.org/soispha/clients/yt/commit/848270ed0d9ed0409fe563a130e2913d9dfcc897)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +#### Features +- **(yt/cli)** Add support for command line completions - ([e635ee7](https://git.foss-syndicate.org/soispha/clients/yt/commit/e635ee79a4ec0d30dca271cc269fee40150ea821)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/select)** Support a directory selection process - ([e2d5dc6](https://git.foss-syndicate.org/soispha/clients/yt/commit/e2d5dc6a9f000a875c3f2a100f660adc2a43275a)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/select/split)** Make sorting configurable - ([8b644e4](https://git.foss-syndicate.org/soispha/clients/yt/commit/8b644e4e0e058a003984c02d48e829de437145c6)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/status)** Show the percentage of videos that were actually watched - ([ec4e0c9](https://git.foss-syndicate.org/soispha/clients/yt/commit/ec4e0c91d33b2a8c11b71d4cdb1edeaa44ce8247)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/storage/videos)** Validate in DB, that is_focused is UNIQUE - ([cf16b93](https://git.foss-syndicate.org/soispha/clients/yt/commit/cf16b93b563daee88b3bda4b30666b1b0766a8b0)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/update)** Print a nice progress number - ([c04d530](https://git.foss-syndicate.org/soispha/clients/yt/commit/c04d530a1a9e09dd26adc4116959e5481b970bc6)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/update)** Support grouped updates - ([8a42c83](https://git.foss-syndicate.org/soispha/clients/yt/commit/8a42c835a0dd1fcaa3475938d9442199d57acf75)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/update)** Specify subscriptions to update as positional args - ([51bbd90](https://git.foss-syndicate.org/soispha/clients/yt/commit/51bbd90ab1f08c9056c4e5799e3abba568ae75c9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/videos/list)** Replace the nucleo matcher with a simple `contains` - ([c0a3b61](https://git.foss-syndicate.org/soispha/clients/yt/commit/c0a3b61fb344a5ca86cae1c31d2e42fbe56b6726)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt_dlp)** Support a DeArrow post processor - ([ab61a4e](https://git.foss-syndicate.org/soispha/clients/yt/commit/ab61a4e47a955dd4a5dabeef3ade1b85f6576b84)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **({yt/update,yt_dlp})** Use yt_dlp errors again - ([078dfa0](https://git.foss-syndicate.org/soispha/clients/yt/commit/078dfa09a40a384b5cb8cf8cffd9b68cc9678556)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **({yt_dlp,yt})** Migrate from pyo3 to rustpython - ([69145b4](https://git.foss-syndicate.org/soispha/clients/yt/commit/69145b4deed4fe512239a9f88e6af69d3b8c0309)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +#### Miscellaneous Chores +- **(treewide)** Add missing copyright headers - ([fd029a6](https://git.foss-syndicate.org/soispha/clients/yt/commit/fd029a65d43e1eb935b470b88893c16c30c19746)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(treewide)** Migrate to rust edition 2024 - ([8be7171](https://git.foss-syndicate.org/soispha/clients/yt/commit/8be717167ed77f5a1021fa0825b386674c5c1a39)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt_dlp/wrappers/info_json)** Add additional missing field - ([8ef4cf9](https://git.foss-syndicate.org/soispha/clients/yt/commit/8ef4cf92635003fb79263d22126289d788e34633)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +#### Performance Improvements +- **(yt/update/updater)** Acknowledge, that `yt_dlp` has a sync API - ([ab56f25](https://git.foss-syndicate.org/soispha/clients/yt/commit/ab56f2550d5086ccd1c6981b62081b70743a1f2c)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +#### Refactoring +- **(yt)** Move to `crates/yt` - ([394d4f7](https://git.foss-syndicate.org/soispha/clients/yt/commit/394d4f7d105dadd7b516f198b0d6a9dda2d3f1a5)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt)** Consolidate the multiple ANSI escape code wrapper functions - ([efc35b5](https://git.foss-syndicate.org/soispha/clients/yt/commit/efc35b5bd76bf4e4aab6750ead45713a79e851f9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/select)** Split the `select::select` function up - ([fb49841](https://git.foss-syndicate.org/soispha/clients/yt/commit/fb49841e1ec14b3ab2de981e439d4f10f5494cf5)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/select/selection_file)** Migrate from `trinitry` to `shlex` - ([56011be](https://git.foss-syndicate.org/soispha/clients/yt/commit/56011be94c09828b104008cb7bf3a19177bc1631)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/storage/migrate)** Factor out duplicated code into macro - ([137339d](https://git.foss-syndicate.org/soispha/clients/yt/commit/137339d1d2924da764c54517fcc6d5d11d46a69d)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/storage/migrate/sql)** Use predictable SQL paths - ([420f9c8](https://git.foss-syndicate.org/soispha/clients/yt/commit/420f9c87abe3a3480a2345cbad5ec427636b2cb5)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt_dlp)** Remove the unneeded `async` from the public functions - ([5b5caee](https://git.foss-syndicate.org/soispha/clients/yt/commit/5b5caee512dd82bc5106e69259ba916cd143deda)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt_dlp/lib)** De-duplicate the info json sanitize code - ([e46ab9b](https://git.foss-syndicate.org/soispha/clients/yt/commit/e46ab9bc8bd4ecc35363e27aea9b5445bc858b2d)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt_dlp/lib)** Explicitly convert python exceptions into an error - ([ada9550](https://git.foss-syndicate.org/soispha/clients/yt/commit/ada9550b02ee13a8378bd2ee27d536b83eec4820)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +#### Style +- **(treewide)** Reformat - ([10b07fa](https://git.foss-syndicate.org/soispha/clients/yt/commit/10b07fa5a4f4080ef5417720b2d15179b72d2fc2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(treewide)** Adopt rust edition 2024 rustfmt style - ([a78b66e](https://git.foss-syndicate.org/soispha/clients/yt/commit/a78b66ed784cd6f2f97771d9e170c8f8558140b8)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/cli)** Sort the toplevel flags alphabetically - ([77ea1d8](https://git.foss-syndicate.org/soispha/clients/yt/commit/77ea1d8223b57567b448fb973b7240adaab61778)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +- **(yt/select)** Apply clippy's suggestions - ([b4ee42a](https://git.foss-syndicate.org/soispha/clients/yt/commit/b4ee42a62c683632c589f39e6ceac0b48d222e87)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) +#### Tests +- **(yt/cli)** Test the CLI - ([10f9d8b](https://git.foss-syndicate.org/soispha/clients/yt/commit/10f9d8bfd0c84146638cfdaf6b076493f943e650)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz) + +- - - + ## [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) diff --git a/cog.toml b/cog.toml index 7cc3ac4..781175b 100644 --- a/cog.toml +++ b/cog.toml @@ -29,7 +29,7 @@ post_bump_hooks = [ [changelog] path = "NEWS.md" template = "remote" -remote = "git.vhack.eu" +remote = "git.foss-syndicate.org" repository = "clients/yt" owner = "soispha" -authors = [{ signature = "Benedikt Peetz", username = "soispha" }] +authors = [{ signature = "Benedikt Peetz", username = "bpeetz" }] diff --git a/crates/libmpv2/CHANGELOG.md b/crates/libmpv2/CHANGELOG.md index dc6f861..a3d14d7 100644 --- a/crates/libmpv2/CHANGELOG.md +++ b/crates/libmpv2/CHANGELOG.md @@ -16,7 +16,7 @@ If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. ## Version 3.0.0 -- \[breaking\] Support libmpv version 2.0 (mpv version 0.35.0). Mpv versions \<= +- [breaking] Support libmpv version 2.0 (mpv version 0.35.0). Mpv versions \<= 0.34.0 will no longer be supported. - Add OpenGL rendering @@ -29,10 +29,10 @@ If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. ## Version 2.0.0 - Add method `Mpv::with_initializer` to set options before initialization -- \[breaking\] Borrow `&mut self` in `wait_event` to disallow using two events +- [breaking] Borrow `&mut self` in `wait_event` to disallow using two events where the first points to data freed in the second `wait_event` call -- \[breaking\] `PropertyData<'_>` is no longer `Clone` or `PartialEq`, - `Event<'_>` is no longer `Clone` to avoid cloning/comparing `MpvNode` +- [breaking] `PropertyData<'_>` is no longer `Clone` or `PartialEq`, `Event<'_>` + is no longer `Clone` to avoid cloning/comparing `MpvNode` ## Version 1.1.0 diff --git a/crates/libmpv2/libmpv2-sys/build.rs b/crates/libmpv2/libmpv2-sys/build.rs index bf9a02e..45c2450 100644 --- a/crates/libmpv2/libmpv2-sys/build.rs +++ b/crates/libmpv2/libmpv2-sys/build.rs @@ -30,7 +30,9 @@ fn main() { ), "--verbose", ]) - .generate_comments(true) + // NOTE(@bpeetz): The comments are interpreted as doc-tests, + // which obviously fail, as the code is c. <2025-06-16> + .generate_comments(false) .generate() .expect("Unable to generate bindings"); diff --git a/crates/yt/Cargo.toml b/crates/yt/Cargo.toml index 6803e68..c6d8c30 100644 --- a/crates/yt/Cargo.toml +++ b/crates/yt/Cargo.toml @@ -29,16 +29,16 @@ blake3 = "1.8.2" chrono = { version = "0.4.41", features = ["now"] } chrono-humanize = "0.2.3" clap = { version = "4.5.40", features = ["derive"] } +clap_complete = { version = "4.5.54", features = ["unstable-dynamic"] } futures = "0.3.31" -nucleo-matcher = "0.3.1" owo-colors = "4.2.1" regex = "1.11.1" sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] } stderrlog = "0.6.0" tempfile = "3.20.0" toml = "0.8.23" -trinitry = { version = "0.2.2" } xdg = "3.0.0" +shlex = "1.3.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 index ae1805d..462a126 100644 --- a/crates/yt/src/ansi_escape_codes.rs +++ b/crates/yt/src/ansi_escape_codes.rs @@ -1,3 +1,13 @@ +// 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>. + // see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands const CSI: &str = "\x1b["; pub fn erase_in_display_from_cursor() { diff --git a/crates/yt/src/cli.rs b/crates/yt/src/cli.rs index 634e422..41fadf4 100644 --- a/crates/yt/src/cli.rs +++ b/crates/yt/src/cli.rs @@ -9,12 +9,16 @@ // 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::{path::PathBuf, str::FromStr}; +use std::{ + fmt::{self, Display, Formatter}, + path::PathBuf, + str::FromStr, +}; use anyhow::Context; use bytes::Bytes; use chrono::NaiveDate; -use clap::{ArgAction, Args, Parser, Subcommand}; +use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum}; use url::Url; use crate::{ @@ -294,6 +298,43 @@ impl FromStr for OptionalPublisher { } } +#[derive(Default, ValueEnum, Clone, Copy, Debug)] +pub enum SelectSplitSortKey { + /// Sort by the name of the publisher. + #[default] + Publisher, + + /// Sort by the number of unselected videos per publisher. + Videos, +} +impl Display for SelectSplitSortKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + SelectSplitSortKey::Publisher => f.write_str("publisher"), + SelectSplitSortKey::Videos => f.write_str("videos"), + } + } +} + +#[derive(Default, ValueEnum, Clone, Copy, Debug)] +pub enum SelectSplitSortMode { + /// Sort in ascending order (small -> big) + #[default] + Asc, + + /// Sort in descending order (big -> small) + Desc, +} + +impl Display for SelectSplitSortMode { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + SelectSplitSortMode::Asc => f.write_str("asc"), + SelectSplitSortMode::Desc => f.write_str("desc"), + } + } +} + #[derive(Subcommand, Clone, Debug)] // NOTE: Keep this in sync with the [`constants::HELP_STR`] constant. <2024-08-20> // NOTE: Also keep this in sync with the `tree-sitter-yts/grammar.js`. <2024-11-04> @@ -304,15 +345,26 @@ pub enum SelectCommand { #[arg(long, short)] done: bool, - /// Generate a directory, where each file contains only one subscription. - #[arg(long, short, conflicts_with = "use_last_selection")] - split: bool, - /// Use the last selection file (useful if you've spend time on it and want to get it again) #[arg(long, short, conflicts_with = "done")] use_last_selection: bool, }, + /// Generate a directory, where each file contains only one subscription. + Split { + /// Include done (watched, dropped) videos + #[arg(long, short)] + done: bool, + + /// Which key to use for sorting. + #[arg(default_value_t)] + sort_key: SelectSplitSortKey, + + /// Which mode to use for sorting. + #[arg(default_value_t)] + sort_mode: SelectSplitSortMode, + }, + /// Add a video to the database /// /// This optionally supports to add a playlist. @@ -371,7 +423,6 @@ impl Default for SelectCommand { Self::File { done: false, use_last_selection: false, - split: false, } } } @@ -381,7 +432,7 @@ pub enum CacheCommand { /// Invalidate all cache entries Invalidate { /// Also delete the cache path - #[arg(short, long)] + #[arg(short = 'f', long)] hard: bool, }, @@ -396,3 +447,14 @@ pub enum CacheCommand { all: bool, }, } + +#[cfg(test)] +mod test { + use clap::CommandFactory; + + use super::CliArgs; + #[test] + fn verify_cli() { + CliArgs::command().debug_assert(); + } +} diff --git a/crates/yt/src/download/mod.rs b/crates/yt/src/download/mod.rs index 110bf55..6065cf9 100644 --- a/crates/yt/src/download/mod.rs +++ b/crates/yt/src/download/mod.rs @@ -311,8 +311,11 @@ impl Downloader { 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 let Some(serde_json::Value::Number(num)) = result.get("filesize_approx") { + // NOTE(@bpeetz): yt_dlp sets this value to `Null`, instead of omitting it when it + // can't calculate the approximate filesize. + // Thus, we have to check, that it is actually non-null, before we cast it. <2025-06-15> + json_cast!(num, 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 = json_get!(result, "duration", as_f64).ceil() as u64; @@ -347,7 +350,7 @@ impl Downloader { let yt_dlp = download_opts(app, &addional_opts)?; let result = yt_dlp - .download(&[video.url.to_owned()]) + .download(&[video.url.clone()]) .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 index b75ec00..c507165 100644 --- a/crates/yt/src/download/progress_hook.rs +++ b/crates/yt/src/download/progress_hook.rs @@ -1,3 +1,13 @@ +// 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}, process, diff --git a/crates/yt/src/main.rs b/crates/yt/src/main.rs index 930d269..744fe5f 100644 --- a/crates/yt/src/main.rs +++ b/crates/yt/src/main.rs @@ -19,7 +19,7 @@ use anyhow::{Context, Result, bail}; use app::App; use bytes::Bytes; use cache::{invalidate, maintain}; -use clap::Parser; +use clap::{CommandFactory, Parser}; use cli::{CacheCommand, SelectCommand, SubscriptionCommand, VideosCommand}; use config::Config; use log::{error, info}; @@ -56,6 +56,8 @@ pub mod watch; // This is _the_ main function after all. It is not really good, but it sort of works. #[allow(clippy::too_many_lines)] async fn main() -> Result<()> { + clap_complete::CompleteEnv::with_factory(cli::CliArgs::command).complete(); + let args = cli::CliArgs::parse(); // The default verbosity is 1 (Warn) @@ -115,15 +117,12 @@ async fn main() -> Result<()> { SelectCommand::File { done, use_last_selection, - split, - } => { - if split { - assert!(!use_last_selection); - Box::pin(select::select_split(&app, done)).await? - } else { - Box::pin(select::select_file(&app, done, use_last_selection)).await? - } - } + } => Box::pin(select::select_file(&app, done, use_last_selection)).await?, + SelectCommand::Split { + done, + sort_key, + sort_mode, + } => Box::pin(select::select_split(&app, done, sort_key, sort_mode)).await?, _ => Box::pin(handle_select_cmd(&app, cmd, None)).await?, } } @@ -219,7 +218,14 @@ async fn main() -> Result<()> { current_progress += CHUNK_SIZE; } } else { - update::update(&app, max_backlog, subscriptions, total_number, current_progress).await?; + update::update( + &app, + max_backlog, + subscriptions, + total_number, + current_progress, + ) + .await?; } } Command::Subscriptions { cmd } => match cmd { diff --git a/crates/yt/src/select/cmds/mod.rs b/crates/yt/src/select/cmds/mod.rs index aabcd3d..9da795a 100644 --- a/crates/yt/src/select/cmds/mod.rs +++ b/crates/yt/src/select/cmds/mod.rs @@ -76,7 +76,9 @@ pub async fn handle_select_cmd( firefox.arg(url.as_str()); let _handle = firefox.spawn().context("Failed to run firefox")?; } - SelectCommand::File { .. } => unreachable!("This should have been filtered out"), + SelectCommand::File { .. } | SelectCommand::Split { .. } => { + unreachable!("This should have been filtered out") + } } Ok(()) } diff --git a/crates/yt/src/select/mod.rs b/crates/yt/src/select/mod.rs index 668ab02..135bd76 100644 --- a/crates/yt/src/select/mod.rs +++ b/crates/yt/src/select/mod.rs @@ -21,7 +21,7 @@ use std::{ use crate::{ app::App, - cli::CliArgs, + cli::{CliArgs, SelectSplitSortKey, SelectSplitSortMode}, constants::HELP_STR, storage::video_database::{Video, VideoStatusMarker, get}, unreachable::Unreachable, @@ -39,7 +39,12 @@ use tokio::process::Command; pub mod cmds; pub mod selection_file; -pub async fn select_split(app: &App, done: bool) -> Result<()> { +pub async fn select_split( + app: &App, + done: bool, + sort_key: SelectSplitSortKey, + sort_mode: SelectSplitSortMode, +) -> Result<()> { let temp_dir = Builder::new() .prefix("yt_video_select-") .rand_bytes(6) @@ -69,8 +74,24 @@ pub async fn select_split(app: &App, done: bool) -> Result<()> { let author_map = { let mut temp_vec: Vec<_> = author_map.into_iter().collect(); - // PERFORMANCE: The clone here should not be neeed. <2025-06-15> - temp_vec.sort_by_key(|(name, _)| name.to_owned()); + match sort_key { + SelectSplitSortKey::Publisher => { + // PERFORMANCE: The clone here should not be neeed. <2025-06-15> + temp_vec.sort_by_key(|(name, _): &(String, Vec<Video>)| name.to_owned()); + } + SelectSplitSortKey::Videos => { + temp_vec.sort_by_key(|(_, videos): &(String, Vec<Video>)| videos.len()); + } + } + + match sort_mode { + SelectSplitSortMode::Asc => { + // Std's default mode is ascending. + } + SelectSplitSortMode::Desc => { + temp_vec.reverse(); + } + } temp_vec }; @@ -243,7 +264,7 @@ async fn process_file(app: &App, file: &File, processed: i64) -> Result<i64> { } } - Ok(line_number * -1) + Ok(-line_number) } async fn open_editor_at(path: &Path) -> Result<()> { diff --git a/crates/yt/src/select/selection_file/mod.rs b/crates/yt/src/select/selection_file/mod.rs index abd26c4..f5e0531 100644 --- a/crates/yt/src/select/selection_file/mod.rs +++ b/crates/yt/src/select/selection_file/mod.rs @@ -11,22 +11,32 @@ //! The data structures needed to express the file, which the user edits -use anyhow::{Context, Result}; -use trinitry::Trinitry; +use anyhow::{Result, bail}; +use shlex::Shlex; pub mod duration; +/// # Panics +/// If internal assertions fail. pub fn process_line(line: &str) -> Result<Option<Vec<String>>> { // Filter out comments and empty lines if line.starts_with('#') || line.trim().is_empty() { Ok(None) } else { - let tri = Trinitry::new(line).with_context(|| format!("Failed to parse line '{line}'"))?; + let split: Vec<_> = { + let mut shl = Shlex::new(line); + let res = shl.by_ref().collect(); - let mut vec = Vec::with_capacity(tri.arguments().len() + 1); - vec.push(tri.command().to_owned()); - vec.extend(tri.arguments().to_vec()); + if shl.had_error { + bail!("Failed to parse line '{line}'") + } - Ok(Some(vec)) + assert_eq!(shl.line_no, 1, "A unexpected newline appeared"); + res + }; + + assert!(!split.is_empty()); + + Ok(Some(split)) } } diff --git a/crates/yt/src/status/mod.rs b/crates/yt/src/status/mod.rs index 18bef7d..6883802 100644 --- a/crates/yt/src/status/mod.rs +++ b/crates/yt/src/status/mod.rs @@ -92,7 +92,8 @@ pub async fn show(app: &App) -> Result<()> { 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)); + let count = + to_f64(watched_videos_len) / (to_f64(drop_videos_len) + to_f64(dropped_videos_len)); count * 100.0 }; diff --git a/crates/yt/src/update/updater.rs b/crates/yt/src/update/updater.rs index 04bcaa1..60e9855 100644 --- a/crates/yt/src/update/updater.rs +++ b/crates/yt/src/update/updater.rs @@ -19,7 +19,7 @@ use futures::{StreamExt, future::join_all, stream}; use log::{Level, debug, error, log_enabled}; use serde_json::json; use tokio_util::task::LocalPoolHandle; -use yt_dlp::{InfoJson, YoutubeDLOptions, json_cast, json_get, process_ie_result}; +use yt_dlp::{InfoJson, PythonError, YoutubeDLOptions, json_cast, json_get, process_ie_result}; use crate::{ ansi_escape_codes::{clear_whole_line, move_to_col}, @@ -160,24 +160,28 @@ impl Updater { } }) // Don't fail the whole update, if one of the entries fails to fetch. - .filter_map(|base| match base { + .filter_map(move |base| match base { Ok(ok) => Some(ok), Err(err) => { - let process_ie_result::Error::Python(err) = &err; - - if err.contains( - "Join this channel to get access to members-only content ", - ) { - // Hide this error - } else { - // Show the error, but don't fail. - let error = err - .strip_prefix("DownloadError: \u{1b}[0;31mERROR:\u{1b}[0m ") - .unwrap_or(err); - error!("{error}"); + match err { + process_ie_result::Error::Python(PythonError(err)) => { + if err.contains( "Join this channel to get access to members-only content ",) { + // Hide this error + } else { + // Show the error, but don't fail. + let error = err + .strip_prefix("DownloadError: \u{1b}[0;31mERROR:\u{1b}[0m ") + .unwrap_or(&err); + error!("While fetching {:#?}: {error}", sub.name); + } + + None + } + process_ie_result::Error::InfoJsonPrepare(error) => { + error!("While fetching {:#?}: Failed to prepare info json: {error}", sub.name); + None + }, } - - None } })) } diff --git a/crates/yt/src/videos/mod.rs b/crates/yt/src/videos/mod.rs index e821772..960340b 100644 --- a/crates/yt/src/videos/mod.rs +++ b/crates/yt/src/videos/mod.rs @@ -11,10 +11,6 @@ use anyhow::Result; use futures::{TryStreamExt, stream::FuturesUnordered}; -use nucleo_matcher::{ - Matcher, - pattern::{CaseMatching, Normalization, Pattern}, -}; pub mod display; @@ -46,19 +42,10 @@ pub async fn query(app: &App, limit: Option<usize>, search_query: Option<String> .await?; if let Some(query) = search_query { - let mut matcher = Matcher::new(nucleo_matcher::Config::DEFAULT.match_paths()); - - let pattern_matches = Pattern::parse( - &query.replace(' ', "\\ "), - CaseMatching::Ignore, - Normalization::Smart, - ) - .match_list(all_video_strings, &mut matcher); - - pattern_matches - .iter() - .rev() - .for_each(|(val, key)| println!("{val} ({key})")); + all_video_strings + .into_iter() + .filter(|video| video.to_lowercase().contains(&query.to_lowercase())) + .for_each(|video| println!("{video}")); } else { println!("{}", all_video_strings.join("\n")); } diff --git a/crates/yt_dlp/Cargo.toml b/crates/yt_dlp/Cargo.toml index 90f2e10..81e1412 100644 --- a/crates/yt_dlp/Cargo.toml +++ b/crates/yt_dlp/Cargo.toml @@ -24,7 +24,15 @@ publish = true [dependencies] indexmap = { version = "2.9.0", default-features = false } log.workspace = true -rustpython = { git = "https://github.com/RustPython/RustPython.git", features = ["threading", "stdlib", "stdio", "importlib", "ssl"], default-features = false } +reqwest = { version = "0.12.20", features = ["blocking", "json"] } +rustpython = { git = "https://github.com/RustPython/RustPython.git", features = [ + "threading", + "stdlib", + "stdio", + "importlib", + "ssl", +], default-features = false } +serde = { workspace = true, features = ["derive"] } serde_json.workspace = true thiserror = "2.0.12" url.workspace = true diff --git a/crates/yt_dlp/README.md b/crates/yt_dlp/README.md index 591ef2e..ece8540 100644 --- a/crates/yt_dlp/README.md +++ b/crates/yt_dlp/README.md @@ -12,7 +12,7 @@ If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. # Yt_py -> \[can be empty\] +> [can be empty] Some text about the project. diff --git a/crates/yt_dlp/src/lib.rs b/crates/yt_dlp/src/lib.rs index dd42fc6..e7b37c6 100644 --- a/crates/yt_dlp/src/lib.rs +++ b/crates/yt_dlp/src/lib.rs @@ -1,10 +1,21 @@ +// 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>. + //! The `yt_dlp` interface is completely contained in the [`YoutubeDL`] structure. -use std::{self, env, mem, path::PathBuf}; +use std::{self, env, fmt::Display, path::PathBuf}; use indexmap::IndexMap; use log::{Level, debug, error, info, log_enabled}; use logging::setup_logging; +use post_processors::PostProcessor; use rustpython::{ InterpreterConfig, vm::{ @@ -18,23 +29,42 @@ use rustpython::{ use url::Url; mod logging; +pub mod post_processors; pub mod progress_hook; #[macro_export] macro_rules! json_get { - ($value:expr, $name:literal, $into:ident) => { - $crate::json_cast!($value.get($name).expect("Should exist"), $into) - }; + ($value:expr, $name:literal, $into:ident) => {{ + match $value.get($name) { + Some(val) => $crate::json_cast!(val, $into), + None => panic!( + concat!( + "Expected '", + $name, + "' to be a key for the'", + stringify!($value), + "' object: {:#?}" + ), + $value + ), + } + }}; } #[macro_export] macro_rules! json_cast { - ($value:expr, $into:ident) => { - $value.$into().expect(concat!( - "Should be able to cast value into ", - stringify!($into) - )) - }; + ($value:expr, $into:ident) => {{ + match $value.$into() { + Some(result) => result, + None => panic!( + concat!( + "Expected to be able to cast value ({:#?}) ", + stringify!($into) + ), + $value + ), + } + }}; } /// The core of the `yt_dlp` interface. @@ -43,6 +73,7 @@ pub struct YoutubeDL { youtube_dl_class: PyObjectRef, yt_dlp_module: PyObjectRef, options: serde_json::Map<String, serde_json::Value>, + post_processors: Vec<Box<dyn PostProcessor>>, } impl std::fmt::Debug for YoutubeDL { @@ -60,7 +91,7 @@ impl YoutubeDL { /// /// # Errors /// If a python call fails. - pub fn from_options(mut options: YoutubeDLOptions) -> Result<Self, build::Error> { + pub fn from_options(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(':') { @@ -92,9 +123,8 @@ impl YoutubeDL { let yt_dlp_module = vm.import("yt_dlp", 0)?; let class = yt_dlp_module.get_attr("YoutubeDL", vm)?; - let maybe_hook = mem::take(&mut options.progress_hook); - let opts = options.into_py_dict(vm); - if let Some(function) = maybe_hook { + let opts = json_loads(options.options, vm); + if let Some(function) = options.progress_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]) @@ -192,6 +222,7 @@ impl YoutubeDL { youtube_dl_class, yt_dlp_module, options: output_options, + post_processors: options.post_processors, }) } @@ -267,7 +298,7 @@ impl YoutubeDL { download: bool, process: bool, ) -> Result<InfoJson, extract_info::Error> { - match self.interpreter.enter(|vm| { + self.interpreter.enter(|vm| { let pos_args = PosArgs::new(vec![vm.new_pyobj(url.to_string())]); let kw_args = KwArgs::new({ @@ -279,9 +310,13 @@ impl YoutubeDL { let fun_args = FuncArgs::new(pos_args, kw_args); - let inner = self.youtube_dl_class.get_attr("extract_info", vm)?; + let inner = self + .youtube_dl_class + .get_attr("extract_info", vm) + .map_err(|exc| PythonError::from_exception(vm, &exc))?; let result = inner - .call_with_args(fun_args, vm)? + .call_with_args(fun_args, vm) + .map_err(|exc| PythonError::from_exception(vm, &exc))? .downcast::<PyDict>() .expect("This is a dict"); @@ -295,7 +330,9 @@ impl YoutubeDL { }); let mut out = vec![]; - let next = generator.get_attr("__next__", vm)?; + let next = generator + .get_attr("__next__", vm) + .map_err(|exc| PythonError::from_exception(vm, &exc))?; while let Ok(output) = next.call((), vm) { out.push(output); @@ -303,27 +340,16 @@ impl YoutubeDL { break; } } - result.set_item("entries", vm.new_pyobj(out), vm)?; + result + .set_item("entries", vm.new_pyobj(out), vm) + .map_err(|exc| PythonError::from_exception(vm, &exc))?; } } - 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_json = json_dumps(result, vm); + let result = self.prepare_info_json(result, vm)?; - Ok::<_, PyRef<PyBaseException>>(result_json) - }) { - Ok(ok) => Ok(ok), - Err(err) => self.interpreter.enter(|vm| { - let buffer = process_exception(vm, &err); - Err(extract_info::Error::Python(buffer)) - }), - } + Ok(result) + }) } /// Take the (potentially modified) result of the information extractor (i.e., @@ -344,7 +370,7 @@ impl YoutubeDL { ie_result: InfoJson, download: bool, ) -> Result<InfoJson, process_ie_result::Error> { - match self.interpreter.enter(|vm| { + self.interpreter.enter(|vm| { let pos_args = PosArgs::new(vec![vm.new_pyobj(json_loads(ie_result, vm))]); let kw_args = KwArgs::new({ @@ -355,46 +381,109 @@ impl YoutubeDL { let fun_args = FuncArgs::new(pos_args, kw_args); - let inner = self.youtube_dl_class.get_attr("process_ie_result", vm)?; + let inner = self + .youtube_dl_class + .get_attr("process_ie_result", vm) + .map_err(|exc| PythonError::from_exception(vm, &exc))?; let result = inner - .call_with_args(fun_args, vm)? + .call_with_args(fun_args, vm) + .map_err(|exc| PythonError::from_exception(vm, &exc))? .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)?; + let result = self.prepare_info_json(result, vm)?; - value.downcast::<PyDict>().expect("This should stay a dict") - }; + Ok(result) + }) + } - let result_json = json_dumps(result, vm); + fn prepare_info_json( + &self, + info: PyRef<PyDict>, + vm: &VirtualMachine, + ) -> Result<InfoJson, prepare::Error> { + let sanitize = self + .youtube_dl_class + .get_attr("sanitize_info", vm) + .map_err(|exc| PythonError::from_exception(vm, &exc))?; - Ok::<_, PyRef<PyBaseException>>(result_json) - }) { - Ok(ok) => Ok(ok), - Err(err) => self.interpreter.enter(|vm| { - let buffer = process_exception(vm, &err); - Err(process_ie_result::Error::Python(buffer)) - }), + let value = sanitize + .call((info,), vm) + .map_err(|exc| PythonError::from_exception(vm, &exc))?; + + let result = value.downcast::<PyDict>().expect("This should stay a dict"); + + let mut json = json_dumps(result, vm); + + for pp in &self.post_processors { + if pp + .extractors() + .iter() + .any(|extractor| *extractor == json_get!(json, "extractor_key", as_str)) + { + json = pp.process(json)?; + } else { + error!("Extractor not found for {pp:#?}"); + } } + + Ok(json) + } +} + +#[derive(thiserror::Error, Debug)] +pub struct PythonError(pub String); + +impl Display for PythonError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Python threw an exception: {}", self.0) + } +} + +impl PythonError { + fn from_exception(vm: &VirtualMachine, exc: &PyRef<PyBaseException>) -> Self { + let buffer = process_exception(vm, exc); + Self(buffer) } } #[allow(missing_docs)] pub mod process_ie_result { + use crate::{PythonError, prepare}; + #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Python threw an exception: {0}")] - Python(String), + #[error(transparent)] + Python(#[from] PythonError), + + #[error("Failed to prepare the info json")] + InfoJsonPrepare(#[from] prepare::Error), } } #[allow(missing_docs)] pub mod extract_info { + use crate::{PythonError, prepare}; + #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Python threw an exception: {0}")] - Python(String), + #[error(transparent)] + Python(#[from] PythonError), + + #[error("Failed to prepare the info json")] + InfoJsonPrepare(#[from] prepare::Error), + } +} +#[allow(missing_docs)] +pub mod prepare { + use crate::{PythonError, post_processors}; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + Python(#[from] PythonError), + + #[error("Failed to run a post processor")] + PostProcessorRun(#[from] post_processors::Error), } } @@ -410,15 +499,19 @@ pub type ProgressHookFunction = fn(input: FuncArgs, vm: &VirtualMachine); pub struct YoutubeDLOptions { options: serde_json::Map<String, serde_json::Value>, progress_hook: Option<ProgressHookFunction>, + post_processors: Vec<Box<dyn PostProcessor>>, } impl YoutubeDLOptions { #[must_use] pub fn new() -> Self { - Self { + let me = Self { options: serde_json::Map::new(), progress_hook: None, - } + post_processors: vec![], + }; + + me.with_post_processor(post_processors::dearrow::DeArrowPP) } #[must_use] @@ -426,10 +519,7 @@ impl YoutubeDLOptions { let mut options = self.options; options.insert(key.into(), value.into()); - Self { - options, - progress_hook: self.progress_hook, - } + Self { options, ..self } } #[must_use] @@ -438,12 +528,18 @@ impl YoutubeDLOptions { todo!() } else { Self { - options: self.options, progress_hook: Some(progress_hook), + ..self } } } + #[must_use] + pub fn with_post_processor<P: PostProcessor + 'static>(mut self, post_processor: P) -> Self { + self.post_processors.push(Box::new(post_processor)); + self + } + /// # Errors /// If the underlying [`YoutubeDL::from_options`] errors. pub fn build(self) -> Result<YoutubeDL, build::Error> { @@ -454,7 +550,7 @@ impl YoutubeDLOptions { pub fn from_json_options(options: serde_json::Map<String, serde_json::Value>) -> Self { Self { options, - progress_hook: None, + ..Self::new() } } @@ -462,10 +558,6 @@ impl YoutubeDLOptions { pub fn get(&self, key: &str) -> Option<&serde_json::Value> { self.options.get(key) } - - fn into_py_dict(self, vm: &VirtualMachine) -> PyRef<PyDict> { - json_loads(self.options, vm) - } } #[allow(missing_docs)] @@ -474,9 +566,6 @@ pub mod build { pub enum Error { #[error("Python threw an exception: {0}")] Python(String), - - #[error("Io error: {0}")] - Io(#[from] std::io::Error), } } diff --git a/crates/yt_dlp/src/post_processors/dearrow.rs b/crates/yt_dlp/src/post_processors/dearrow.rs new file mode 100644 index 0000000..bdbea7c --- /dev/null +++ b/crates/yt_dlp/src/post_processors/dearrow.rs @@ -0,0 +1,118 @@ +// 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 log::{info, warn}; +use serde::{Deserialize, Serialize}; + +use crate::{InfoJson, json_get}; + +use super::PostProcessor; + +#[derive(Debug, Clone, Copy)] +pub struct DeArrowPP; + +impl PostProcessor for DeArrowPP { + fn extractors(&self) -> &'static [&'static str] { + &["Youtube"] + } + + fn process(&self, mut info: InfoJson) -> Result<InfoJson, super::Error> { + let mut output: DeArrowApi = reqwest::blocking::get(format!( + "https://sponsor.ajay.app/api/branding?videoID={}", + json_get!(info, "id", as_str) + ))? + .json()?; + + output.titles.reverse(); + + let title_len = output.titles.len(); + loop { + let Some(title) = output.titles.pop() else { + break; + }; + + if (title.locked || title.votes < 1) && title_len > 1 { + info!( + "Skipping title {:#?}, as it is not good enough", + title.value + ); + // Skip titles that are not “good” enough. + continue; + } + + if let Some(old_title) = info.insert( + "title".to_owned(), + serde_json::Value::String(title.value.clone()), + ) { + warn!("Updating title from {:#?} to {:#?}", old_title, title.value); + info.insert("original_title".to_owned(), old_title); + } else { + warn!("Setting title to {:#?}", title.value); + } + + break; + } + + Ok(info) + } +} + +#[derive(Serialize, Deserialize)] +/// See: <https://wiki.sponsor.ajay.app/w/API_Docs/DeArrow> +struct DeArrowApi { + titles: Vec<Title>, + thumbnails: Vec<Thumbnail>, + + #[serde(alias = "randomTime")] + random_time: Option<f64>, + + #[serde(alias = "videoDuration")] + video_duration: Option<f64>, + + #[serde(alias = "casualVotes")] + casual_votes: Vec<String>, +} + +#[derive(Serialize, Deserialize)] +struct Title { + /// Note: Titles will sometimes contain > before a word. + /// This tells the auto-formatter to not format a word. + /// If you have no auto-formatter, you can ignore this and replace it with an empty string + #[serde(alias = "title")] + value: String, + + original: bool, + votes: u64, + locked: bool, + + #[serde(alias = "UUID")] + uuid: String, + + /// only present if requested + #[serde(alias = "userID")] + user_id: Option<String>, +} + +#[derive(Serialize, Deserialize)] +struct Thumbnail { + // null if original is true + timestamp: Option<f64>, + + original: bool, + votes: u64, + locked: bool, + + #[serde(alias = "UUID")] + uuid: String, + + /// only present if requested + #[serde(alias = "userID")] + user_id: Option<String>, +} diff --git a/crates/yt_dlp/src/post_processors/mod.rs b/crates/yt_dlp/src/post_processors/mod.rs new file mode 100644 index 0000000..65801c2 --- /dev/null +++ b/crates/yt_dlp/src/post_processors/mod.rs @@ -0,0 +1,30 @@ +// 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 crate::InfoJson; + +pub mod dearrow; + +pub trait PostProcessor: std::fmt::Debug + Send { + /// Process a [`InfoJson`] object and return the updated one. + /// + /// # Errors + /// If the processing steps failed. + fn process(&self, info: InfoJson) -> Result<InfoJson, Error>; + + /// The supported extractors for this post processor + fn extractors(&self) -> &'static [&'static str]; +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Failed to access a api: {0}")] + Get(#[from] reqwest::Error), +} diff --git a/crates/yt_dlp/src/progress_hook.rs b/crates/yt_dlp/src/progress_hook.rs index 7a7628a..43f85e0 100644 --- a/crates/yt_dlp/src/progress_hook.rs +++ b/crates/yt_dlp/src/progress_hook.rs @@ -1,3 +1,13 @@ +// 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>. + #[macro_export] macro_rules! mk_python_function { ($name:ident, $new_name:ident) => { diff --git a/flake.lock b/flake.lock index ba25c93..fcf9d4c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1749809936, - "narHash": "sha256-WPGRaj7CKfZukjcpxiacp29uYfMl3S9zFiEsVFv/HWM=", + "lastModified": 1750005889, + "narHash": "sha256-5Ja4RfAWUqzX1B1MC/mSQzNBsTtXmlW4RQyPqmHVU90=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ec4c48ddcd5718cc1312f432b800fbbfe63ee2fe", + "rev": "0fbc85d348db795d46453097b151c08712b86a84", "type": "github" }, "original": { diff --git a/package/package.nix b/package/package.nix index c3bc338..5a29fad 100644 --- a/package/package.nix +++ b/package/package.nix @@ -8,68 +8,117 @@ # 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>. { - ffmpeg, - glibc, lib, - llvmPackages_latest, - makeWrapper, - mpv-unwrapped, - python3, rustPlatform, + installShellFiles, + # buildInputs + mpv-unwrapped, + python3Packages, + ffmpeg, + openssl, + libffi, + # NativeBuildInputs + makeWrapper, + llvmPackages_latest, + glibc, sqlite, - tree-sitter-yts, fd, -}: let - version = "0.1.0"; + pkg-config, + SDL2, + # Passthru + tree-sitter-yts, +}: +rustPlatform.buildRustPackage (finalAttrs: { + pname = "yt"; + inherit + ((builtins.fromTOML (builtins.readFile + ../Cargo.toml)).workspace.package) + version + ; - src = ./..; + src = lib.cleanSourceWith { + src = lib.cleanSource ./..; + filter = name: type: + (type == "directory") + || (builtins.elem (builtins.baseNameOf name) [ + "Cargo.toml" + "Cargo.lock" + "mkdb.sh" + "help.str" + "raw_error_warning.txt" + ]) + || (lib.strings.hasSuffix ".rs" (builtins.baseNameOf name)) + || (lib.strings.hasSuffix ".h" (builtins.baseNameOf name)) + || (lib.strings.hasSuffix ".sql" (builtins.baseNameOf name)); + }; + + nativeBuildInputs = [ + installShellFiles + makeWrapper + sqlite + fd + pkg-config + ]; buildInputs = [ - (python3.withPackages (ps: [ps.yt-dlp])) + python3Packages.yt-dlp mpv-unwrapped.dev ffmpeg + openssl + libffi ]; -in - rustPlatform.buildRustPackage { - inherit version src buildInputs; - pname = "yt"; - nativeBuildInputs = [ - makeWrapper - sqlite - fd - ]; + checkInputs = [ + # Needed for the tests in `libmpv2` + SDL2 + ]; - env = let - clang_version = - lib.versions.major - llvmPackages_latest.clang-unwrapped.version; - in { - FFMPEG_LOCATION = "${lib.getExe ffmpeg}"; - PYO3_PYTHON = lib.getExe (python3.withPackages (ps: [ps.yt-dlp])); + env = let + clang_version = + lib.versions.major + llvmPackages_latest.clang-unwrapped.version; + in { + # Needed for the compile time sqlite checks. + DATABASE_URL = "sqlite://database.sqlx"; - C_INCLUDE_PATH = "${glibc.dev}/include"; - DATABASE_URL = "sqlite://target/database.sqlx"; - LIBCLANG_INCLUDE_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/clang/${clang_version}/include"; - LIBCLANG_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/libclang.so"; - }; + # Required by yt_dlp + FFMPEG_LOCATION = "${lib.getExe ffmpeg}"; - doCheck = false; + # Needed for the libmpv2. + C_INCLUDE_PATH = "${glibc.dev}/include"; + LIBCLANG_INCLUDE_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/clang/${clang_version}/include"; + LIBCLANG_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/libclang.so"; + }; - prePatch = '' - bash ./scripts/mkdb.sh - ''; + doCheck = true; - passthru = { - inherit tree-sitter-yts; - }; + prePatch = '' + # Generate the sqlite db, so that we can run the comp-time sqlite checks. + bash ./scripts/mkdb.sh + ''; - cargoLock = { - lockFile = ../Cargo.lock; + passthru = { + inherit tree-sitter-yts; + }; + + cargoLock = { + lockFile = ../Cargo.lock; + outputHashes = { + "ruff_python_ast-0.0.0" = "sha256-/CVpNBOBpvQhz7X80nUHC2x7ZxxCJH8O0WAABJKEriA="; + "rustpython-0.4.0" = "sha256-hsWqLRiqA0pUNFkHPwvu+Myh5a3p/VXotkPq5ZexqaQ="; + "rustpython-doc-0.3.0" = "sha256-34ERuLFKzUD9Xmf1zlafe42GLWZfUlw17ejf/NN6yH4="; }; + }; + + postInstall = '' + installShellCompletion --cmd yt \ + --bash <(COMPLETE=bash $out/bin/yt) \ + --fish <(COMPLETE=fish $out/bin/yt) \ + --zsh <(COMPLETE=zsh $out/bin/yt) - postInstall = '' - wrapProgram $out/bin/yt \ - --prefix PATH : ${lib.makeBinPath buildInputs} - ''; - } + # NOTE: We cannot clear the path, because we need access to the $EDITOR. <2025-04-04> + wrapProgram $out/bin/yt \ + --prefix PATH : ${lib.makeBinPath finalAttrs.buildInputs} \ + --set YTDLP_NO_PLUGINS 1 + ''; +}) diff --git a/scripts/mkdb.sh b/scripts/mkdb.sh index f0c7740..6674841 100755 --- a/scripts/mkdb.sh +++ b/scripts/mkdb.sh @@ -11,7 +11,7 @@ # If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. root="$(dirname "$0")/.." -db="$root/target/database.sqlx" +db="${DATABASE_URL#sqlite://}" [ -f "$db" ] && rm "$db" [ -d "$root/target" ] || mkdir "$root/target" |