diff --git a/Cargo.lock b/Cargo.lock index 89c43c4..095f161 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,18 +12,29 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" name = "ae_anons" version = "0.1.0" dependencies = [ + "anyhow", "calamine", "chrono", - "csv", "dotenv", + "futures", + "regex", "reqwest", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "urlencoding", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -33,6 +44,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arbitrary" version = "1.4.2" @@ -54,28 +71,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-lc-rs" -version = "1.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "base64" version = "0.22.1" @@ -128,29 +123,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.44" @@ -164,15 +145,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "codepage" version = "0.1.2" @@ -182,16 +154,6 @@ dependencies = [ "encoding_rs", ] -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -233,27 +195,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -282,12 +223,6 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -313,6 +248,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -335,6 +276,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -345,10 +301,19 @@ dependencies = [ ] [[package]] -name = "fs_extra" -version = "1.3.0" +name = "futures" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] [[package]] name = "futures-channel" @@ -357,6 +322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -365,6 +331,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -400,6 +377,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -417,10 +395,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -430,11 +406,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasip2", - "wasm-bindgen", ] [[package]] @@ -537,6 +511,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -721,60 +711,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.95" @@ -793,6 +729,12 @@ version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -814,12 +756,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "memchr" version = "2.8.0" @@ -863,6 +799,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -878,12 +831,50 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl" +version = "0.10.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-sys" +version = "0.9.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -919,6 +910,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "potential_utf" version = "0.1.5" @@ -928,15 +925,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -956,62 +944,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.52.0", -] - [[package]] name = "quote" version = "1.0.45" @@ -1027,35 +959,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -1066,10 +969,39 @@ dependencies = [ ] [[package]] -name = "reqwest" -version = "0.13.2" +name = "regex" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -1082,22 +1014,22 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", "mime", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", "rustls-pki-types", - "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-native-tls", "tokio-util", "tower", "tower-http", @@ -1124,10 +1056,17 @@ dependencies = [ ] [[package]] -name = "rustc-hash" -version = "2.1.2" +name = "rustix" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] [[package]] name = "rustls" @@ -1135,7 +1074,6 @@ version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ - "aws-lc-rs", "once_cell", "rustls-pki-types", "rustls-webpki", @@ -1143,62 +1081,21 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "web-time", "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1216,15 +1113,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "schannel" version = "0.1.29" @@ -1306,6 +1194,18 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1415,12 +1315,16 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "1.0.69" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "thiserror-impl 1.0.69", + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -1429,18 +1333,7 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1464,21 +1357,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.52.0" @@ -1507,6 +1385,16 @@ dependencies = [ "syn", ] +[[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.4" @@ -1643,14 +1531,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "walkdir" -version = "2.5.0" +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "want" @@ -1733,9 +1617,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.5.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -1754,34 +1638,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -1852,22 +1708,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1879,67 +1726,34 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1952,48 +1766,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2035,26 +1825,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.7" @@ -2128,7 +1898,7 @@ dependencies = [ "flate2", "indexmap", "memchr", - "thiserror 2.0.18", + "thiserror", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index 0031ae7..4e5fcd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,21 @@ [package] name = "ae_anons" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] -reqwest = { version = "0.13", features = ["json", "multipart", "stream"] } +reqwest = { version = "0.12", features = ["json", "multipart", "stream"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2.0" urlencoding = "2.1" tokio = { version = "1.0", features = ["full"] } -csv = "1.3" dotenv = "0.15" calamine = "0.26" chrono = "0.4" +regex = "1.11" +futures = "0.3" +anyhow = "1.0" [dev-dependencies] tokio = { version = "1.0", features = ["full", "rt-multi-thread"] } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 9c37fc1..4e24aec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,24 @@ +mod nexrender; +mod synology; + +use anyhow::{anyhow, Context, Result}; use calamine::{Data, Reader, Xlsx}; use chrono::{Duration, NaiveDate}; use dotenv::dotenv; +use nexrender::{JobData, LogoRegistry}; +use reqwest::Client; use serde_json::Value; use std::collections::HashMap; use std::env; use std::fs::File; use std::io::Write; use std::path::Path; +use std::time::Duration as StdDuration; +use synology::SynologyClient; +use tokio::time::sleep; -use ae_anons::{SynologyClient, SynologyError}; +const NEXRENDER_API: &str = "http://10.10.2.20:3000/api/v1/jobs"; +const OUTPUT_FOLDER: &str = "//10.10.35.3/edit/Auto_Anons"; /// Универсальная структура для хранения данных любого листа #[derive(Debug, Clone)] @@ -19,28 +29,6 @@ pub struct SheetData { } impl SheetData { - /// Получить значение по индексу строки и имени колонки - pub fn get(&self, row_idx: usize, col_name: &str) -> Option<&String> { - self.rows.get(row_idx).and_then(|row| row.get(col_name)) - } - - /// Получить все значения колонки - pub fn get_column(&self, col_name: &str) -> Vec> { - self.rows.iter().map(|row| row.get(col_name)).collect() - } - - /// Получить строку как JSON - pub fn row_to_json(&self, row_idx: usize) -> Option { - self.rows.get(row_idx).map(|row| { - let map: serde_json::Map = row - .iter() - .map(|(k, v)| (k.clone(), Value::String(v.clone()))) - .collect(); - Value::Object(map) - }) - } - - /// Конвертировать весь лист в JSON pub fn to_json(&self) -> Value { let rows_json: Vec = self .rows @@ -64,56 +52,38 @@ impl SheetData { } } -/// Структура для хранения всех листов Excel #[derive(Debug, Default)] pub struct ExcelWorkbook { pub sheets: Vec, } impl ExcelWorkbook { - /// Получить лист по имени pub fn get_sheet(&self, name: &str) -> Option<&SheetData> { self.sheets.iter().find(|s| s.name == name) } - /// Получить лист по индексу - pub fn get_sheet_by_index(&self, index: usize) -> Option<&SheetData> { - self.sheets.get(index) - } - - /// Конвертировать всю книгу в JSON pub fn to_json(&self) -> Value { let sheets_json: Vec = self.sheets.iter().map(|s| s.to_json()).collect(); serde_json::json!(sheets_json) } - - /// Получить все заголовки всех листов - pub fn get_all_headers(&self) -> HashMap> { - self.sheets - .iter() - .map(|sheet| (sheet.name.clone(), sheet.headers.clone())) - .collect() - } } #[tokio::main] -async fn main() -> Result<(), SynologyError> { +async fn main() -> Result<()> { dotenv().ok(); - let nas_fqdn = env::var("NAS_FQDN").expect("❌ Переменная NAS_FQDN не задана в .env"); - let nas_user = env::var("NAS_USER").expect("❌ Переменная NAS_USER не задана в .env"); - let nas_pass = env::var("NAS_PASS").expect("❌ Переменная NAS_PASS не задана в .env"); - let nas_file = env::var("NAS_FILE").expect("❌ Переменная NAS_FILE не задана в .env"); + let nas_fqdn = env::var("NAS_FQDN").context("NAS_FQDN not set")?; + let nas_user = env::var("NAS_USER").context("NAS_USER not set")?; + let nas_pass = env::var("NAS_PASS").context("NAS_PASS not set")?; + let nas_file = env::var("NAS_FILE").context("NAS_FILE not set")?; let mut client = SynologyClient::new(&nas_fqdn); - client.login(&nas_user, &nas_pass).await?; println!("✅ Logged in"); let info = client.get_info().await?; println!("📡 Connected to: {}", info.hostname); - // Получаем имя файла let file_name_full = Path::new(&nas_file) .file_name() .and_then(|n| n.to_str()) @@ -129,20 +99,15 @@ async fn main() -> Result<(), SynologyError> { .replace(' ', ""); let full_expected_path_api = format!("{}/{}", api_format_path, file_name_full); - println!("\n🔍 Looking for file: {}", full_expected_path_api); - // Ищем файл let search_result = client.search_file_by_name(&search_name).await?; - let items = search_result["data"]["items"].as_array().ok_or_else(|| { - SynologyError::InvalidResponseFormat("No items in search result".to_string()) - })?; + let items = search_result["data"]["items"] + .as_array() + .ok_or_else(|| anyhow!("No items in search result"))?; if items.is_empty() { - return Err(SynologyError::Api { - code: 404, - message: Some(format!("File '{}' not found", file_name_full)), - }); + return Err(anyhow!("File '{}' not found", file_name_full)); } let exact_match: Vec<&Value> = items @@ -156,23 +121,19 @@ async fn main() -> Result<(), SynologyError> { .collect(); if exact_match.is_empty() { - return Err(SynologyError::Api { - code: 400, - message: Some(format!("File not found at: {}", full_expected_path_api)), - }); + return Err(anyhow!("File not found at: {}", full_expected_path_api)); } let file = exact_match[0]; let file_id = file["file_id"] .as_str() - .ok_or_else(|| SynologyError::InvalidResponseFormat("Missing file_id".to_string()))?; + .ok_or_else(|| anyhow!("Missing file_id"))?; let actual_file_name = file["name"] .as_str() - .ok_or_else(|| SynologyError::InvalidResponseFormat("Missing name".to_string()))?; + .ok_or_else(|| anyhow!("Missing name"))?; println!(" 📄 Found: {}", actual_file_name); - // Экспортируем файл println!("\n📥 Exporting file..."); let output_path = format!("./{}_export.xlsx", search_name); @@ -181,11 +142,9 @@ async fn main() -> Result<(), SynologyError> { file.write_all(&data)?; println!(" ✅ Exported to: {} ({} bytes)", output_path, data.len()); - // Читаем Excel и получаем структурированные данные println!("\n📖 Parsing Excel file..."); let workbook = read_excel_workbook(&output_path)?; - // Выводим информацию о структуре println!("\n📊 Excel Structure:"); println!(" Total sheets: {}", workbook.sheets.len()); @@ -197,50 +156,131 @@ async fn main() -> Result<(), SynologyError> { sheet.headers ); println!(" Rows: {}", sheet.rows.len()); - - // Показываем пример данных из первой строки - if let Some(first_row) = sheet.rows.first() { - println!(" First row sample:"); - for (key, value) in first_row.iter().take(5) { - println!(" {} = {}", key, value); - } - if first_row.len() > 5 { - println!(" ... and {} more fields", first_row.len() - 5); - } - } } - // Сохраняем всю книгу в JSON + // Сохраняем JSON let json_output = format!("./{}_workbook.json", search_name); let json_data = workbook.to_json(); - let json_str = serde_json::to_string_pretty(&json_data) - .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; - std::fs::write(&json_output, json_str)?; + std::fs::write(&json_output, serde_json::to_string_pretty(&json_data)?)?; println!("\n💾 Full workbook saved to: {}", json_output); - // Сохраняем каждый лист отдельно for sheet in &workbook.sheets { let safe_name = sheet .name .replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], "_"); let sheet_json_path = format!("./{}_{}.json", search_name, safe_name); - let sheet_json = sheet.to_json(); - let sheet_json_str = serde_json::to_string_pretty(&sheet_json) - .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; - std::fs::write(&sheet_json_path, sheet_json_str)?; - println!("💾 Sheet '{}' saved to: {}", sheet.name, sheet_json_path); + std::fs::write( + &sheet_json_path, + serde_json::to_string_pretty(&sheet.to_json())?, + )?; + } + + // ============ NEXRENDER INTEGRATION ============ + println!("\n🎬 Creating Nexrender jobs..."); + + let start_sheet = workbook + .get_sheet("Start") + .ok_or_else(|| anyhow!("Sheet 'Start' not found"))?; + let sport_sheet = workbook + .get_sheet("SPORT") + .ok_or_else(|| anyhow!("Sheet 'SPORT' not found"))?; + let teams_sheet = workbook + .get_sheet("TEAMS") + .ok_or_else(|| anyhow!("Sheet 'TEAMS' not found"))?; + let chanell_sheet = workbook + .get_sheet("CHANELL") + .ok_or_else(|| anyhow!("Sheet 'CHANELL' not found"))?; + + let packs: HashMap = sport_sheet + .rows + .iter() + .filter_map(|row| Some((row.get("SPORT")?.clone(), row.get("LINK")?.clone()))) + .collect(); + println!(" 📦 Loaded {} sport packs", packs.len()); + + let mut logos = LogoRegistry::new(); + for row in &teams_sheet.rows { + if let (Some(team), Some(sport), Some(link)) = + (row.get("TEAM"), row.get("SPORT"), row.get("LINK")) + { + logos.insert(team.clone(), sport.clone(), link.clone()); + } + } + println!(" 🏷️ Loaded {} team logos", teams_sheet.rows.len()); + + // Загружаем логотипы каналов из листа CHANELL + let channels: HashMap = chanell_sheet + .rows + .iter() + .filter_map(|row| Some((row.get("CHANELL")?.clone(), row.get("LINK")?.clone()))) + .collect(); + println!(" 📺 Loaded {} channel logos", channels.len()); + + let mut jobs: Vec = Vec::new(); + for (idx, row) in start_sheet.rows.iter().enumerate() { + if let Some(state) = row.get("STATE") { + if state == "FALSE" { + if let Some(job) = JobData::from_row(row, idx, &packs, &logos, &channels) { + println!(" ✅ Job for row {}: {}", idx, job.outfile_name); + jobs.extend(job.create_variants()); + } + } + } + } + + println!("\n📦 Created {} total jobs (with variants)", jobs.len()); + + if jobs.is_empty() { + println!(" ℹ️ No jobs with STATE='FALSE' found"); + client.logout().await?; + println!("\n👋 Logged out"); + return Ok(()); + } + + cleanup_finished_jobs().await?; + + let http_client = Client::new(); + let mut submitted_jobs: Vec<(String, String)> = Vec::new(); + + for job in &jobs { + let nexrender_job = job.to_nexrender_job(OUTPUT_FOLDER); + println!(" 📤 Submitting: {}", job.outfile_name); + + let response = http_client + .post(NEXRENDER_API) + .json(&nexrender_job) + .send() + .await?; + + if response.status().is_success() { + let result: Value = response.json().await?; + if let Some(uid) = result.get("uid").and_then(|u| u.as_str()) { + submitted_jobs.push((uid.to_string(), job.outfile_name.clone())); + println!(" ✅ Submitted, uid: {}", uid); + } + } else { + let status = response.status(); + let text = response.text().await.unwrap_or_default(); + eprintln!(" ❌ Failed to submit job ({}): {}", status, text); + } + } + + println!("\n📊 Submitted {} jobs to nexrender", submitted_jobs.len()); + + if !submitted_jobs.is_empty() { + println!("\n⏳ Monitoring job completion..."); + monitor_jobs(&http_client, submitted_jobs).await?; } client.logout().await?; - println!("\n👋 Logged out"); + println!("\n👋 Done"); Ok(()) } -/// Читает Excel файл и возвращает структурированную книгу -fn read_excel_workbook(file_path: &str) -> Result { +fn read_excel_workbook(file_path: &str) -> Result { let mut workbook: Xlsx<_> = calamine::open_workbook(file_path) - .map_err(|e| SynologyError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?; + .map_err(|e| anyhow!("Failed to open workbook: {}", e))?; let sheet_names = workbook.sheet_names().to_vec(); let mut excel_workbook = ExcelWorkbook::default(); @@ -248,13 +288,9 @@ fn read_excel_workbook(file_path: &str) -> Result for sheet_name in sheet_names { println!(" 📄 Processing sheet: '{}'", sheet_name); - let range = match workbook.worksheet_range(&sheet_name) { - Ok(range) => range, - Err(e) => { - println!(" ⚠️ Error reading sheet: {}", e); - continue; - } - }; + let range = workbook + .worksheet_range(&sheet_name) + .map_err(|e| anyhow!("Failed to read sheet '{}': {}", sheet_name, e))?; let sheet_data = parse_sheet_dynamic(&sheet_name, &range)?; excel_workbook.sheets.push(sheet_data); @@ -263,12 +299,7 @@ fn read_excel_workbook(file_path: &str) -> Result Ok(excel_workbook) } -/// Парсит лист в динамическую структуру -fn parse_sheet_dynamic( - sheet_name: &str, - range: &calamine::Range, -) -> Result { - // Собираем все ячейки в структуру +fn parse_sheet_dynamic(sheet_name: &str, range: &calamine::Range) -> Result { let mut cell_map: HashMap<(usize, usize), String> = HashMap::new(); let mut max_row = 0usize; let mut max_col = 0usize; @@ -290,19 +321,15 @@ fn parse_sheet_dynamic( }); } - // Создаем матрицу данных let mut data_matrix: Vec> = Vec::new(); - for row in 0..=max_row { let mut row_data = Vec::new(); for col in 0..=max_col { - let value = cell_map.get(&(row, col)).cloned().unwrap_or_default(); - row_data.push(value); + row_data.push(cell_map.get(&(row, col)).cloned().unwrap_or_default()); } data_matrix.push(row_data); } - // Первая строка - заголовки let headers: Vec = if !data_matrix.is_empty() { data_matrix[0] .iter() @@ -320,32 +347,22 @@ fn parse_sheet_dynamic( Vec::new() }; - println!(" Found {} headers: {:?}", headers.len(), headers); - let mut rows_data = Vec::new(); - - // Остальные строки - данные for row_values in data_matrix.iter().skip(1) { let mut row_map = HashMap::new(); - for (col_idx, value) in row_values.iter().enumerate() { if col_idx < headers.len() && !value.is_empty() { row_map.insert(headers[col_idx].clone(), value.clone()); } } - if !row_map.is_empty() { rows_data.push(row_map); } - if rows_data.len() >= 10000 { - println!(" ... truncated at 10000 rows"); break; } } - println!(" Loaded {} data rows", rows_data.len()); - Ok(SheetData { name: sheet_name.to_string(), headers, @@ -353,12 +370,9 @@ fn parse_sheet_dynamic( }) } -/// Преобразует Excel серийный номер даты в строку формата DD.MM.YYYY fn excel_date_to_string(serial: f64) -> String { let days = serial as i64; - // Excel использует 30.12.1899 как базовую дату let base = NaiveDate::from_ymd_opt(1899, 12, 30).unwrap(); - if let Some(date) = base.checked_add_signed(Duration::days(days)) { date.format("%d.%m.%Y").to_string() } else { @@ -366,13 +380,10 @@ fn excel_date_to_string(serial: f64) -> String { } } -/// Проверяет, является ли число датой Excel fn is_excel_date(value: f64) -> bool { - // Excel даты обычно в диапазоне от 1 до 100000 - value > 1.0 && value < 100000.0 + (1.0..100000.0).contains(&value) } -/// Преобразует ячейку в строку с конвертацией дат fn cell_to_string(cell: &Data) -> String { match cell { Data::Empty => String::new(), @@ -408,3 +419,65 @@ fn cell_to_string(cell: &Data) -> String { Data::Error(e) => format!("{:?}", e), } } + +async fn cleanup_finished_jobs() -> Result<()> { + let client = Client::new(); + let response = client.get(NEXRENDER_API).send().await?; + + if response.status().is_success() { + let jobs: Vec = response.json().await?; + for job in jobs { + if let (Some(uid), Some(state)) = ( + job.get("uid").and_then(|u| u.as_str()), + job.get("state").and_then(|s| s.as_str()), + ) { + if state == "finished" || state == "error" { + client + .delete(&format!("{}/{}", NEXRENDER_API, uid)) + .send() + .await?; + println!(" 🧹 Cleaned up job: {}", uid); + } + } + } + } + Ok(()) +} + +async fn monitor_jobs(client: &Client, mut pending: Vec<(String, String)>) -> Result<()> { + while !pending.is_empty() { + sleep(StdDuration::from_secs(25)).await; + + let mut remaining = Vec::new(); + for (uid, outname) in &pending { + let response = client + .get(&format!("{}/{}", NEXRENDER_API, uid)) + .send() + .await?; + + if response.status().is_success() { + let job: Value = response.json().await?; + let state = job + .get("state") + .and_then(|s| s.as_str()) + .unwrap_or("unknown"); + + match state { + "finished" => { + println!(" ✅ {} - finished ({} left)", outname, pending.len() - 1) + } + "error" => eprintln!(" ❌ {} - error ({} left)", outname, pending.len() - 1), + _ => remaining.push((uid.clone(), outname.clone())), + } + } else { + remaining.push((uid.clone(), outname.clone())); + } + } + pending = remaining; + if !pending.is_empty() { + println!(" ⏳ Waiting for {} jobs...", pending.len()); + } + } + println!("\n🎉 All jobs completed!"); + Ok(()) +} diff --git a/src/nexrender.rs b/src/nexrender.rs new file mode 100644 index 0000000..8fad865 --- /dev/null +++ b/src/nexrender.rs @@ -0,0 +1,586 @@ +use chrono::{Datelike, NaiveDate}; +use serde::Serialize; +use serde_json::json; +use std::collections::HashMap; + +fn transliterate(text: &str, _reversed: bool) -> String { + let mapping = [ + ("а", "a"), + ("б", "b"), + ("в", "v"), + ("г", "g"), + ("д", "d"), + ("е", "e"), + ("ё", "yo"), + ("ж", "zh"), + ("з", "z"), + ("и", "i"), + ("й", "y"), + ("к", "k"), + ("л", "l"), + ("м", "m"), + ("н", "n"), + ("о", "o"), + ("п", "p"), + ("р", "r"), + ("с", "s"), + ("т", "t"), + ("у", "u"), + ("ф", "f"), + ("х", "h"), + ("ц", "ts"), + ("ч", "ch"), + ("ш", "sh"), + ("щ", "sch"), + ("ъ", ""), + ("ы", "y"), + ("ь", ""), + ("э", "e"), + ("ю", "yu"), + ("я", "ya"), + ("А", "A"), + ("Б", "B"), + ("В", "V"), + ("Г", "G"), + ("Д", "D"), + ("Е", "E"), + ("Ё", "Yo"), + ("Ж", "Zh"), + ("З", "Z"), + ("И", "I"), + ("Й", "Y"), + ("К", "K"), + ("Л", "L"), + ("М", "M"), + ("Н", "N"), + ("О", "O"), + ("П", "P"), + ("Р", "R"), + ("С", "S"), + ("Т", "T"), + ("У", "U"), + ("Ф", "F"), + ("Х", "H"), + ("Ц", "Ts"), + ("Ч", "Ch"), + ("Ш", "Sh"), + ("Щ", "Sch"), + ("Ъ", ""), + ("Ы", "Y"), + ("Ь", ""), + ("Э", "E"), + ("Ю", "Yu"), + ("Я", "Ya"), + ]; + let mut result = text.to_string(); + for (cyr, lat) in mapping { + result = result.replace(cyr, lat); + } + result +} + +#[derive(Debug, Clone, Serialize)] +pub struct Template { + pub src: String, + pub composition: String, + #[serde(rename = "outputModule")] + pub output_module: String, + #[serde(rename = "outputExt")] + pub output_ext: String, +} + +impl Default for Template { + fn default() -> Self { + Self { + src: "file:///c:/users/virtVmix-2/Downloads/PackShot_DOUBLE.aepx".to_string(), + composition: "pack".to_string(), + output_module: "Start_h264".to_string(), + output_ext: "mp4".to_string(), + } + } +} + +impl Template { + pub fn single() -> Self { + Self { + src: "file:///c:/users/virtVmix-2/Downloads/PackShot_SINGLE.aepx".to_string(), + ..Default::default() + } + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "module")] +pub enum PostrenderAction { + #[serde(rename = "@nexrender/action-encode")] + Encode { preset: String, output: String }, + #[serde(rename = "@nexrender/action-copy")] + Copy { input: String, output: String }, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum Asset { + Data { + #[serde(rename = "type")] + asset_type: String, + #[serde(rename = "layerName")] + layer_name: String, + property: String, + value: serde_json::Value, + }, + DataExpression { + #[serde(rename = "type")] + asset_type: String, + #[serde(rename = "layerName")] + layer_name: String, + property: String, + expression: String, + }, + Image { + src: String, + #[serde(rename = "type")] + asset_type: String, + #[serde(rename = "layerName")] + layer_name: String, + }, + Video { + src: String, + #[serde(rename = "type")] + asset_type: String, + #[serde(rename = "layerName")] + layer_name: String, + }, +} + +#[derive(Debug, Clone, Serialize)] +pub struct NexrenderJob { + pub template: Template, + pub actions: Actions, + pub assets: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Actions { + pub postrender: Vec, +} + +impl Default for Actions { + fn default() -> Self { + Self { + postrender: vec![PostrenderAction::Encode { + preset: "mp4".to_string(), + output: "encoded.mp4".to_string(), + }], + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct JobData { + pub date: String, + pub time: String, + pub time_h: String, + pub time_m: String, + pub channel: String, + pub sport: String, + pub league: String, + pub team_a: String, + pub team_b: String, + pub data_display: String, + pub pack_path: String, + pub team_a_logo: String, + pub team_b_logo: String, + pub team_a_logo_res: Option, + pub team_b_logo_res: Option, + pub channel_logo: String, + pub outfile_name: String, + pub row_index: usize, +} + +impl JobData { + pub fn from_row( + row: &HashMap, + row_index: usize, + packs: &HashMap, + logos: &LogoRegistry, + channels: &HashMap, + ) -> Option { + let state = row.get("STATE").map(|s| s.as_str()).unwrap_or(""); + if state != "FALSE" { + return None; + } + + let sport = row.get("SPORT")?.clone(); + let league = row.get("LEAGUE")?.clone(); + let team_a = row.get("TEAM A").cloned().unwrap_or_default(); + let team_b = row.get("TEAM B").cloned().unwrap_or_default(); + let channel = row.get("CHANEL").cloned().unwrap_or_default(); + let time = row.get("TIME")?.clone(); + let data_str = row.get("DATA")?.clone(); + + let (date_obj, data_display) = parse_date(&data_str); + let (time_h, time_m) = parse_time(&time); + + let pack_path = packs.get(&sport).cloned().unwrap_or_default(); + let channel_logo = channels.get(&channel).cloned().unwrap_or_default(); + + let (team_a_name, team_a_res) = parse_team_name(&team_a); + let (team_b_name, team_b_res) = parse_team_name(&team_b); + + let team_a_logo = logos.find(&team_a, &sport).unwrap_or_default(); + let team_b_logo = logos.find(&team_b, &sport).unwrap_or_default(); + + let mut fn_parts = Vec::new(); + fn_parts.push(format!( + "{:04}{:02}{:02}", + date_obj.year(), + date_obj.month(), + date_obj.day() + )); + if !sport.is_empty() && sport != "Без оформления" { + fn_parts.push(sport.clone()); + } + fn_parts.push(league.trim_end_matches('.').to_string()); + if !team_a_name.is_empty() { + fn_parts.push(team_a_name.clone()); + } + if !team_b_name.is_empty() { + fn_parts.push(team_b_name.clone()); + } + if !channel.is_empty() { + fn_parts.push(channel.clone()); + } + + let outfile_name = transliterate(&fn_parts.join("_"), true) + .replace(' ', "-") + .replace('\'', ""); + + Some(Self { + date: data_str, + time: time.clone(), + time_h, + time_m, + channel, + sport, + league, + team_a: team_a_name, + team_b: team_b_name, + data_display, + pack_path: unc_to_uri(&pack_path), + team_a_logo: unc_to_uri(&team_a_logo), + team_b_logo: unc_to_uri(&team_b_logo), + team_a_logo_res: team_a_res, + team_b_logo_res: team_b_res, + channel_logo: unc_to_uri(&channel_logo), + outfile_name, + row_index, + }) + } + + pub fn create_variants(&self) -> Vec { + let mut variants = vec![self.clone()]; + let mut today = self.clone(); + today.data_display = "сегодня".to_string(); + today.outfile_name = format!("{}_Today", self.outfile_name); + variants.push(today); + let mut tomorrow = self.clone(); + tomorrow.data_display = "завтра".to_string(); + tomorrow.outfile_name = format!("{}_Tomorrow", self.outfile_name); + variants.push(tomorrow); + variants + } + + pub fn to_nexrender_job(&self, output_folder: &str) -> NexrenderJob { + let template = if self.team_b.is_empty() { + Template::single() + } else { + Template::default() + }; + + let output_path = format!("{}/{}.mp4", output_folder, self.outfile_name); + let mut actions = Actions::default(); + actions.postrender.push(PostrenderAction::Copy { + input: "encoded.mp4".to_string(), + output: output_path, + }); + + let mut assets = Vec::new(); + self.add_text_assets(&mut assets); + self.add_image_assets(&mut assets); + if !self.pack_path.is_empty() { + assets.push(Asset::Video { + src: self.pack_path.clone(), + asset_type: "video".to_string(), + layer_name: "TOP".to_string(), + }); + } + + NexrenderJob { + template, + actions, + assets, + } + } + + fn add_text_assets(&self, assets: &mut Vec) { + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "DATA".to_string(), + property: "Source Text".to_string(), + value: json!(self.data_display), + }); + self.add_date_adjustments(assets); + + if !self.team_b.is_empty() { + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "TIME_H".to_string(), + property: "Source Text".to_string(), + value: json!(self.time_h), + }); + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "TIME_M".to_string(), + property: "Source Text".to_string(), + value: json!(self.time_m), + }); + self.add_time_adjustments(assets); + } else { + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "TIME".to_string(), + property: "Source Text".to_string(), + value: json!(self.time), + }); + } + + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "LEAGUE".to_string(), + property: "Source Text".to_string(), + value: json!(self.league), + }); + self.add_league_adjustments(assets); + + if !self.sport.is_empty() { + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "SPORT".to_string(), + property: "Source Text".to_string(), + value: json!(self.sport), + }); + } + + let teams_text = if !self.team_a.is_empty() && !self.team_b.is_empty() { + format!("{} - {}", self.team_a, self.team_b) + } else if !self.team_a.is_empty() { + self.team_a.clone() + } else { + self.team_b.clone() + }; + if !teams_text.is_empty() { + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "TEAMS".to_string(), + property: "Source Text".to_string(), + value: json!(teams_text), + }); + self.add_teams_adjustments(assets); + } + } + + fn add_date_adjustments(&self, assets: &mut Vec) { + let (font_size, anchor_point): (&str, Vec) = match self.data_display.as_str() { + "сегодня" if !self.team_b.is_empty() => ("105", vec![0, 5]), + "завтра" if !self.team_b.is_empty() => ("115", vec![0, 25]), + _ if self.data_display.len() < 6 && !self.team_b.is_empty() => ("120", vec![0, 20]), + _ => return, + }; + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "DATA".to_string(), + property: "Source Text.fontSize".to_string(), + value: json!(font_size), + }); + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "DATA".to_string(), + property: "transform.anchorPoint".to_string(), + value: json!(anchor_point), + }); + } + + fn add_time_adjustments(&self, assets: &mut Vec) { + let anchor_point = if self.time_h.len() < 2 { + vec![60, 0] + } else if self.time_h.len() == 2 && self.time_h.parse::().unwrap_or(20) < 20 { + vec![20, 0] + } else { + return; + }; + for layer in ["TIME_H", "TIME_M", "TIME"] { + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: layer.to_string(), + property: "transform.anchorPoint".to_string(), + value: json!(anchor_point), + }); + } + } + + fn add_league_adjustments(&self, assets: &mut Vec) { + if self.league.len() > 16 { + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "LEAGUE".to_string(), + property: "Source Text.fontSize".to_string(), + value: json!("73"), + }); + } + } + + fn add_teams_adjustments(&self, assets: &mut Vec) { + if (self.team_a.len() + self.team_b.len()) >= 32 { + assets.push(Asset::Data { + asset_type: "data".to_string(), + layer_name: "TEAMS".to_string(), + property: "Source Text.fontSize".to_string(), + value: json!("55"), + }); + } + } + + fn add_image_assets(&self, assets: &mut Vec) { + if !self.team_a_logo.is_empty() { + assets.push(Asset::Image { + src: self.team_a_logo.clone(), + asset_type: "image".to_string(), + layer_name: "TEAM_A_LOGO".to_string(), + }); + if let Some(res) = &self.team_a_logo_res { + assets.push(Asset::DataExpression { + asset_type: "data".to_string(), + layer_name: "TEAM_A_LOGO".to_string(), + property: "scale".to_string(), + expression: logo_scale_expression(res), + }); + } + } + if !self.team_b_logo.is_empty() { + assets.push(Asset::Image { + src: self.team_b_logo.clone(), + asset_type: "image".to_string(), + layer_name: "TEAM_B_LOGO".to_string(), + }); + if let Some(res) = &self.team_b_logo_res { + assets.push(Asset::DataExpression { + asset_type: "data".to_string(), + layer_name: "TEAM_B_LOGO".to_string(), + property: "scale".to_string(), + expression: logo_scale_expression(res), + }); + } + } + if !self.channel_logo.is_empty() { + assets.push(Asset::Image { + src: self.channel_logo.clone(), + asset_type: "image".to_string(), + layer_name: "CHANELL".to_string(), + }); + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct LogoRegistry { + logos: HashMap<(String, String), String>, +} + +impl LogoRegistry { + pub fn new() -> Self { + Self::default() + } + + pub fn insert(&mut self, team: String, sport: String, link: String) { + self.logos.insert((team, sport), link); + } + + pub fn find(&self, team: &str, sport: &str) -> Option { + if let Some(link) = self.logos.get(&(team.to_string(), sport.to_string())) { + return Some(link.clone()); + } + for ((t, s), link) in &self.logos { + if s == sport && team.starts_with(t) { + return Some(link.clone()); + } + } + None + } +} + +fn parse_date(s: &str) -> (NaiveDate, String) { + let parts: Vec<&str> = s.split('.').collect(); + if parts.len() == 3 { + if let (Ok(day), Ok(month), Ok(year)) = ( + parts[0].parse::(), + parts[1].parse::(), + parts[2].parse::(), + ) { + if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) { + let months = [ + "", + "января", + "февраля", + "марта", + "апреля", + "мая", + "июня", + "июля", + "августа", + "сентября", + "октября", + "ноября", + "декабря", + ]; + return (date, format!("{} {}", day, months[month as usize])); + } + } + } + (NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), s.to_string()) +} + +fn parse_time(s: &str) -> (String, String) { + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() >= 2 { + (parts[0].to_string(), parts[1].to_string()) + } else { + (s.to_string(), "00".to_string()) + } +} + +fn parse_team_name(s: &str) -> (String, Option) { + let parts: Vec<&str> = s.split('#').collect(); + if parts.len() >= 3 { + (parts[0].to_string(), Some(parts[2].to_string())) + } else { + (parts[0].to_string(), None) + } +} + +fn unc_to_uri(path: &str) -> String { + if path.is_empty() { + return String::new(); + } + if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("file://") { + return path.to_string(); + } + format!("file://{}", path.replace('\\', "/").trim_start_matches('/')) +} + +fn logo_scale_expression(target_size: &str) -> String { + format!( + "if (width > height) {{max_size = width;}} else {{max_size = height;}} var real_size = {}/max_size*100;[real_size,real_size]", + target_size + ) +} diff --git a/src/lib.rs b/src/synology.rs similarity index 99% rename from src/lib.rs rename to src/synology.rs index eefee7f..f97c0e8 100644 --- a/src/lib.rs +++ b/src/synology.rs @@ -1,9 +1,9 @@ use reqwest::{ - Client, ClientBuilder, multipart::{Form, Part}, + Client, ClientBuilder, }; -use serde::Deserialize; use serde::de::DeserializeOwned; +use serde::Deserialize; use serde_json::Value; use std::collections::HashMap; use std::time::Duration; @@ -37,7 +37,7 @@ pub enum SynologyError { Io(#[from] std::io::Error), #[error("CSV error: {0}")] - Csv(#[from] csv::Error), + Csv(String), } /// Результат операций