diff options
Diffstat (limited to '')
29 files changed, 1050 insertions, 412 deletions
diff --git a/pkgs/by-name/yt/yt/.cargo/config.toml b/pkgs/by-name/yt/yt/.cargo/config.toml index 6c88c6ba..e5151a56 100644 --- a/pkgs/by-name/yt/yt/.cargo/config.toml +++ b/pkgs/by-name/yt/yt/.cargo/config.toml @@ -2,5 +2,4 @@ PYO3_PYTHON = "/nix/store/7xzk119acyws2c4ysygdv66l0grxkr39-python3-3.11.9-env/bin/python3" [build] -# rustflags = ["-C", "link-args=-fuse-ld=mold,--no-rosegment"] -rustflags = ["-Clink-arg=-fuse-ld=mold", "-Clink-arg=-Wl,--no-rosegment"] +rustflags = ["-Clink-arg=-fuse-ld=mold", "-Ctarget-cpu=native"] diff --git a/pkgs/by-name/yt/yt/Cargo.lock b/pkgs/by-name/yt/yt/Cargo.lock index 1342e29b..c949c313 100644 --- a/pkgs/by-name/yt/yt/Cargo.lock +++ b/pkgs/by-name/yt/yt/Cargo.lock @@ -27,7 +27,7 @@ dependencies = [ "getrandom", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -123,9 +123,9 @@ checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "atoi" @@ -188,7 +188,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.72", + "syn 2.0.75", "which", ] @@ -243,15 +243,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.1.7" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +dependencies = [ + "shlex", +] [[package]] name = "cexpr" @@ -304,9 +307,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.13" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", @@ -314,9 +317,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.13" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", @@ -333,7 +336,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] @@ -362,15 +365,15 @@ checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" dependencies = [ "libc", ] @@ -579,7 +582,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] @@ -686,6 +689,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -753,9 +762,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown", @@ -769,11 +778,11 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] @@ -786,9 +795,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.10.5" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -801,9 +810,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -825,9 +834,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.157" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "374af5f94e54fa97cf75e945cce8a6b201e88a1a07e688b47dfd2a59c66dbd86" [[package]] name = "libloading" @@ -936,11 +945,11 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", @@ -1005,9 +1014,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.2" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] @@ -1063,6 +1072,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] +name = "pest" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.75", +] + +[[package]] +name = "pest_meta" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1109,11 +1163,11 @@ checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "ppv-lite86" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy 0.6.6", + "zerocopy", ] [[package]] @@ -1123,7 +1177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] @@ -1182,7 +1236,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] @@ -1195,7 +1249,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] @@ -1257,9 +1311,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -1343,29 +1397,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", "memchr", @@ -1716,9 +1770,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" dependencies = [ "proc-macro2", "quote", @@ -1733,14 +1787,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1769,7 +1824,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] @@ -1799,9 +1854,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", @@ -1822,7 +1877,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] @@ -1856,7 +1911,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] @@ -1869,12 +1924,28 @@ dependencies = [ ] [[package]] +name = "trinitry" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f814008587cd653ef1f92f9caf321e86a6f53899ec118fd50eaed55974863a40" +dependencies = [ + "pest", + "pest_derive", +] + +[[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1969,34 +2040,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2004,22 +2076,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "which" @@ -2045,11 +2117,11 @@ dependencies = [ [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2080,6 +2152,15 @@ dependencies = [ ] [[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2225,6 +2306,7 @@ dependencies = [ "stderrlog", "tempfile", "tokio", + "trinitry", "url", "xdg", "yt_dlp", @@ -2243,32 +2325,12 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" -dependencies = [ - "byteorder", - "zerocopy-derive 0.6.6", -] - -[[package]] -name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy-derive" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", + "byteorder", + "zerocopy-derive", ] [[package]] @@ -2279,7 +2341,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] diff --git a/pkgs/by-name/yt/yt/Cargo.toml b/pkgs/by-name/yt/yt/Cargo.toml index d1e40cf0..78b9ab2c 100644 --- a/pkgs/by-name/yt/yt/Cargo.toml +++ b/pkgs/by-name/yt/yt/Cargo.toml @@ -10,20 +10,21 @@ anyhow = "1.0.86" blake3 = "1.5.3" chrono = { version = "0.4.38", features = ["now"] } chrono-humanize = "0.2.3" -clap = { version = "4.5.13", features = ["derive"] } +clap = { version = "4.5.16", features = ["derive"] } futures = "0.3.30" log = "0.4.22" -regex = "1.10.5" -serde = { version = "1.0.204", features = ["derive"] } -serde_json = "1.0.121" +regex = "1.10.6" +serde = { version = "1.0.208", features = ["derive"] } +serde_json = "1.0.125" sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"] } stderrlog = "0.6.0" -tempfile = "3.10.1" -tokio = { version = "1.39.2", features = ["rt-multi-thread", "macros", "process"] } +tempfile = "3.12.0" +tokio = { version = "1.39.3", features = ["rt-multi-thread", "macros", "process", "time"] } url = { version = "2.5.2", features = ["serde"] } xdg = "2.5.2" yt_dlp = { path = "./yt_dlp/" } libmpv2 = { path = "./libmpv2" } +trinitry = { version = "0.2.2" } [[bin]] name = "yt" @@ -31,3 +32,9 @@ name = "yt" [profile.profiling] inherits = "release" debug = true + +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +split-debuginfo = "off" diff --git a/pkgs/by-name/yt/yt/flake.lock b/pkgs/by-name/yt/yt/flake.lock index cf99a35f..122b5b09 100644 --- a/pkgs/by-name/yt/yt/flake.lock +++ b/pkgs/by-name/yt/yt/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1722421184, - "narHash": "sha256-/DJBI6trCeVnasdjUo9pbnodCLZcFqnVZiLUfqLH4jA=", + "lastModified": 1723637854, + "narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9f918d616c5321ad374ae6cb5ea89c9e04bf3e58", + "rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9", "type": "github" }, "original": { diff --git a/pkgs/by-name/yt/yt/flake.nix b/pkgs/by-name/yt/yt/flake.nix index ea9b0de4..5ced0fbd 100644 --- a/pkgs/by-name/yt/yt/flake.nix +++ b/pkgs/by-name/yt/yt/flake.nix @@ -25,10 +25,15 @@ ]; in { devShells.default = pkgs.mkShell { - env = { + env = let + clang_version = + pkgs.lib.versions.major + pkgs.llvmPackages_latest.clang-unwrapped.version; + in { FFMPEG_LOCATION = "${pkgs.lib.getExe pkgs.ffmpeg}"; LIBCLANG_PATH = "${pkgs.llvmPackages_latest.clang-unwrapped.lib}/lib/libclang.so"; - C_INCLUDE_PATH = "${pkgs.llvmPackages_latest.clang-unwrapped.lib}/lib/clang/18/include"; + LIBCLANG_INCLUDE_PATH = "${pkgs.llvmPackages_latest.clang-unwrapped.lib}/lib/clang/${clang_version}/include"; + C_INCLUDE_PATH = "${pkgs.glibc.dev}/include"; }; inherit buildInputs nativeBuildInputs; diff --git a/pkgs/by-name/yt/yt/libmpv2/libmpv2-sys/build.rs b/pkgs/by-name/yt/yt/libmpv2/libmpv2-sys/build.rs index 5361e198..1380b58d 100644 --- a/pkgs/by-name/yt/yt/libmpv2/libmpv2-sys/build.rs +++ b/pkgs/by-name/yt/yt/libmpv2/libmpv2-sys/build.rs @@ -12,7 +12,14 @@ fn main() { .opaque_type("mpv_handle") .opaque_type("mpv_render_context") .enable_function_attribute_detection() - .clang_arg("-fretain-comments-from-system-headers") + .clang_args(&[ + "-fretain-comments-from-system-headers", + &format!( + "--include-directory={}", + env::var("LIBCLANG_INCLUDE_PATH").unwrap() + ), + "--verbose", + ]) .generate_comments(true) .generate() .expect("Unable to generate bindings"); diff --git a/pkgs/by-name/yt/yt/libmpv2/src/lib.rs b/pkgs/by-name/yt/yt/libmpv2/src/lib.rs index 4b267e24..199d6ee9 100644 --- a/pkgs/by-name/yt/yt/libmpv2/src/lib.rs +++ b/pkgs/by-name/yt/yt/libmpv2/src/lib.rs @@ -19,6 +19,7 @@ #![allow(non_upper_case_globals)] +use std::fmt::Display; use std::os::raw as ctype; pub const MPV_CLIENT_API_MAJOR: ctype::c_ulong = 2; @@ -86,11 +87,79 @@ pub mod mpv_log_level { } /// The reason a file stopped. -pub use libmpv2_sys::mpv_end_file_reason as EndFileReason; -pub mod mpv_end_file_reason { - pub use libmpv2_sys::mpv_end_file_reason_MPV_END_FILE_REASON_EOF as Eof; - pub use libmpv2_sys::mpv_end_file_reason_MPV_END_FILE_REASON_ERROR as Error; - pub use libmpv2_sys::mpv_end_file_reason_MPV_END_FILE_REASON_QUIT as Quit; - pub use libmpv2_sys::mpv_end_file_reason_MPV_END_FILE_REASON_REDIRECT as Redirect; - pub use libmpv2_sys::mpv_end_file_reason_MPV_END_FILE_REASON_STOP as Stop; +#[derive(Debug, Clone, Copy)] +pub enum EndFileReason { + /** + * The end of file was reached. Sometimes this may also happen on + * incomplete or corrupted files, or if the network connection was + * interrupted when playing a remote file. It also happens if the + * playback range was restricted with --end or --frames or similar. + */ + Eof, + + /** + * Playback was stopped by an external action (e.g. playlist controls). + */ + Stop, + + /** + * Playback was stopped by the quit command or player shutdown. + */ + Quit, + + /** + * Some kind of error happened that lead to playback abort. Does not + * necessarily happen on incomplete or broken files (in these cases, both + * MPV_END_FILE_REASON_ERROR or MPV_END_FILE_REASON_EOF are possible). + * + * mpv_event_end_file.error will be set. + */ + Error, + + /** + * The file was a playlist or similar. When the playlist is read, its + * entries will be appended to the playlist after the entry of the current + * file, the entry of the current file is removed, and a MPV_EVENT_END_FILE + * event is sent with reason set to MPV_END_FILE_REASON_REDIRECT. Then + * playback continues with the playlist contents. + * Since API version 1.18. + */ + Redirect, +} + +impl From<libmpv2_sys::mpv_end_file_reason> for EndFileReason { + fn from(value: libmpv2_sys::mpv_end_file_reason) -> Self { + match value { + 0 => Self::Eof, + 2 => Self::Stop, + 3 => Self::Quit, + 4 => Self::Error, + 5 => Self::Redirect, + _ => unreachable!("Other enum variants do not exist yet"), + } + } +} + +impl Display for EndFileReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EndFileReason::Eof => f.write_str("The end of file was reached.")?, + EndFileReason::Error => { + f.write_str( + "Playback was stopped by an external action (e.g. playlist controls).", + )?; + } + EndFileReason::Quit => { + f.write_str("Playback was stopped by the quit command or player shutdown.")?; + } + EndFileReason::Redirect => { + f.write_str("Some kind of error happened that lead to playback abort.")?; + } + EndFileReason::Stop => { + f.write_str("The file was a playlist or similar.")?; + } + } + + Ok(()) + } } diff --git a/pkgs/by-name/yt/yt/libmpv2/src/mpv/events.rs b/pkgs/by-name/yt/yt/libmpv2/src/mpv/events.rs index b6513890..115c23fc 100644 --- a/pkgs/by-name/yt/yt/libmpv2/src/mpv/events.rs +++ b/pkgs/by-name/yt/yt/libmpv2/src/mpv/events.rs @@ -270,7 +270,7 @@ impl EventContext { if let Err(e) = mpv_err((), end_file.error) { Some(Err(e)) } else { - Some(Ok(Event::EndFile(end_file.reason as _))) + Some(Ok(Event::EndFile(end_file.reason.into()))) } } mpv_event_id::FileLoaded => Some(Ok(Event::FileLoaded)), diff --git a/pkgs/by-name/yt/yt/scripts/mkdb.sh b/pkgs/by-name/yt/yt/scripts/mkdb.sh index 307e4459..f4246a49 100755 --- a/pkgs/by-name/yt/yt/scripts/mkdb.sh +++ b/pkgs/by-name/yt/yt/scripts/mkdb.sh @@ -1,9 +1,10 @@ #!/usr/bin/env sh root="$(dirname "$0")/.." +db="$root/target/database.sqlite" -rm "$root/target/database.sqlite" +[ -f "$db" ] && rm "$db" -sqlite3 "$root/target/database.sqlite" <"$root/src/storage/video_database/schema.sql" +sqlite3 "$db" <"$root/src/storage/video_database/schema.sql" # vim: ft=sh diff --git a/pkgs/by-name/yt/yt/src/cache/mod.rs b/pkgs/by-name/yt/yt/src/cache/mod.rs index b60cda89..f9715e40 100644 --- a/pkgs/by-name/yt/yt/src/cache/mod.rs +++ b/pkgs/by-name/yt/yt/src/cache/mod.rs @@ -6,16 +6,14 @@ use log::info; use crate::{ app::App, storage::video_database::{ - downloader::set_video_cache_path, getters::get_videos, setters::set_video_status, Video, - VideoStatus, + downloader::set_video_cache_path, getters::get_videos, Video, VideoStatus, }, }; async fn invalidate_video(app: &App, video: &Video) -> Result<()> { info!("Invalidating cache of video: '{}'", video.title); - set_video_status(app, &video.extractor_hash, VideoStatus::Watch, None).await?; - set_video_cache_path(app, &video, None).await?; + set_video_cache_path(app, &video.extractor_hash, None).await?; Ok(()) } diff --git a/pkgs/by-name/yt/yt/src/cli.rs b/pkgs/by-name/yt/yt/src/cli.rs index 66b1d4c1..799c9ee4 100644 --- a/pkgs/by-name/yt/yt/src/cli.rs +++ b/pkgs/by-name/yt/yt/src/cli.rs @@ -1,12 +1,18 @@ use std::path::PathBuf; -use clap::{ArgAction, Parser, Subcommand}; +use chrono::NaiveDate; +use clap::{ArgAction, Args, Parser, Subcommand}; use url::Url; +use crate::{ + select::selection_file::duration::Duration, + storage::video_database::extractor_hash::LazyExtractorHash, +}; + #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] /// An command line interface to select, download and watch videos -pub struct Args { +pub struct CliArgs { #[command(subcommand)] /// The subcommand to execute [default: select] pub command: Option<Command>, @@ -51,8 +57,11 @@ pub enum Command { command: CacheCommand, }, - /// Open a `git rebase` like file to select the videos to watch - Select, + /// Change the state of videos in the database (the default) + Select { + #[command(subcommand)] + cmd: Option<SelectCommand>, + }, /// Subscribe to an URL Subscribe { @@ -73,12 +82,16 @@ pub enum Command { /// Update the video database Update { #[arg(short, long, default_value = "20")] - /// The number of videos to stop updating + /// The number of videos to updating max_backlog: u32, #[arg(short, long)] /// The subscriptions to update (can be given multiple times) subscriptions: Vec<String>, + + #[arg(short, long, default_value = "6")] + /// How many processes to spawn at the same time + concurrent_processes: usize, }, /// Update one subscription (this is here to parallelize the normal `update`) @@ -95,6 +108,67 @@ pub enum Command { Subscriptions {}, } +impl Default for Command { + fn default() -> Self { + Self::Select { + cmd: Some(SelectCommand::default()), + } + } +} + +#[derive(Clone, Debug, Args)] +#[command(infer_subcommands = true)] +/// Mark the video given by the hash to be watched +pub struct SharedSelectionCommandArgs { + /// The short extractor hash + pub hash: LazyExtractorHash, + + pub title: String, + + pub date: NaiveDate, + + pub publisher: String, + + pub duration: Duration, + + pub url: Url, +} + +#[derive(Subcommand, Clone, Debug, Default)] +#[command(infer_subcommands = true)] +pub enum SelectCommand { + #[default] + /// Open a `git rebase` like file to select the videos to watch (the default) + File, + + Watch { + #[command(flatten)] + shared: SharedSelectionCommandArgs, + + #[arg(short, long, default_value = "0")] + /// The ordering priority (higher means more at the top) + priority: i64, + }, + + /// Mark the video given by the hash to be dropped + Drop { + #[command(flatten)] + shared: SharedSelectionCommandArgs, + }, + + /// Open the video URL in Firefox's `timesinks.youtube` profile + Url { + #[command(flatten)] + shared: SharedSelectionCommandArgs, + }, + + /// Reset the videos status to 'Pick' + Pick { + #[command(flatten)] + shared: SharedSelectionCommandArgs, + }, +} + #[derive(Subcommand, Clone, Debug)] pub enum CheckCommand { /// Check if the given info.json is deserializable diff --git a/pkgs/by-name/yt/yt/src/constants.rs b/pkgs/by-name/yt/yt/src/constants.rs index 17e0f018..fbe51413 100644 --- a/pkgs/by-name/yt/yt/src/constants.rs +++ b/pkgs/by-name/yt/yt/src/constants.rs @@ -3,32 +3,9 @@ use std::{env::temp_dir, path::PathBuf}; use anyhow::Context; pub const HELP_STR: &str = include_str!("./select/selection_file/help.str"); +pub const LOCAL_COMMENTS_LENGTH: usize = 1000; -pub const YT_DLP_FLAGS: [&str; 13] = [ - // Ignore errors arising of unavailable sponsor block API - "--ignore-errors", - "--format", - "bestvideo[height<=?1080]+bestaudio/best", - "--embed-chapters", - "--progress", - "--write-comments", - "--extractor-args", - "youtube:max_comments=150,all,100;comment_sort=top", - "--write-info-json", - "--sponsorblock-mark", - "default", - "--sponsorblock-remove", - "sponsor", -]; -pub const MPV_FLAGS: [&str; 4] = [ - "--speed=2.7", - "--volume=75", - "--keep-open=yes", - "--msg-level=osd/libass=fatal", -]; - -pub const CONCURRENT: u32 = 5; - +pub const CONCURRENT_DOWNLOADS: u32 = 5; // We download to the temp dir to avoid taxing the disk pub fn download_dir() -> PathBuf { const DOWNLOAD_DIR: &str = "/tmp/yt"; @@ -48,6 +25,21 @@ fn get_data_path(name: &'static str) -> anyhow::Result<PathBuf> { .place_data_file(name) .with_context(|| format!("Failed to place data file: '{}'", name)) } +fn get_config_path(name: &'static str) -> anyhow::Result<PathBuf> { + let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?; + xdg_dirs + .place_config_file(name) + .with_context(|| format!("Failed to place config file: '{}'", name)) +} + +pub fn mpv_config_path() -> anyhow::Result<PathBuf> { + const MPV_CONFIG_PATH: &str = "mpv.conf"; + get_config_path(MPV_CONFIG_PATH) +} +pub fn mpv_input_path() -> anyhow::Result<PathBuf> { + const MPV_INPUT_CONFIG_PATH: &str = "mpv.input.conf"; + get_config_path(MPV_INPUT_CONFIG_PATH) +} pub fn status_path() -> anyhow::Result<PathBuf> { const STATUS_PATH: &str = "running.info.json"; diff --git a/pkgs/by-name/yt/yt/src/download/mod.rs b/pkgs/by-name/yt/yt/src/download/mod.rs index 2dc7c431..1499171f 100644 --- a/pkgs/by-name/yt/yt/src/download/mod.rs +++ b/pkgs/by-name/yt/yt/src/download/mod.rs @@ -6,8 +6,7 @@ use crate::{ storage::video_database::{ downloader::{get_next_uncached_video, set_video_cache_path}, extractor_hash::ExtractorHash, - setters::set_video_status, - Video, VideoStatus, + Video, }, }; @@ -64,10 +63,15 @@ impl Downloader { if let Some(_) = &self.current_download { let current_download = self.current_download.take().expect("Is Some"); + if current_download.task_handle.is_finished() { + current_download.task_handle.await??; + continue; + } + if next_video.extractor_hash != current_download.extractor_hash { info!( "Noticed, that the next video is not the video being downloaded, replacing it ('{}' vs. '{}')!", - next_video.extractor_hash.format(app).await?, current_download.extractor_hash.format(app).await? + next_video.extractor_hash.into_short_hash(app).await?, current_download.extractor_hash.into_short_hash(app).await? ); // Replace the currently downloading video @@ -79,7 +83,7 @@ impl Downloader { } else { debug!( "Currently downloading '{}'", - current_download.extractor_hash.format(app).await? + current_download.extractor_hash.into_short_hash(app).await? ); // Reset the taken value self.current_download = Some(current_download); @@ -130,6 +134,7 @@ impl Downloader { // } async fn actually_cache_video(app: &App, video: &Video) -> Result<()> { + debug!("Download started: {}", &video.title); let result = yt_dlp::download(&[video.url.clone()], &download_opts()) .await .with_context(|| format!("Failed to download video: '{}'", video.title))?; @@ -137,9 +142,7 @@ impl Downloader { assert_eq!(result.len(), 1); let result = &result[0]; - set_video_cache_path(app, video, Some(&result)).await?; - - set_video_status(app, &(video.extractor_hash), VideoStatus::Cached, None).await?; + set_video_cache_path(app, &video.extractor_hash, Some(&result)).await?; info!( "Video '{}' was downlaoded to path: {}", diff --git a/pkgs/by-name/yt/yt/src/main.rs b/pkgs/by-name/yt/yt/src/main.rs index b89c2eec..92fd1a8d 100644 --- a/pkgs/by-name/yt/yt/src/main.rs +++ b/pkgs/by-name/yt/yt/src/main.rs @@ -3,8 +3,9 @@ use std::fs; use anyhow::{bail, Context, Result}; use app::App; use clap::Parser; -use cli::{CacheCommand, CheckCommand}; +use cli::{CacheCommand, CheckCommand, SelectCommand}; use log::info; +use select::cmds::handle_select_cmd; use yt_dlp::wrapper::info_json::InfoJson; use crate::{cli::Command, storage::subscriptions::get_subscriptions}; @@ -25,7 +26,7 @@ pub mod watch; #[tokio::main] async fn main() -> Result<()> { - let args = cli::Args::parse(); + let args = cli::CliArgs::parse(); stderrlog::new() .module(module_path!()) .modules(&["yt_dlp".to_owned(), "libmpv2".to_owned()]) @@ -39,7 +40,7 @@ async fn main() -> Result<()> { let app = App::new().await?; - match args.command.unwrap_or(Command::Select) { + match args.command.unwrap_or(Command::default()) { Command::Download { urls } => { if urls.is_empty() { info!("Downloading urls from database"); @@ -51,8 +52,13 @@ async fn main() -> Result<()> { todo!() } } - Command::Select => { - select::select(&app).await?; + Command::Select { cmd } => { + let cmd = cmd.unwrap_or(SelectCommand::default()); + + match cmd { + SelectCommand::File => select::select(&app).await?, + _ => handle_select_cmd(&app, cmd).await?, + } } Command::Subscribe { name, url } => { subscribe::subscribe(name, url) @@ -67,6 +73,7 @@ async fn main() -> Result<()> { Command::Update { max_backlog, subscriptions, + concurrent_processes, } => { let all_subs = get_subscriptions()?; @@ -79,7 +86,7 @@ async fn main() -> Result<()> { } } - update::update(max_backlog, subscriptions).await?; + update::update(max_backlog, subscriptions, concurrent_processes).await?; } Command::UpdateOnce { sub_name, diff --git a/pkgs/by-name/yt/yt/src/select/cmds.rs b/pkgs/by-name/yt/yt/src/select/cmds.rs new file mode 100644 index 00000000..a2a440f8 --- /dev/null +++ b/pkgs/by-name/yt/yt/src/select/cmds.rs @@ -0,0 +1,50 @@ +use crate::{ + app::App, + cli::SelectCommand, + storage::video_database::{getters::get_video_by_hash, setters::set_video_status, VideoStatus}, +}; + +use anyhow::{Context, Result}; + +pub async fn handle_select_cmd(app: &App, cmd: SelectCommand) -> Result<()> { + match cmd { + SelectCommand::Pick { shared } => { + set_video_status( + app, + &shared.hash.realize(app).await?, + VideoStatus::Pick, + None, + ) + .await? + } + SelectCommand::Drop { shared } => { + set_video_status( + app, + &shared.hash.realize(app).await?, + VideoStatus::Drop, + None, + ) + .await? + } + SelectCommand::Watch { shared, priority } => { + let hash = shared.hash.realize(&app).await?; + let video = get_video_by_hash(app, &hash).await?; + + if let Some(_) = video.cache_path { + // Do nothing, as the video *should* already have a `Cached` status and a + // cache_path. + } else { + set_video_status(app, &hash, VideoStatus::Watch, Some(priority)).await?; + } + } + + SelectCommand::Url { shared } => { + let mut firefox = std::process::Command::new("firefox"); + firefox.args(["-P", "timesinks.youtube"]); + firefox.arg(shared.url.as_str()); + let _handle = firefox.spawn().context("Failed to run firefox")?; + } + SelectCommand::File => unreachable!("This should have been filtered out"), + } + Ok(()) +} diff --git a/pkgs/by-name/yt/yt/src/select/mod.rs b/pkgs/by-name/yt/yt/src/select/mod.rs index abb389f0..34fcf56a 100644 --- a/pkgs/by-name/yt/yt/src/select/mod.rs +++ b/pkgs/by-name/yt/yt/src/select/mod.rs @@ -6,16 +6,20 @@ use std::{ use crate::{ app::App, + cli::CliArgs, constants::{last_select, HELP_STR}, - storage::video_database::{getters::get_videos, setters::set_video_status, VideoStatus}, + storage::video_database::{getters::get_videos, VideoStatus}, }; use anyhow::{bail, Context, Result}; +use clap::Parser; +use cmds::handle_select_cmd; use futures::future::join_all; +use log::debug; +use selection_file::process_line; use tempfile::Builder; -use self::selection_file::{filter_line, LineCommand}; - +pub mod cmds; pub mod selection_file; pub async fn select(app: &App) -> Result<()> { @@ -89,32 +93,32 @@ pub async fn select(app: &App) -> Result<()> { let reader = BufReader::new(&read_file); - let mut priority = 1; for line in reader.lines() { let line = line.context("Failed to read a line")?; - - if let Some(line) = filter_line(app, &line) - .await - .with_context(|| format!("Failed to process line: '{}'", line))? - { - match line.cmd { - LineCommand::Pick => { - set_video_status(app, &line.hash, VideoStatus::Pick, None).await? - } - LineCommand::Drop => { - set_video_status(app, &line.hash, VideoStatus::Drop, None).await? - } - LineCommand::Watch => { - set_video_status(app, &line.hash, VideoStatus::Watch, Some(priority)).await?; - priority += 1; - } - LineCommand::Url => { - let mut firefox = std::process::Command::new("firefox"); - firefox.args(["-P", "timesinks.youtube"]); - firefox.arg(line.url.as_str()); - let _handle = firefox.spawn().context("Failed to run firefox")?; - } - } + if let Some(line) = process_line(&line)? { + debug!( + "Parsed command: `{}`", + line.iter() + .map(|val| format!("\"{}\"", val)) + .collect::<Vec<String>>() + .join(" ") + ); + + let arg_line = ["yt", "select"] + .into_iter() + .chain(line.iter().map(|val| val.as_str())); + + let args = CliArgs::parse_from(arg_line); + + let cmd = if let crate::cli::Command::Select { cmd } = + args.command.expect("This will be some") + { + cmd + } else { + unreachable!("This is checked in the `filter_line` function") + }; + + handle_select_cmd(&app, cmd.expect("This value should always be some here")).await? } } diff --git a/pkgs/by-name/yt/yt/src/select/selection_file/display.rs b/pkgs/by-name/yt/yt/src/select/selection_file/display.rs index 8a58ffdd..3649b2b8 100644 --- a/pkgs/by-name/yt/yt/src/select/selection_file/display.rs +++ b/pkgs/by-name/yt/yt/src/select/selection_file/display.rs @@ -36,12 +36,12 @@ impl Video { f, r#"{} {} "{}" "{}" "{}" "{}" "{}"{}"#, self.status.as_command(), - self.extractor_hash.format(app).await?, + self.extractor_hash.into_short_hash(app).await?, self.title.replace(['"', '„', '”'], "'"), publish_date, parent_subscription_name, Duration::from(self.duration), - self.url.as_str().replace('"', "'"), + self.url.as_str().replace('"', "\\\""), "\n" )?; diff --git a/pkgs/by-name/yt/yt/src/select/selection_file/duration.rs b/pkgs/by-name/yt/yt/src/select/selection_file/duration.rs index 700d9202..9e4b3a83 100644 --- a/pkgs/by-name/yt/yt/src/select/selection_file/duration.rs +++ b/pkgs/by-name/yt/yt/src/select/selection_file/duration.rs @@ -1,14 +1,46 @@ +use std::str::FromStr; + +use anyhow::{Context, Result}; + +#[derive(Copy, Clone, Debug)] 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 FromStr for Duration { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + fn parse_num(str: &str, suffix: char) -> Result<u32> { + str.strip_suffix(suffix) + .expect("it has a 'h' suffix") + .parse::<u32>() + .context("Failed to parse hours") } + + let buf: Vec<_> = s.split(' ').collect(); + + let hours; + let minutes; + let seconds; + + assert_eq!(buf.len(), 2, "Other lengths should not happen"); + + if buf[0].ends_with('h') { + hours = parse_num(buf[0], 'h')?; + minutes = parse_num(buf[1], 'm')?; + seconds = 0; + } else if buf[0].ends_with('m') { + hours = 0; + minutes = parse_num(buf[0], 'm')?; + seconds = parse_num(buf[1], 's')?; + } else { + unreachable!("The first part always ends with 'h' or 'm'") + } + + Ok(Self { + time: (hours * 60 * 60) + (minutes * 60) + seconds, + }) } } @@ -37,9 +69,9 @@ impl std::fmt::Display for Duration { if self.time == 0 { write!(f, "[No Duration]") } else if h > 0 { - write!(f, "[{h}h {m}m]") + write!(f, "{h}h {m}m") } else { - write!(f, "[{m}m {s}s]") + write!(f, "{m}m {s}s") } } } @@ -50,11 +82,11 @@ mod test { #[test] fn test_display_duration_1h() { let dur = Duration { time: 60 * 60 }; - assert_eq!("[1h 0m]".to_owned(), dur.to_string()); + 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()); + assert_eq!("30m 0s".to_owned(), dur.to_string()); } } diff --git a/pkgs/by-name/yt/yt/src/select/selection_file/mod.rs b/pkgs/by-name/yt/yt/src/select/selection_file/mod.rs index 957fcd08..c63ca85a 100644 --- a/pkgs/by-name/yt/yt/src/select/selection_file/mod.rs +++ b/pkgs/by-name/yt/yt/src/select/selection_file/mod.rs @@ -1,79 +1,25 @@ //! The data structures needed to express the file, which the user edits -use anyhow::{bail, Context}; -use url::Url; - -use crate::{app::App, storage::video_database::extractor_hash::ExtractorHash}; +use anyhow::{Context, Result}; +use trinitry::Trinitry; pub mod display; pub mod duration; -pub enum LineCommand { - Pick, - Drop, - Watch, - Url, -} - -impl std::str::FromStr for LineCommand { - type Err = anyhow::Error; - fn from_str(v: &str) -> Result<Self, <Self as std::str::FromStr>::Err> { - match v { - "pick" | "p" => Ok(Self::Pick), - "drop" | "d" => Ok(Self::Drop), - "watch" | "w" => Ok(Self::Watch), - "url" | "u" => Ok(Self::Url), - other => bail!("'{}' is not a recognized command!", other), - } - } -} - -pub struct Line { - pub cmd: LineCommand, - pub hash: ExtractorHash, - pub url: Url, -} - -impl Line { - pub async fn from_str(app: &App, s: &str) -> anyhow::Result<Self> { - let buf: Vec<_> = s.split_whitespace().collect(); - - let url_as_str = buf - .last() - .with_context(|| format!("The line '{}' misses it's url field!'", s))? - .trim_matches('"'); +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 { + // pick 2195db "CouchRecherche? Gunnar und Han von STRG_F sind #mitfunkzuhause" "2020-04-01" "STRG_F - Live" "[1h 5m]" "https://www.youtube.com/watch?v=C8UXOaoMrXY" - let url: Url = Url::parse(url_as_str) - .with_context(|| format!("The url '{}' could not be parsed!", url_as_str))?; + let tri = + Trinitry::new(line).with_context(|| format!("Failed to parse line '{}'", line))?; - Ok(Line { - cmd: buf - .get(0) - .with_context(|| format!("The line '{}' is missing it's command!", s))? - .parse()?, - hash: ExtractorHash::parse_from_short_version( - app, - buf.get(1) - .with_context(|| format!("The line '{}' is missing it's blake3 hash!", s))?, - ) - .await - .with_context(|| { - format!( - "Can't parse '{}' as blake3 hash!", - buf.get(1).expect("Already checked"), - ) - })?, - url, - }) - } -} + let mut vec = Vec::with_capacity(tri.arguments().len() + 1); + vec.push(tri.command().to_owned()); + vec.extend(tri.arguments().to_vec().into_iter()); -pub async fn filter_line(app: &App, line: &str) -> anyhow::Result<Option<Line>> { - // Filter out comments and empty lines - if line.starts_with('#') || line.trim().is_empty() { - return Ok(None); + Ok(Some(vec)) } - - let line: Line = Line::from_str(app, line).await?; - Ok(Some(line)) } diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/downloader.rs b/pkgs/by-name/yt/yt/src/storage/video_database/downloader.rs index 5c472ac5..ca3a2ea3 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/downloader.rs +++ b/pkgs/by-name/yt/yt/src/storage/video_database/downloader.rs @@ -12,14 +12,17 @@ use super::{ExtractorHash, Video}; /// Returns to next video which should be downloaded. This respects the priority assigned by select. /// It does not return videos, which are already cached. pub async fn get_next_uncached_video(app: &App) -> Result<Option<Video>> { + let status = VideoStatus::Watch.as_db_integer(); + let result = query!( r#" SELECT * FROM videos - WHERE status = 'Watching' AND cache_path IS NULL + WHERE status = ? AND cache_path IS NULL ORDER BY priority ASC LIMIT 1; - "# + "#, + status ) .fetch_one(&app.database) .await; @@ -46,7 +49,11 @@ pub async fn get_next_uncached_video(app: &App) -> Result<Option<Video>> { cache_path: base.cache_path.as_ref().map(|val| PathBuf::from(val)), description: base.description.clone(), duration: base.duration, - extractor_hash: ExtractorHash::new(base.extractor_hash.parse()?), + extractor_hash: ExtractorHash::from_hash( + base.extractor_hash + .parse() + .expect("The hash in the db should be valid"), + ), last_status_change: base.last_status_change, parent_subscription_name: base.parent_subscription_name.clone(), priority: base.priority, @@ -99,7 +106,11 @@ pub async fn get_next_video_watchable(app: &App) -> Result<Option<Video>> { cache_path: base.cache_path.as_ref().map(|val| PathBuf::from(val)), description: base.description.clone(), duration: base.duration, - extractor_hash: ExtractorHash::new(base.extractor_hash.parse()?), + extractor_hash: ExtractorHash::from_hash( + base.extractor_hash + .parse() + .expect("The db extractor_hash should be valid blake3 hash"), + ), last_status_change: base.last_status_change, parent_subscription_name: base.parent_subscription_name.clone(), priority: base.priority, @@ -116,30 +127,32 @@ pub async fn get_next_video_watchable(app: &App) -> Result<Option<Video>> { } /// Update the cached path of a video. Will be set to NULL if the path is None -pub async fn set_video_cache_path(app: &App, video: &Video, path: Option<&Path>) -> Result<()> { - debug!( - "Setting cache path from '{}' to '{:#?}'", - video.title, - path.unwrap_or(&PathBuf::from("None")).display() - ); - +/// This will also set the status to `Cached` when path is Some, otherwise it set's the status to +/// `Watch`. +pub async fn set_video_cache_path( + app: &App, + video: &ExtractorHash, + path: Option<&Path>, +) -> Result<()> { if let Some(path) = path { debug!( "Setting cache path from '{}' to '{}'", - video.title, + video.into_short_hash(app).await?, path.display() ); let path_str = path.display().to_string(); - let extractor_hash = video.extractor_hash.0.to_string(); + let extractor_hash = video.hash().to_string(); + let status = VideoStatus::Cached.as_db_integer(); query!( r#" UPDATE videos - SET cache_path = ? + SET cache_path = ?, status = ? WHERE extractor_hash = ?; "#, path_str, + status, extractor_hash ) .execute(&app.database) @@ -147,14 +160,21 @@ pub async fn set_video_cache_path(app: &App, video: &Video, path: Option<&Path>) Ok(()) } else { - let extractor_hash = video.extractor_hash.0.to_string(); + debug!( + "Setting cache path from '{}' to 'NULL'", + video.into_short_hash(app).await?, + ); + + let extractor_hash = video.hash().to_string(); + let status = VideoStatus::Watch.as_db_integer(); query!( r#" UPDATE videos - SET cache_path = NULL + SET cache_path = NULL, status = ? WHERE extractor_hash = ?; "#, + status, extractor_hash ) .execute(&app.database) diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/extractor_hash.rs b/pkgs/by-name/yt/yt/src/storage/video_database/extractor_hash.rs index e90a5277..ac2f46ee 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/extractor_hash.rs +++ b/pkgs/by-name/yt/yt/src/storage/video_database/extractor_hash.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; use anyhow::{bail, Result}; use blake3::Hash; @@ -9,35 +9,95 @@ use crate::{app::App, storage::video_database::getters::get_all_hashes}; static EXTRACTOR_HASH_LENGTH: OnceCell<usize> = OnceCell::const_new(); -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtractorHash(pub(super) Hash); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtractorHash { + hash: Hash, +} + +#[derive(Debug, Clone)] +pub struct ShortHash(String); -impl Display for ExtractorHash { +impl Display for ShortHash { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } +#[derive(Debug, Clone)] +pub struct LazyExtractorHash { + value: ShortHash, +} + +impl FromStr for LazyExtractorHash { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { + // perform some cheap validation + if s.len() > 64 { + bail!("A hash can only contain 64 bytes!"); + } + + Ok(Self { + value: ShortHash(s.to_owned()), + }) + } +} + +impl LazyExtractorHash { + /// Turn the [`LazyExtractorHash`] into the [`ExtractorHash`] + pub async fn realize(self, app: &App) -> Result<ExtractorHash> { + ExtractorHash::from_short_hash(app, &self.value).await + } +} + impl ExtractorHash { - pub fn new(hash: Hash) -> Self { - Self(hash) + pub fn from_hash(hash: Hash) -> Self { + Self { hash } + } + pub async fn from_short_hash(app: &App, s: &ShortHash) -> Result<Self> { + Ok(Self { + hash: Self::short_hash_to_full_hash(app, s).await?, + }) + } + + pub fn hash(&self) -> &Hash { + &self.hash } - pub fn parse_from_full_version(s: &str) -> Result<Self> { - assert_eq!(s.len(), 64); + pub async fn into_short_hash(&self, app: &App) -> Result<ShortHash> { + let needed_chars = if let Some(needed_chars) = EXTRACTOR_HASH_LENGTH.get() { + debug!("Using cached char length: {}", needed_chars); + *needed_chars + } else { + let needed_chars = self.get_needed_char_len(app).await?; + debug!("Setting the needed has char lenght."); + EXTRACTOR_HASH_LENGTH + .set(needed_chars) + .expect("This should work at this stage"); + + needed_chars + }; + + debug!("Formatting a hash with char length: {}", needed_chars); - let hash = s.parse()?; - Ok(Self::new(hash)) + Ok(ShortHash( + self.hash() + .to_hex() + .chars() + .into_iter() + .take(needed_chars) + .collect::<String>(), + )) } - pub async fn parse_from_short_version(app: &App, s: &str) -> Result<Self> { + async fn short_hash_to_full_hash(app: &App, s: &ShortHash) -> Result<Hash> { let all_hashes = get_all_hashes(app).await?; - let needed_chars = s.len(); + let needed_chars = s.0.len(); for hash in all_hashes { - if &hash.to_hex()[..needed_chars] == s { - return Ok(Self::new(hash)); + if &hash.to_hex()[..needed_chars] == s.0 { + return Ok(hash); } } @@ -78,29 +138,4 @@ impl ExtractorHash { Ok(needed_chars) } - - pub async fn format(&self, app: &App) -> Result<String> { - let needed_chars = if let Some(needed_chars) = EXTRACTOR_HASH_LENGTH.get() { - debug!("Using cached char length: {}", needed_chars); - *needed_chars - } else { - let needed_chars = self.get_needed_char_len(app).await?; - debug!("Setting the needed has char lenght."); - EXTRACTOR_HASH_LENGTH - .set(needed_chars) - .expect("This should work at this stage"); - - needed_chars - }; - - debug!("Formatting a hash with char length: {}", needed_chars); - - Ok(self - .0 - .to_hex() - .chars() - .into_iter() - .take(needed_chars) - .collect::<String>()) - } } diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/getters.rs b/pkgs/by-name/yt/yt/src/storage/video_database/getters.rs index 6685603c..cec6c426 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/getters.rs +++ b/pkgs/by-name/yt/yt/src/storage/video_database/getters.rs @@ -1,10 +1,7 @@ //! These functions interact with the storage db in a read-only way. They are added on-demaned (as //! you could theoretically just could do everything with the `get_videos` function), as //! performance or convince requires. -use std::{ - fs::{File}, - path::PathBuf, -}; +use std::{fs::File, path::PathBuf}; use anyhow::{bail, Context, Result}; use blake3::Hash; @@ -23,6 +20,42 @@ use crate::{ use super::VideoStatus; +macro_rules! video_from_record { + ($record:expr) => { + let thumbnail_url = if let Some(url) = &$record.thumbnail_url { + Some(Url::parse(&url)?) + } else { + None + }; + + Ok(Video { + cache_path: $record.cache_path.as_ref().map(|val| PathBuf::from(val)), + description: $record.description.clone(), + duration: $record.duration, + extractor_hash: ExtractorHash::from_hash( + $record + .extractor_hash + .parse() + .expect("The db hash should be a valid blake3 hash"), + ), + last_status_change: $record.last_status_change, + parent_subscription_name: $record.parent_subscription_name.clone(), + publish_date: $record.publish_date, + status: VideoStatus::from_db_integer($record.status), + thumbnail_url, + title: $record.title.clone(), + url: Url::parse(&$record.url)?, + priority: $record.priority, + status_change: if $record.status_change == 1 { + true + } else { + assert_eq!($record.status_change, 0); + false + }, + }) + }; +} + /// Get the lines to display at the selection file pub async fn get_videos(app: &App, allowed_states: &[VideoStatus]) -> Result<Vec<Video>> { let mut qb: QueryBuilder<Sqlite> = QueryBuilder::new( @@ -77,9 +110,11 @@ pub async fn get_videos(app: &App, allowed_states: &[VideoStatus]) -> Result<Vec .map(|val| PathBuf::from(val)), description: base.get::<Option<String>, &str>("description").clone(), duration: base.get("duration"), - extractor_hash: ExtractorHash::parse_from_full_version( - &base.get::<String, &str>("extractor_hash"), - )?, + extractor_hash: ExtractorHash::from_hash( + base.get::<String, &str>("extractor_hash") + .parse() + .expect("The db hash should be a valid blake3 hash"), + ), last_status_change: base.get("last_status_change"), parent_subscription_name: base .get::<Option<String>, &str>("parent_subscription_name") @@ -90,7 +125,15 @@ pub async fn get_videos(app: &App, allowed_states: &[VideoStatus]) -> Result<Vec title: base.get::<String, &str>("title").to_owned(), url: Url::parse(base.get("url"))?, priority: base.get("priority"), - status_change: base.get("status_change"), + status_change: { + let val = base.get::<i64, &str>("status_change"); + if val == 1 { + true + } else { + assert_eq!(val, 0, "Can only be 1 or 0"); + false + } + }, }) }) .collect::<Result<Vec<Video>>>()?; @@ -115,6 +158,21 @@ pub async fn get_video_info_json(video: &Video) -> Result<Option<InfoJson>> { } } +pub async fn get_video_by_hash(app: &App, hash: &ExtractorHash) -> Result<Video> { + let ehash = hash.hash().to_string(); + + let raw_video = query!( + " + SELECT * FROM videos WHERE extractor_hash = ?; + ", + ehash + ) + .fetch_one(&app.database) + .await?; + + video_from_record! {raw_video} +} + pub async fn get_currently_playing_video(app: &App) -> Result<Option<Video>> { let mut videos: Vec<Video> = get_changing_videos(app, VideoStatus::Cached).await?; @@ -132,7 +190,27 @@ pub async fn get_currently_playing_video(app: &App) -> Result<Option<Video>> { } pub async fn get_changing_videos(app: &App, old_state: VideoStatus) -> Result<Vec<Video>> { - todo!() + let status = old_state.as_db_integer(); + + let matching = query!( + r#" + SELECT * + FROM videos + WHERE status_change = 1 AND status = ?; + "#, + status + ) + .fetch_all(&app.database) + .await?; + + let real_videos: Vec<Video> = matching + .iter() + .map(|base| -> Result<Video> { + video_from_record! {base} + }) + .collect::<Result<Vec<Video>>>()?; + + Ok(real_videos) } pub async fn get_all_hashes(app: &App) -> Result<Vec<Hash>> { diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/schema.sql b/pkgs/by-name/yt/yt/src/storage/video_database/schema.sql index e4d1e514..2634eef4 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/schema.sql +++ b/pkgs/by-name/yt/yt/src/storage/video_database/schema.sql @@ -1,16 +1,16 @@ -- The base schema -- Keep this table in sync with the `Video` structure CREATE TABLE IF NOT EXISTS videos ( - cache_path TEXT UNIQUE, + cache_path TEXT UNIQUE CHECK (CASE WHEN cache_path IS NOT NULL THEN status == 2 ELSE 1 END), description TEXT, duration FLOAT, extractor_hash TEXT UNIQUE NOT NULL PRIMARY KEY, - last_status_change INTEGER NOT NULL, + last_status_change INTEGER NOT NULL, parent_subscription_name TEXT, - priority INTEGER NOT NULL DEFAULT 0, + priority INTEGER NOT NULL DEFAULT 0, publish_date INTEGER, - status INTEGER NOT NULL CHECK (status IN (0, 1, 2, 3, 4, 5)), - status_change INTEGER NOT NULL CHECK (status_change IN (0, 1)), + 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 ELSE 1 END AND CASE WHEN status != 2 THEN cache_path IS NULL ELSE 1 END), + status_change INTEGER NOT NULL DEFAULT 0 CHECK (status_change IN (0, 1)), thumbnail_url TEXT, title TEXT NOT NULL, url TEXT UNIQUE NOT NULL diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/setters.rs b/pkgs/by-name/yt/yt/src/storage/video_database/setters.rs index 4f1d98cf..251f1e6f 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/setters.rs +++ b/pkgs/by-name/yt/yt/src/storage/video_database/setters.rs @@ -16,7 +16,7 @@ pub async fn set_video_status( new_status: VideoStatus, new_priority: Option<i64>, ) -> Result<()> { - let video_hash = video_hash.0.to_string(); + let video_hash = video_hash.hash().to_string(); let new_status = new_status.as_db_integer(); let old = query!( @@ -74,13 +74,54 @@ pub async fn set_video_status( Ok(()) } +/// Mark a video as watched. +/// This will both set the status to `Watched` and the cache_path to Null. +pub async fn set_video_watched(app: &App, video_hash: &ExtractorHash) -> Result<()> { + // FIXME: Also delete the cache file <2024-08-19> + + let video_hash = video_hash.hash().to_string(); + let new_status = VideoStatus::Watched.as_db_integer(); + + let old = query!( + r#" + SELECT status, priority + FROM videos + WHERE extractor_hash = ? + "#, + video_hash + ) + .fetch_one(&app.database) + .await?; + + if old.status == new_status { + return Ok(()); + } + + 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?; + + Ok(()) +} + pub async fn set_state_change( app: &App, video_extractor_hash: &ExtractorHash, changing: bool, ) -> Result<()> { let state_change = if changing { 1 } else { 0 }; - let video_extractor_hash = video_extractor_hash.to_string(); + let video_extractor_hash = video_extractor_hash.hash().to_string(); query!( r#" @@ -111,14 +152,37 @@ pub async fn add_video(app: &App, video: Video) -> Result<()> { }; let status = video.status.as_db_integer(); + let status_change = if video.status_change { 1 } else { 0 }; let url = video.url.to_string(); - let extractor_hash = video.extractor_hash.0.to_string(); + let extractor_hash = video.extractor_hash.hash().to_string(); query!( r#" - INSERT INTO videos (parent_subscription_name, status, last_status_change, title, url, description, duration, publish_date, thumbnail_url, extractor_hash) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); - "#, parent_subscription_name, status, video.last_status_change, video.title, url, video.description, video.duration, video.publish_date, thumbnail_url, extractor_hash + INSERT INTO videos ( + parent_subscription_name, + status, + status_change, + last_status_change, + title, + url, + description, + duration, + publish_date, + thumbnail_url, + extractor_hash) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + "#, + parent_subscription_name, + status, + status_change, + video.last_status_change, + video.title, + url, + video.description, + video.duration, + video.publish_date, + thumbnail_url, + extractor_hash ) .execute(&app.database) .await?; diff --git a/pkgs/by-name/yt/yt/src/update/mod.rs b/pkgs/by-name/yt/yt/src/update/mod.rs index 2f97361d..2cbae9fd 100644 --- a/pkgs/by-name/yt/yt/src/update/mod.rs +++ b/pkgs/by-name/yt/yt/src/update/mod.rs @@ -1,10 +1,10 @@ -use std::{env::current_exe, str::FromStr}; +use std::{env::current_exe, str::FromStr, sync::Arc}; use anyhow::{bail, Context, Ok, Result}; use chrono::{DateTime, Utc}; use log::{debug, error, info, warn}; use serde_json::{json, Value}; -use tokio::{process::Command, task::JoinSet}; +use tokio::{process::Command, sync::Semaphore, task::JoinSet}; use yt_dlp::unsmuggle_url; use crate::{ @@ -12,37 +12,51 @@ use crate::{ storage::{ subscriptions::{get_subscriptions, Subscription}, video_database::{ - extractor_hash::ExtractorHash, getters::get_video_hashes, setters::add_video, - Video, VideoStatus, + extractor_hash::ExtractorHash, getters::get_video_hashes, setters::add_video, Video, + VideoStatus, }, }, }; -pub async fn update(max_backlog: u32, subs_to_update: Vec<String>) -> Result<()> { +pub async fn update( + max_backlog: u32, + subs_to_update: Vec<String>, + concurrent_processes: usize, +) -> Result<()> { let subscriptions = get_subscriptions()?; let mut join_set = JoinSet::new(); + let permits = Arc::new(Semaphore::const_new(concurrent_processes)); - for key in subscriptions.0.keys() { + for key in subscriptions.0.into_keys() { if subs_to_update.contains(&key) || subs_to_update.is_empty() { - let exe_name = current_exe().context("Failed to get the current executable")?; + let new_permits = Arc::clone(&permits); - let mut child = Command::new(exe_name) - // TODO: Add currying of the verbosity flags <2024-07-28> - // .arg("-vvvv") - .arg("update-once") - .arg(&key) - .arg(max_backlog.to_string()) - .spawn() - .context("Failed to call yt update-once")?; + join_set.spawn(async move { + let _permit = new_permits + .acquire() + .await + .expect("The semaphore should not be closed"); + + debug!( + "Started downloading: `yt 'update-once' '{}' '{}'`", + &key, + max_backlog.to_string() + ); - let new_key = key.clone(); + let exe_name = current_exe().context("Failed to get the current executable")?; + let mut child = Command::new(exe_name) + // TODO: Add currying of the verbosity flags <2024-07-28> + // .arg("-vvvv") + .arg("update-once") + .arg(&key) + .arg(max_backlog.to_string()) + .spawn() + .context("Failed to call yt update-once")?; - // PERFORMANCE: Switch back to a somewhat parallellied version <2024-07-29> - let output = child.wait().await; - join_set.spawn(async move { - // let output = child.wait().await; - (output, new_key) + let output = child.wait().await; + + Ok((output, key)) }); } else { info!( @@ -53,7 +67,7 @@ pub async fn update(max_backlog: u32, subs_to_update: Vec<String>) -> Result<()> } while let Some(res) = join_set.join_next().await { - let (output, key) = res?; + let (output, key) = res??; debug!("{} finished its update.", &key); match output { @@ -199,7 +213,7 @@ async fn update_subscription(app: &App, sub: &Subscription, max_backlog: u32) -> cache_path: None, description: entry.description.clone(), duration: entry.duration, - extractor_hash: ExtractorHash::new(extractor_hash), + extractor_hash: ExtractorHash::from_hash(extractor_hash), last_status_change: Utc::now().timestamp(), parent_subscription_name: Some(sub.name.clone()), priority: 0, diff --git a/pkgs/by-name/yt/yt/src/watch/events.rs b/pkgs/by-name/yt/yt/src/watch/events.rs new file mode 100644 index 00000000..cab6807f --- /dev/null +++ b/pkgs/by-name/yt/yt/src/watch/events.rs @@ -0,0 +1,160 @@ +use std::{env::current_exe, usize}; + +use anyhow::{bail, Result}; +use libmpv2::{events::Event, EndFileReason, Mpv}; +use log::{debug, info}; +use tokio::process::Command; + +use crate::{ + app::App, + comments::get_comments, + constants::LOCAL_COMMENTS_LENGTH, + storage::video_database::{ + extractor_hash::ExtractorHash, + setters::{set_state_change, set_video_watched}, + }, +}; + +pub struct MpvEventHandler { + currently_playing_index: Option<usize>, + current_playlist_position: usize, + current_playlist: Vec<ExtractorHash>, +} + +impl MpvEventHandler { + pub fn from_playlist(playlist: Vec<ExtractorHash>) -> Self { + Self { + currently_playing_index: None, + current_playlist: playlist, + current_playlist_position: 0, + } + } + + async fn mark_cvideo_watched(&mut self, app: &App) -> Result<()> { + if let Some(index) = self.currently_playing_index { + let video_hash = &self.current_playlist[(index) as usize]; + set_video_watched(&app, video_hash).await?; + } + Ok(()) + } + + async fn mark_cvideo_inactive(&mut self, app: &App) -> Result<()> { + if let Some(index) = self.currently_playing_index { + let video_hash = &self.current_playlist[(index) as usize]; + self.currently_playing_index = None; + set_state_change(&app, video_hash, false).await?; + } + Ok(()) + } + async fn mark_video_active(&mut self, app: &App, playlist_index: usize) -> Result<()> { + let video_hash = &self.current_playlist[(playlist_index) as usize]; + self.currently_playing_index = Some(playlist_index); + set_state_change(&app, video_hash, true).await?; + Ok(()) + } + + /// This will return [`true`], if the event handling should be stopped + pub async fn handle_mpv_event<'a>( + &mut self, + app: &App, + mpv: &Mpv, + event: Event<'a>, + ) -> Result<bool> { + match event { + Event::EndFile(r) => match r { + EndFileReason::Eof => { + info!("Mpv reached eof of current video. Marking it watched."); + + self.mark_cvideo_watched(app).await?; + self.mark_cvideo_inactive(app).await?; + } + EndFileReason::Stop => {} + EndFileReason::Quit => { + info!("Mpv quit. Exiting playback"); + + self.mark_cvideo_watched(app).await?; + self.mark_cvideo_inactive(app).await?; + return Ok(true); + } + EndFileReason::Error => { + unreachable!("This have raised a separate error") + } + EndFileReason::Redirect => { + todo!("We probably need to handle this somehow"); + } + }, + Event::StartFile(playlist_index) => { + self.mark_video_active(app, (playlist_index - 1) as usize) + .await?; + self.current_playlist_position = (playlist_index - 1) as usize; + } + Event::FileLoaded => {} + Event::ClientMessage(a) => { + debug!("Got Client Message event: '{}'", a.join(" ")); + + match a.as_slice() { + &["yt-comments-external"] => { + let binary = current_exe().expect("A current exe should exist"); + + let status = Command::new("riverctl") + .args(["focus-output", "next"]) + .status() + .await?; + if !status.success() { + bail!("focusing the next output failed!"); + } + + let status = Command::new("alacritty") + .args(&[ + "--title", + "floating please", + "--command", + binary.to_str().expect("Should be valid unicode"), + "comments", + ]) + .status() + .await?; + if !status.success() { + bail!("Falied to start `yt comments`"); + } + + let status = Command::new("riverctl") + .args(["focus-output", "next"]) + .status() + .await?; + if !status.success() { + bail!("focusing the next output failed!"); + } + } + &["yt-comments-local"] => { + let comments: String = get_comments(app) + .await? + .render(false) + .replace("\"", "") + .replace("'", "") + .chars() + .take(LOCAL_COMMENTS_LENGTH) + .collect(); + + mpv.execute("show-text", &[&format!("'{}'", comments), "6000"])?; + } + &["yt-description"] => { + // let description = description(app).await?; + mpv.execute("script-message", &["osc-message", "'<YT Description>'"])?; + } + &["yt-mark-watch-later"] => { + self.mark_cvideo_inactive(app).await?; + mpv.execute("write-watch-later-config", &[])?; + mpv.execute("playlist-remove", &["current"])?; + } + other => { + debug!("Unknown message: {}", other.join(" ")) + } + } + } + _ => {} + } + + Ok(false) + } +} diff --git a/pkgs/by-name/yt/yt/src/watch/mod.rs b/pkgs/by-name/yt/yt/src/watch/mod.rs index 683d49c1..3a4f8983 100644 --- a/pkgs/by-name/yt/yt/src/watch/mod.rs +++ b/pkgs/by-name/yt/yt/src/watch/mod.rs @@ -1,19 +1,17 @@ use anyhow::Result; -use libmpv2::{ - events::{Event, EventContext}, - Mpv, -}; -use log::{debug, info}; +use events::MpvEventHandler; +use libmpv2::{events::EventContext, Mpv}; +use log::{debug, info, warn}; use crate::{ app::App, cache::maintain, - comments::get_comments, - storage::video_database::{ - extractor_hash::ExtractorHash, getters::get_videos, setters::set_state_change, VideoStatus, - }, + constants::{mpv_config_path, mpv_input_path}, + storage::video_database::{extractor_hash::ExtractorHash, getters::get_videos, VideoStatus}, }; +pub mod events; + pub async fn watch(app: &App) -> Result<()> { maintain(app, false).await?; @@ -22,11 +20,43 @@ pub async fn watch(app: &App) -> Result<()> { // the player (and e.g. close the window). mpv.set_property("input-default-bindings", "yes")?; mpv.set_property("input-vo-keyboard", "yes")?; + // Show the on screen controller. mpv.set_property("osc", "yes")?; + + // Don't automatically advance to the next video (or exit the player) + mpv.set_option("keep-open", "always")?; Ok(()) })?; + let config_path = mpv_config_path()?; + if config_path.try_exists()? { + info!("Found mpv.conf at '{}'!", config_path.display()); + mpv.execute( + "load-config-file", + &[config_path.to_str().expect("This should be utf8-able")], + )?; + } else { + warn!( + "Did not find a mpv.conf file at '{}'", + config_path.display() + ); + } + + let input_path = mpv_input_path()?; + if input_path.try_exists()? { + info!("Found mpv.input.conf at '{}'!", input_path.display()); + mpv.execute( + "load-input-conf", + &[input_path.to_str().expect("This should be utf8-able")], + )?; + } else { + warn!( + "Did not find a mpv.input.conf file at '{}'", + input_path.display() + ); + } + let mut ev_ctx = EventContext::new(mpv.ctx); ev_ctx.disable_deprecated_events()?; @@ -58,37 +88,17 @@ pub async fn watch(app: &App) -> Result<()> { playlist_cache.push(play_thing.extractor_hash); } + let mut mpv_event_handler = MpvEventHandler::from_playlist(playlist_cache); loop { if let Some(ev) = ev_ctx.wait_event(600.) { match ev { - Ok(Event::EndFile(r)) => { - println!("Exiting! Reason: {:?}", r); - break; - } - Ok(Event::StartFile(playlist_index)) => { - let video_hash = &playlist_cache[playlist_index as usize]; - set_state_change(&app, video_hash, true).await?; - } - Ok(Event::ClientMessage(a)) => { - debug!("Got Client Message event: '{}'", a.join(" ")); - - match a.as_slice() { - &["yt-comments"] => { - let comments = get_comments(app).await?.render(false); - mpv.execute("script-message", &["osc-message", &comments])?; - } - &["yt-description"] => { - // let description = description(app).await?; - mpv.execute("script-message", &["osc-message", "'<YT Description>'"])?; - } - other => { - debug!("Unknown message: {}", other.join(" ")) - } + Ok(event) => { + debug!("Mpv event triggered: {:#?}", event); + if mpv_event_handler.handle_mpv_event(app, &mpv, event).await? { + break; } } - - Ok(e) => println!("Event triggered: {:#?}", e), - Err(e) => println!("Event errored: {}", e), + Err(e) => debug!("Mpv Event errored: {}", e), } } } diff --git a/pkgs/by-name/yt/yt/yt_dlp/src/lib.rs b/pkgs/by-name/yt/yt/yt_dlp/src/lib.rs index 8425d0d3..851c9ab7 100644 --- a/pkgs/by-name/yt/yt/yt_dlp/src/lib.rs +++ b/pkgs/by-name/yt/yt/yt_dlp/src/lib.rs @@ -1,4 +1,4 @@ -use std::{fs::File, io::Write}; +// use std::{fs::File, io::Write}; use std::{path::PathBuf, sync::Once}; @@ -283,8 +283,8 @@ pub async fn extract_info( let result_str = json_dumps(py, result)?; - let mut file = File::create("output.info.json").unwrap(); - write!(file, "{}", result_str).unwrap(); + //let mut file = File::create("output.info.json").unwrap(); + //write!(file, "{}", result_str).unwrap(); Ok(serde_json::from_str(&result_str) .expect("Python should be able to produce correct json")) diff --git a/pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs b/pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs index 8673998e..e9bf0402 100644 --- a/pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs +++ b/pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs @@ -155,7 +155,7 @@ pub struct RequestedDownloads { pub fps: f64, pub height: u32, pub infojson_filename: PathBuf, - pub language: String, + pub language: Option<String>, pub protocol: String, pub requested_formats: Vec<Format>, pub resolution: String, @@ -444,6 +444,7 @@ pub struct Format { 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>, |