diff --git a/.gitignore b/.gitignore index d47e0bd..ebb10cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +webdav-server/ server/certs/ client-iced/ .env @@ -50,3 +51,4 @@ Thumbs.db /server/target /target /server/library +queue.db diff --git a/Cargo.lock b/Cargo.lock index 60d777d..11e3857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,16 +1,105 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "atty" version = "0.2.14" @@ -34,12 +123,24 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + [[package]] name = "block-buffer" version = "0.10.2" @@ -49,6 +150,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + [[package]] name = "byteorder" version = "1.4.3" @@ -61,12 +168,87 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +[[package]] +name = "cc" +version = "1.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.3" @@ -96,6 +278,26 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.9.0" @@ -109,6 +311,24 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fnv" version = "1.0.7" @@ -117,62 +337,97 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "matches", "percent-encoding", ] [[package]] -name = "futures-channel" +name = "futures" version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1" +checksum = "ab30e97ab6aacfe635fad58f22c2bb06c8b685f7421eb1e064a729e2a5f481fa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" +checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.23" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0db9cce532b0eae2ccf2766ab246f114b56b9cf6d445e00c2549fbc100ca045d" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] name = "futures-sink" -version = "0.3.23" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.23" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.23" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -201,9 +456,83 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -222,14 +551,31 @@ checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 0.4.7", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", ] [[package]] name = "httparse" -version = "1.4.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -238,90 +584,323 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] -name = "idna" -version = "0.2.3" +name = "hyper" +version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 0.4.7", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", ] [[package]] -name = "itoa" -version = "0.4.7" +name = "iana-time-zone" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] [[package]] -name = "libc" -version = "0.2.132" +name = "iana-time-zone-haiku" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] [[package]] -name = "lock_api" -version = "0.4.7" +name = "icu_collections" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ - "autocfg", - "scopeguard", + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", ] [[package]] -name = "log" -version = "0.4.17" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ - "cfg-if", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "matches" -version = "0.1.8" +name = "icu_normalizer" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] [[package]] -name = "memchr" -version = "2.4.0" +name = "icu_normalizer_data" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] -name = "mio" -version = "0.8.4" +name = "icu_properties" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", ] [[package]] -name = "num_cpus" -version = "1.13.0" +name = "icu_properties_data" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -dependencies = [ - "hermit-abi", - "libc", -] +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] -name = "once_cell" -version = "1.7.2" +name = "icu_provider" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] [[package]] -name = "parking_lot" -version = "0.12.1" +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.3", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.36.1", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "parking_lot" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ @@ -339,20 +918,40 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] [[package]] name = "pin-project-lite" -version = "0.2.6" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -360,6 +959,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -368,18 +982,18 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -430,14 +1044,26 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" dependencies = [ - "bitflags", + "bitflags 1.2.1", ] [[package]] name = "regex" -version = "1.5.6" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -446,9 +1072,38 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rusqlite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustversion" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -456,39 +1111,58 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.126" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa 1.0.15", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ - "itoa", + "form_urlencoded", + "itoa 1.0.15", "ryu", "serde", ] @@ -505,25 +1179,56 @@ dependencies = [ ] [[package]] -name = "signal-hook-registry" +name = "sha1" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared" +version = "0.1.0" +dependencies = [ + "chrono", + "rusqlite", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.3" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.6.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" @@ -535,6 +1240,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.72" @@ -546,6 +1269,28 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -557,44 +1302,39 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.25" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.25" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] -name = "tinyvec" -version = "1.2.0" +name = "tinystr" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - [[package]] name = "tokio" -version = "1.20.1" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" dependencies = [ "autocfg", "bytes", @@ -602,13 +1342,12 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.45.0", ] [[package]] @@ -619,7 +1358,18 @@ checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.72", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", ] [[package]] @@ -631,19 +1381,77 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.17.3", ] [[package]] -name = "tungstenite" -version = "0.17.3" +name = "tokio-tungstenite" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" dependencies = [ - "base64", - "byteorder", - "bytes", - "http", + "futures-util", + "log", + "tokio", + "tungstenite 0.18.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +dependencies = [ + "base64 0.13.0", + "byteorder", + "bytes", + "http", "httparse", "log", "rand", @@ -653,6 +1461,25 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.0", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.15.0" @@ -660,22 +1487,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] -name = "unicode-bidi" -version = "0.3.5" +name = "unicase" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" -dependencies = [ - "matches", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] -name = "unicode-normalization" -version = "0.1.18" +name = "unicode-ident" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33717dca7ac877f497014e10d73f3acf948c342bee31b5ca7892faf94ccc6b49" -dependencies = [ - "tinyvec", -] +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-xid" @@ -685,13 +1506,12 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "url" -version = "2.2.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] @@ -701,11 +1521,69 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "ba431ef570df1287f7f8b07e376491ad54f84d26ac473489427231e1718e1f69" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "rustls-pemfile", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite 0.18.0", + "tokio-util", + "tower-service", + "tracing", +] [[package]] name = "wasi" @@ -719,6 +1597,64 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -750,62 +1686,402 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] +[[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.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.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_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", +] + +[[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.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[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.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[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.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[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.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "youoke-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "rand", +] + +[[package]] +name = "youoke-player" +version = "0.1.0" +dependencies = [ + "futures", + "serde", + "serde_json", + "tokio", + "warp", +] + [[package]] name = "youoke-server" version = "0.1.0" dependencies = [ "env_logger", + "futures", "futures-channel", "futures-util", "glob", "log", + "rusqlite", "serde", "serde_json", + "shared", "tokio", - "tokio-tungstenite", - "tungstenite", + "tokio-tungstenite 0.17.2", + "tungstenite 0.17.3", "url", + "warp", +] + +[[package]] +name = "youoke-worker" +version = "0.1.0" +dependencies = [ + "futures", + "futures-util", + "rusqlite", + "serde", + "serde_json", + "shared", + "tokio", + "tokio-tungstenite 0.17.2", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] diff --git a/Cargo.toml b/Cargo.toml index bf282d4..0b2b76f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,4 @@ [workspace] # note: this is here at the top-level to appease vscode convention. -members = [ - "./server/src/..", -] +members = ["server", "player", "worker", "shared", "cli"] diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..afc800a --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "youoke-cli" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4", features = ["derive"] } +rand = "0.8" +anyhow = "1.0" diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..a930857 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,127 @@ +// main.rs +use clap::{Parser, Subcommand}; +use rand::Rng; +use std::collections::HashMap; +use std::env; +use std::io::{self, Write}; +use std::process::Command; +use std::process::Stdio; +use std::sync::Arc; + +#[derive(Parser, Debug)] +#[command( + name = "Youoke CLI Launcher", + about = "πŸš€ Youoke multi-process dev launcher", + version +)] +struct Cli { + #[command(subcommand)] + command: Option, + + /// Show debug logs from all child processes + #[arg(long, default_value_t = false)] + verbose: bool, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Launch server(s) with configured environment + Launch, + + /// Show the environment variables to be used + Env, +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + show_banner(); + + let mut envs = default_envs(); + prompt_env_vars(&mut envs)?; + + match &cli.command { + Some(Commands::Env) => { + println!("\nπŸ”§ Using environment variables:"); + for (k, v) in &envs { + println!(" {} = {}", k, v); + } + } + Some(Commands::Launch) | None => { + println!("\nπŸš€ Launching youoke processes..."); + run_commands(&envs, cli.verbose)?; + } + } + + Ok(()) +} + +fn show_banner() { + println!( + r#" +__ _____ _ _ ___ _ _______ +\ \ / / _ \| | | |/ _ \| |/ / ____| + \ V / | | | | | | | | | ' /| _| + | || |_| | |_| | |_| | . \| |___ + |_| \___/ \___/ \___/|_|\_\_____| +"# + ); +} + +fn default_envs() -> HashMap { + let mut map = HashMap::new(); + map.insert("WS_ADDRESS".into(), "127.0.0.1:9001".into()); + map.insert("HTTP_ADDRESS".into(), "127.0.0.1:9002".into()); + map.insert("LIB_DIR".into(), "./library".into()); + map.insert("PLAYER_DIR".into(), "player/public/".into()); + map.insert("HANDSHAKE_CODE".into(), generate_code()); + map +} + +fn prompt_env_vars(envs: &mut HashMap) -> anyhow::Result<()> { + println!("\nπŸ”§ Configure environment (press Enter to use default):"); + for (key, default) in envs.clone() { + print!("{} [{}]: ", key, default); + io::stdout().flush()?; + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + let input = input.trim(); + if !input.is_empty() { + if key == "HANDSHAKE_CODE" + && (input.len() != 6 || !input.chars().all(|c| c.is_digit(10))) + { + println!("❌ HANDSHAKE_CODE must be exactly 6 digits. Using auto-generated."); + } else { + envs.insert(key, input.to_string()); + } + } + } + Ok(()) +} + +fn run_commands(envs: &HashMap, verbose: bool) -> anyhow::Result<()> { + let mut commands = vec![ + ("youoke-server", vec!["--bin", "youoke-server"]), + ("youoke-worker", vec!["--bin", "youoke-worker"]), + ]; + + for (name, args) in commands.drain(..) { + let mut cmd = Command::new("cargo"); + cmd.args(args); + for (key, val) in envs { + cmd.env(key, val); + } + if verbose { + cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit()); + } else { + cmd.stdout(Stdio::null()).stderr(Stdio::null()); + } + println!("▢️ Starting: {}...", name); + let _child = cmd.spawn()?; + } + Ok(()) +} + +fn generate_code() -> String { + let mut rng = rand::thread_rng(); + format!("{:06}", rng.gen_range(0..1_000_000)) +} diff --git a/client/README.md b/client/README.md index cd13da5..7ce9330 100644 --- a/client/README.md +++ b/client/README.md @@ -4,8 +4,14 @@ just a vcr-menu inspired react app, here. :shrug: -note: currently this app is configured to look for websocket servers on localhost:9001 (and maybe a few other selected IP addresses); maybe at some point there will be some kind of discovery system? see: `KNOWN_ROOMS` in Landing.tsx. +note on `https://`: so yeah, a browser can only connect to websocket servers on your local area network if this page is served on `http://` (no `s`). there's an annoying message because browsers (and github pages) prefer https 🀷 + +note on `KNOWN_ROOMS`: currently this app is configured to look for websocket servers on localhost:9001 (and maybe a few other selected addresses); maybe at some point there will be some kind of discovery system? see: `KNOWN_ROOMS` in Landing.tsx. + +## devel localhost dev: `npm start` -prod build: `npm run build` +0.0.0.0 dev: `npm run pubstart` + +build: `npm run build` diff --git a/client/index.html b/client/index.html index 9548fcd..4ae402e 100644 --- a/client/index.html +++ b/client/index.html @@ -1,7 +1,7 @@ - + - diff --git a/client/package.json b/client/package.json index f4c90b0..815de7e 100644 --- a/client/package.json +++ b/client/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "start": "vite --port 3000", + "startpub": "vite --port 3000 --host 0.0.0.0", "dev": "npm start", "build": "tsc -b && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", diff --git a/client/src/App.tsx b/client/src/App.tsx index e9a4dbb..84f2a15 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,16 +6,6 @@ import Room, { IRoom } from './pages/Room' function App() { const [room, setAppRoom] = useState(() => { - // first, try to get a room from the url query params: - const search = window.location.search - const params = new URLSearchParams(search) - const name = params.get('name') - const href = params.get('href') - if (name && href) { - console.log('zomg have room from query params!', { name, href }) - return { name, href } - } - // otherwise fallback to localstorage const lRoom = localStorage.getItem('room') console.log('lRoom:', lRoom) @@ -35,7 +25,7 @@ function App() { return (
{!room ? ( - + ) : ( )} diff --git a/client/src/index.css b/client/src/index.css index ab989ac..b58f96a 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -16,6 +16,7 @@ input { font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + font-size: 16px; } .center { @@ -95,7 +96,9 @@ input { font-size: 1em; } .sticky { - top: 0px; + position: sticky; + + top: 50px; } } diff --git a/client/src/main.tsx b/client/src/main.tsx index 3d7150d..c4fc9bb 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -6,5 +6,5 @@ import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( - , + ) diff --git a/client/src/pages/HttpNotice.css b/client/src/pages/HttpNotice.css new file mode 100644 index 0000000..a089e00 --- /dev/null +++ b/client/src/pages/HttpNotice.css @@ -0,0 +1,23 @@ +.notice { + border: 2em double white; +} +.notice h2 { + font-size: 3em; + font-weight: bold; +} +.notice h3 { + font-size: 2em; +} + +@media (max-width: 500px) { + .notice { + border: 0.5em double white; + } + .notice h2 { + font-size: 1.5em; + font-weight: bold; + } + .notice h3 { + font-size: 1em; + } +} diff --git a/client/src/pages/HttpsNotice.tsx b/client/src/pages/HttpsNotice.tsx new file mode 100644 index 0000000..591861c --- /dev/null +++ b/client/src/pages/HttpsNotice.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react' + +import './HttpNotice.css' + +export function HttpsNotice() { + const [isHttps, setIsHttps] = useState(false) + useEffect(() => { + setIsHttps(window.location.protocol === 'https:') + }, []) + + return ( + isHttps && ( +
+

+ important! you're using https://* +

+

+ please switch to http:// +

+

+ manually type the "http://" part of "http://youoke.party" +

+

+ * your browser will not let you connect to non-https servers{' '} + + ...which is anoying (sorry (but to browser's credit, security-wise, + is a reasonable default)) + +

+

+ if you're using a room with https support (or localhost), you + can ignore this!{' '} +

+
+ ) + ) +} diff --git a/client/src/pages/Landing.css b/client/src/pages/Landing.css index 10faf52..1dba91a 100644 --- a/client/src/pages/Landing.css +++ b/client/src/pages/Landing.css @@ -24,6 +24,11 @@ cursor: pointer; } +.code { + display: flex; + gap: 1em; +} + @media (max-width: 500px) { .youoke { font-size: 3em; diff --git a/client/src/pages/Landing.tsx b/client/src/pages/Landing.tsx index 99f6862..c6bd784 100644 --- a/client/src/pages/Landing.tsx +++ b/client/src/pages/Landing.tsx @@ -1,42 +1,112 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import useInterval from '../hooks' import './Landing.css' import { IRoom } from './Room' +import { HttpsNotice } from './HttpsNotice' export interface LandingProps { + room?: IRoom setRoom: (room: IRoom) => void } type RoomList = IRoom[] +const search = window.location.search +const params = new URLSearchParams(search) +const code = params.get('code') || '' + +const tryToTidyNameOrHref = (name?: string | null, href?: string | null) => { + if (!name && !href) return + + if (!name && href) { + name = + href.replace('ws://', '').replace('wss://', '').replace(/:\d+/, '') || '' + } + + if (!href && name) href = name + + // still no href?! + if (!href || !name) return + + if (!href.startsWith('ws://') || !href.startsWith('wss://')) { + href = `ws://${href}` + } + if (!href.match(/:\d+/)) { + href = `${href}:9001` + } + + console.log('zomg tryToTidyNameOrHref', { name, href, code }) + + return { name, href, code } +} + +const getWindowLocationRoom = () => { + const name = window.location.hostname.includes('youoke.party') + ? 'LOCALHOST' // all capz, cuz better + : window.location.hostname + const href = `ws://${name.toLowerCase()}:9001` + return { name, href, code } +} + +const getQueryParamsRoom = () => + tryToTidyNameOrHref(params.get('name'), params.get('href')) + +const getSpecialRooms = () => { + const queryParamsRoom = getQueryParamsRoom() + if (!queryParamsRoom) { + return [getWindowLocationRoom()] + } else { + return [getWindowLocationRoom(), queryParamsRoom] + } +} + const KNOWN_ROOMS: RoomList = [ - { name: 'LOCALHOST', href: 'ws://localhost:9001' }, - // { name: 'FOLK', href: 'ws://10.246.17.194:9001' }, - { name: 'PARTYLINE', href: 'wss://f8da-68-161-154-113.ngrok-free.app' }, + ...getSpecialRooms(), + // { name: 'PIZZAPARTY', href: 'wss://youoke.ngrok.pizza', code }, ] -function testWS(href: string): Promise { - const ws = new WebSocket(href) +function testRoom(href: string): Promise { + // simple ping to see if server is alive + return fetch( + `${href.replace('ws://', 'http://').replace('wss://', 'https://').replace('9001', '9002')}/hello` + ) + .then((response) => response.status === 200 || response.status === 401) + .catch(() => false) +} - return new Promise((resolve, reject) => { - ws.onerror = () => reject(false) - ws.onopen = () => { - ws.close() - resolve(true) - } - }) +function testCode(href: string, code: string): Promise { + // simple ping to see if server is alive + const fixed_href = href + .replace('ws://', 'http://') + .replace('wss://', 'https://') + .replace('9001', '9002') + return fetch(`${fixed_href}/hello?code=${code}`) + .then((response) => response.status === 200) + .catch(() => false) } export default function Landing(props: LandingProps) { - const { setRoom } = props + const { room, setRoom } = props + const [code, setCode] = useState('') + const [needsCode, setNeedsCode] = useState>({}) const [addNewRoom, setAddNewRoom] = useState(false) - const [newRoom, setNewRoom] = useState(KNOWN_ROOMS[0]) + const [newRoom, setNewRoom] = useState({ + name: '', + href: '', + code: '', + }) const [roomsToFind, setRoomsToFind] = useState(KNOWN_ROOMS) const [roomList, setRoomList] = useState() const [delay, setDelay] = useState(1000) + useEffect(() => { + if (!room) return + console.log('zomg add props room!', room) + setRoomsToFind((prev) => [...prev, room]) + }, [room]) + useInterval( () => { if (roomsToFind.length === 0) { @@ -44,8 +114,9 @@ export default function Landing(props: LandingProps) { return } roomsToFind.forEach((room) => { - testWS(room.href) - .then(() => { + testRoom(room.href) + .then((success) => { + if (!success) return console.log('zomg FOUND room!', room) setRoomList((prev) => [...(prev ? prev : []), room]) const roomsToFindClone = [...roomsToFind] @@ -56,7 +127,8 @@ export default function Landing(props: LandingProps) { } }) .catch(() => { - console.warn('onoz, bad room!', room) + // console.warn('onoz, bad room!', room, ' error:', e) + // πŸ€·β€β™€οΈ }) }) @@ -70,8 +142,10 @@ export default function Landing(props: LandingProps) { return (

YOUOKE

+

- - - JOIN ROOM - - -

+
  1. - */}
    { setRoomsToFind((prev) => { - if (prev.find((r) => r.name === newRoom.name && r.href === newRoom.href)) { + const fixedNewRoom = tryToTidyNameOrHref( + newRoom.name, + newRoom.href + ) + if (!fixedNewRoom || !fixedNewRoom.href) return prev + if (prev.find((r) => r.href === fixedNewRoom.href)) { return prev } - - return [...prev, newRoom] + return [...prev, fixedNewRoom] }) // reset inputz? // setNewRoom(KNOWN_ROOMS[0]) setAddNewRoom(false) + setDelay(1000) }} > add new room @@ -151,11 +230,52 @@ export default function Landing(props: LandingProps) { key={`${room}${idx}`} tabIndex={idx} onClick={() => { - setRoom(room) - setDelay(null) + setNeedsCode((prev) => ({ + ...prev, + [`${room}${idx}`]: true, + })) }} > - {room.name} + {needsCode[`${room}${idx}`] ? ( + + ) : ( + room.name + )}
  2. ))}
diff --git a/client/src/pages/Room.css b/client/src/pages/Room.css index d15bda4..3a9b612 100644 --- a/client/src/pages/Room.css +++ b/client/src/pages/Room.css @@ -20,14 +20,27 @@ input { .flex-responsive { display: flex; } +.search-q { + width: 100%; +} .flex-grow { flex-grow: 1; } .search-q .list-btn { width: 30px; } -.search-q-input { - width: calc(100% - 60px); +.search-q .invert-list-btn { + height: 60px; + margin-top: 6px; +} +.search-q .flex-grow { + flex-grow: 1; +} + +.search-q-input-tip { + font-size: large; + line-height: 1em; + text-decoration: underline; } .search-results li { @@ -44,7 +57,32 @@ input { padding-top: 50px; } +.controls { + display: flex; + justify-content: space-between; +} + +.controls div { + margin: 0; + padding: 0 1em; + flex-grow: 1; + text-align: center; +} +.controls div:hover { + text-decoration: underline; +} +@media (max-width: 799px) { + .search-q .invert-list-btn { + margin-top: 0px; + } +} + @media (max-width: 500px) { + .search-q-input-tip { + font-size: xx-small; + line-height: 1em; + } + #search-results-container { margin-left: -50px; } @@ -69,4 +107,7 @@ input { .flex-responsive { flex-direction: column; } + .controls div { + padding: 0; + } } diff --git a/client/src/pages/Room.tsx b/client/src/pages/Room.tsx index 9a4938b..70ce2b1 100644 --- a/client/src/pages/Room.tsx +++ b/client/src/pages/Room.tsx @@ -3,10 +3,12 @@ import debounce from 'lodash.debounce' import './Room.css' import youtubeSearch, { YTSearchItem } from '../youtube' +import { HttpsNotice } from './HttpsNotice' export interface IRoom { name: string href: string + code: string } export interface RoomProps { @@ -29,6 +31,9 @@ export interface DeQueue { export interface QueueSetSinger { QueueSetSinger: { id: string; singer: string } } +export interface PlayerSetWallmessage { + PlayerSetWallmessage: { wallmessage: string } +} export type PlayerRequest = 'PlayerPause' | 'PlayerPlay' | 'PlayerSkip' export type LibraryRequest = 'GetLibrary' type Request = @@ -37,6 +42,7 @@ type Request = | QueueSetSinger | DeQueue | PlayerRequest + | PlayerSetWallmessage | LibraryRequest interface QueueItem { @@ -46,6 +52,7 @@ interface QueueItem { singer: string status: string title: string + wallmessage: string } interface LibraryItem { @@ -70,7 +77,7 @@ function QSinger(props: { onClick={() => !renameSinger && setRenameSinger(true)} > {renameSinger ? ( - <> +
- + +
setRenameSinger(false)} + > + {' '} + x{' '} +
+
) : ( - singer +
{singer}
)}
) @@ -103,6 +118,8 @@ export default function Room(props: RoomProps) { () => localStorage.getItem('singer') || 'nobody' ) const [editSinger, setEditSinger] = useState(false) + const [wallmessage, setWallmessage] = useState('') + const [editWallMessage, setEditWallmessage] = useState(false) const [searchQ, setSearchQ] = useState('') const [showSearchResults, setShowSearchResults] = useState(false) const [searchResults, setSearchResults] = useState([]) @@ -110,6 +127,7 @@ export default function Room(props: RoomProps) { const [showIdInput, setShowIdInput] = useState(false) const [showSearchInput, setShowSeachInout] = useState(false) const [ytSearchResulta, setYtSearchResults] = useState([]) + const [ytNextPageToken, setYtNextPageToken] = useState('') function handleWsMessage(message: WebSocketEventMap['message']) { try { @@ -170,8 +188,17 @@ export default function Room(props: RoomProps) { }) } + function qWallMessage(wallmessage: string) { + sendWsMessage({ + PlayerSetWallmessage: { + wallmessage, + }, + }) + } + useEffect(() => { - ws.current = new WebSocket(room.href) + // console.log('zomg using ws url:', `${room.href}?code=${room.code}`) + ws.current = new WebSocket(`${room.href}?code=${room.code}`) ws.current.onopen = () => { setWsStatus('open') sendWsMessage('GetLibrary') @@ -182,11 +209,13 @@ export default function Room(props: RoomProps) { return () => { ws.current && ws.current.close() } - }, [room.href]) + }, [room.href, room.code]) useEffect(() => { const fResults = library.filter((item) => - item.title.toLowerCase().includes(searchQ.toLowerCase()) + item.title + .toLowerCase() + .includes(searchQ.toLowerCase().replace('karaoke', '')) ) setSearchResults(fResults) document.getElementById('search-results-container')?.scrollIntoView() @@ -194,17 +223,31 @@ export default function Room(props: RoomProps) { }, [library, searchQ, queue]) function ytSearch(q: string) { - youtubeSearch(q).then((results) => setYtSearchResults(results)) + youtubeSearch(q).then((results) => { + setYtNextPageToken(results?.nextPageToken || '') + setYtSearchResults(results?.items || []) + }) } const debounceYtSearch = useCallback(debounce(ytSearch, 2500), []) + function ytSearchNextPage() { + youtubeSearch(searchQ, 10, ytNextPageToken).then((results) => { + setYtNextPageToken(results?.nextPageToken || '') + setYtSearchResults((prev) => [...prev, ...(results?.items || [])]) + }) + } + return (

{room.name}

{wsStatus === 'closed' ? (
* * * disconnected * * * +
    +
  1. location.reload()}> + reload +
  2. setRoom(undefined)}> exit room
  3. @@ -274,21 +317,33 @@ export default function Room(props: RoomProps) {
    {showSearchInput ? (
    - { - if (e.key === 'Escape') { - setShowSearchResults(false) - setShowSeachInout(false) - } - }} - onFocus={() => setShowSearchResults(true)} - value={searchQ} - onChange={(e) => setSearchQ(e.target.value)} - autoFocus - /> +
    + { + if (e.key === 'Escape') { + setShowSearchResults(false) + setShowSeachInout(false) + } + }} + onFocus={() => setShowSearchResults(true)} + value={searchQ} + onChange={(e) => setSearchQ(e.target.value)} + autoFocus + /> + {!searchQ.match('karaoke') && ( +
    + setSearchQ((prev) => `${prev} karaoke`) + } + > + tip: add "karaoke" to the search query! +
    + )} +
    {showSearchResults && (
    !isQueued && q(r.id)} + onClick={() => { + if (isQueued) return + q(r.id) + setShowSearchResults(false) + setShowSeachInout(false) + }} key={`result${r.id}`} > {isQueued && 🎀}{' '} @@ -349,9 +409,12 @@ export default function Room(props: RoomProps) { return (
  4. - !isQueued && q(item.id.videoId) - } + onClick={() => { + if (isQueued) return + q(item.id.videoId) + setShowSearchResults(false) + setShowSeachInout(false) + }} key={`ytresult${item.id.videoId}`} >
    @@ -370,6 +433,13 @@ export default function Room(props: RoomProps) {
  5. ) })} + +
  6. ytSearchNextPage()} + className="list-btn" + > + load more youtube results +
)} @@ -415,17 +485,75 @@ export default function Room(props: RoomProps) {
singer: {singer}
)} -
  • sendWsMessage('PlayerPause')}> -
    pause
    -
  • -
  • sendWsMessage('PlayerPlay')}> -
    play
    + +
  • { + setWallmessage('') + setEditWallmessage(true) + }} + > + {editWallMessage ? ( +
    + { + if (e.key === 'Enter') { + setEditWallmessage(false) + qWallMessage(wallmessage) + } else if (e.key === 'Escape') { + setEditWallmessage(false) + } + }} + autoFocus + onBlur={() => setEditWallmessage(false)} + value={wallmessage} + onChange={(e) => setWallmessage(e.target.value)} + placeholder="show a message between songz" + maxLength={75} + /> +
    { + setEditWallmessage(false) + }} + > + {' '} + x{' '} +
    +
    + ) : ( +
    bumper message
    + )}
  • -
  • sendWsMessage('PlayerSkip')}> -
    skip
    + +
  • +
    +
    sendWsMessage('PlayerPause')} + > + pause +
    +
    sendWsMessage('PlayerPlay')} + > + play +
    +
    sendWsMessage('PlayerSkip')} + > + skip +
    +
  • -
  • setRoom(undefined)}> -
    exit
    +
  • setRoom(undefined)}> + exit room
  • diff --git a/client/src/youtube.ts b/client/src/youtube.ts index d2f53db..3ae274d 100644 --- a/client/src/youtube.ts +++ b/client/src/youtube.ts @@ -43,17 +43,27 @@ export interface DefaultOrMediumOrHigh { height: number } -export default function youtubeSearch(q: string): Promise { +export default function youtubeSearch( + q: string, + maxResults: number = 3, + pageToken: string | null = null +): Promise { if (!q || q.length === 0) { console.log('no search q, gonna return []') - return Promise.resolve([]) + return Promise.resolve(null) } const params = new URLSearchParams({ q, part: 'snippet', - maxResults: '25', + maxResults: maxResults.toString(), key: YT_API_KEY, }) + // NOTE: videoEmbeddable will show only videos that can be played outside youtube + // const params = new URLSearchParams({ + // type: 'video', + // videoEmbeddable: 'true' + // }); + if (pageToken) params.set('pageToken', pageToken) return fetch( `https://www.googleapis.com/youtube/v3/search?${params.toString()}` @@ -61,10 +71,10 @@ export default function youtubeSearch(q: string): Promise { .then((response) => response.json()) .then((result: SearchResult) => { console.log('zomg youtube resultz! data:', result) - return result.items + return result }) .catch((error) => { console.warn('onoz! youtube search caught error:', error) - return [] + return null }) } diff --git a/makefile b/makefile new file mode 100644 index 0000000..0decbd7 --- /dev/null +++ b/makefile @@ -0,0 +1,73 @@ +# === Variables === +CARGO := cargo +SERVER := youoke-server +WORKER := youoke-worker +PLAYER := youoke-player +DB := queue.db + +# === Help (default target) === +.PHONY: help +help: + @echo "πŸ“¦ Usage: make [target]" + @echo "" + @echo "πŸ› οΈ Build & Run:" + @echo " make build - Build all workspace binaries" + @echo " make run-server - Run the WebSocket/HTTP server" + @echo " make run-pub-server - Run the WebSocket/HTTP server on 0.0.0.0" + @echo " make run-worker - Run the job worker processor" + @echo " make run-player - Run the player thing" + @echo "" + @echo "🧹 Dev Utilities:" + @echo " make fmt - Format code using rustfmt" + @echo " make lint - Lint all targets with clippy" + @echo " make clean - Clean target artifacts" + @echo " make reset-db - Delete SQLite database (queue.db)" + @echo "" + +# Default to help if no target is specified +.DEFAULT_GOAL := help + +# === Build All Binaries === +.PHONY: build +build: + $(CARGO) build --workspace + +# === Run Server === +.PHONY: run-server +run-server: + LIB_DIR=./server/library PLAYER_DIR=./player/public HANDSHAKE_CODE=666666 $(CARGO) run --bin $(SERVER) + +# === Run Pub Server === +.PHONY: run-pub-server +run-pub-server: + WS_ADDRESS=0.0.0.0:9001 HTTP_ADDRESS=0.0.0.0:9002 LIB_DIR=./server/library PLAYER_DIR=./player/public HANDSHAKE_CODE=666666 $(CARGO) run --bin $(SERVER) + +# === Run Worker === +.PHONY: run-worker +run-worker: + $(CARGO) run --bin $(WORKER) + +# === Run Player === +.PHONY: run-player +run-player: + $(CARGO) run --bin $(PLAYER) + +# === Format Code === +.PHONY: fmt +fmt: + $(CARGO) fmt --all + +# === Lint === +.PHONY: lint +lint: + $(CARGO) clippy --all-targets --all-features -- -D warnings + +# === Clean === +.PHONY: clean +clean: + $(CARGO) clean + +# === Reset DB === +.PHONY: reset-db +reset-db: + rm -f $(DB) diff --git a/player/Cargo.toml b/player/Cargo.toml new file mode 100644 index 0000000..a4842ad --- /dev/null +++ b/player/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "youoke-player" +version = "0.1.0" +authors = ["eeddee "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +# [dependencies] +# tungstenite = "0.17.3" +# tokio-tungstenite = "0.17.2" +# futures-channel = "0.3.23" +# futures-util = "0.3.23" +# tokio = { version = "1.20.1", features = ["full"] } +# log = "0.4.17" +# env_logger = "0.9.0" +# url = "2.2.2" +# serde_json = "1.0" +# serde = { version = "1.0", features = ["derive"] } +# glob = "0.3.0" +# # v2 stuff +# warp = "0.3" +# futures = "0.3" +# web-view = "0.7" + +# [build-dependencies] +# tauri-build = { version = "2", features = [] } + +[dependencies] +tokio = { version = "1", features = ["full"] } +warp = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +futures = "0.3" +# tauri = { version = "2", features = ["api-all"] } +# tauri = { version = "2", features = [] } +# tauri-plugin-opener = "2" +# glob = "0.3.2" diff --git a/player/README.md b/player/README.md new file mode 100644 index 0000000..e930997 --- /dev/null +++ b/player/README.md @@ -0,0 +1,3 @@ +# youoke-player + +`RUST_LOG=info cargo run` diff --git a/player/public/bumper.html b/player/public/bumper.html new file mode 100644 index 0000000..c043eac --- /dev/null +++ b/player/public/bumper.html @@ -0,0 +1,265 @@ + + + + + youoke.party bump'r + + + + +
    + +
     
    +
    + + + diff --git a/player/public/bumper.js b/player/public/bumper.js new file mode 100644 index 0000000..51107c4 --- /dev/null +++ b/player/public/bumper.js @@ -0,0 +1,140 @@ +function hide() { + bumper.canvas.classList.add('hidden') +} + +function wrapText(text, fontSize) { + bumper.ctx.font = `${fontSize}px 'VCR OSD Mono'` + const words = text.split(' ') + const lines = [] + let line = '' + + for (let i = 0; i < words.length; i++) { + let testLine = line + words[i] + ' ' + let testWidth = bumper.ctx.measureText(testLine).width + + if (testWidth > bumper.maxWidth && i > 0) { + lines.push(line.trim()) + line = words[i] + ' ' + } else { + line = testLine + } + } + lines.push(line.trim()) + return lines +} + +function render(messages, startIndex, done) { + bumper.messages = messages + console.log('zomg gonna start bumper::', bumper) + bumper.canvas.classList.remove('hidden') + const messageIndex = Number(startIndex) || 0 + const text = messages[messageIndex] + bumper.fontSize = 10 + + // calculate biggest font size + while (true) { + const testLines = wrapText(text, bumper.fontSize) + const totalHeight = testLines.length * bumper.fontSize * 1.2 + const widestLine = Math.max( + ...testLines.map((line) => bumper.ctx.measureText(line).width) + ) + + if (widestLine > bumper.maxWidth || totalHeight > bumper.maxHeight) { + bumper.fontSize -= 2 + bumper.lines = wrapText(text, bumper.fontSize) + break + } + + bumper.fontSize += 2 + } + + // clear canvas + // bumper.ctx.fillStyle = '#000000' + // bumper.ctx.fillRect(0, 0, bumper.canvas.width, bumper.canvas.height) + + const totalHeight = bumper.lines.length * bumper.fontSize * 1.2 + const baseY = (bumper.canvas.height - totalHeight) / 2 + bumper.fontSize + + let scanX = 0 + let scanY = 0 + const stepX = 25 + const stepY = 25 + const delay = 10 + + function scanFrame() { + bumper.ctx.save() + bumper.ctx.font = `${bumper.fontSize}px 'VCR OSD Mono'` + bumper.ctx.fillStyle = 'white' + bumper.ctx.shadowColor = 'magenta' + bumper.ctx.shadowBlur = parseInt(Math.random() * 69) + + bumper.ctx.beginPath() + bumper.ctx.rect(0, 0, scanX, scanY) + bumper.ctx.clip() + + bumper.lines.forEach((line, i) => { + const lineWidth = bumper.ctx.measureText(line).width + const x = (bumper.canvas.width - lineWidth) / 2 + const y = baseY + i * bumper.fontSize * 1.2 + bumper.ctx.fillText(line, x, y) + }) + + bumper.ctx.restore() + + scanX += stepX + if (scanX > bumper.canvas.width) { + scanX = 0 + scanY += stepY + } + + if (scanY <= bumper.canvas.height) { + setTimeout(scanFrame, delay) + } else { + // pause before complete (or starting again) + if (messageIndex + 1 < messages.length) { + setTimeout(() => bumper.render(messages, messageIndex + 1, done), 1000) + } else { + setTimeout(() => { + console.log('zee bumperz are done and done ANDAND !!done:', !!done) + done && bumper.hide() + done && done() + }, 1000) + } + } + } + + scanFrame() +} + +function setup() { + const canvas = document.getElementById('bumper') + const ctx = canvas.getContext('2d') + const maxWidth = canvas.width * 0.9 + const maxHeight = canvas.height * 0.9 + + let fontSize = 10 + let lines = [] + + bumper = { + ...bumper, + canvas, + ctx, + maxWidth, + maxHeight, + fontSize, + lines, + } +} + +const DEMO_MESSAGES = [ + 'DO: SWEET EMOTIONZ IN THE NIGHT; END;', + 'DANCING THROUGH DIGITAL STORMZ && LOVE', + 'THE FUTURE IN A VHS DREAM', +] + +let bumper = { + setup, + render, + hide, + DEMO_MESSAGES, +} diff --git a/server/examples/fonts/VCROSDMono.woff b/player/public/fonts/VCROSDMono.woff similarity index 100% rename from server/examples/fonts/VCROSDMono.woff rename to player/public/fonts/VCROSDMono.woff diff --git a/server/examples/fonts/VCROSDMono.woff2 b/player/public/fonts/VCROSDMono.woff2 similarity index 100% rename from server/examples/fonts/VCROSDMono.woff2 rename to player/public/fonts/VCROSDMono.woff2 diff --git a/server/examples/fonts/fonts.css b/player/public/fonts/fonts.css similarity index 100% rename from server/examples/fonts/fonts.css rename to player/public/fonts/fonts.css diff --git a/player/public/index.html b/player/public/index.html new file mode 100644 index 0000000..4487733 --- /dev/null +++ b/player/public/index.html @@ -0,0 +1,516 @@ + + + + + youoke.party player + + + + + + + + +
    +
      + + +
      + +
      +
      +
      +
      + + + + diff --git a/player/public/youoke.png b/player/public/youoke.png new file mode 100644 index 0000000..06920bf Binary files /dev/null and b/player/public/youoke.png differ diff --git a/player/src/main.rs b/player/src/main.rs new file mode 100644 index 0000000..748747a --- /dev/null +++ b/player/src/main.rs @@ -0,0 +1,208 @@ +// add to Cargo.toml these: +// [dependencies] +// tokio = { version = "1", features = ["full"] } +// warp = "0.3" +// serde = { version = "1", features = ["derive"] } +// serde_json = "1" +// futures = "0.3" +// web-view = "0.7" + +// main.rs +use futures::{SinkExt, StreamExt}; +use serde::{Deserialize, Serialize}; +use std::{convert::Infallible, process::Command, sync::Arc}; +use tokio::sync::Mutex; +use warp::ws::{Message, WebSocket}; +use warp::Filter; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +enum CommandMessage { + ReadFile { path: String }, + WriteFile { path: String, data: String }, + SpawnProcess { command: String }, + ForwardToPlayer { action: String }, + Sleep { seconds: u64 }, +} + +#[derive(Default)] +struct AppState { + player_channel: Option>, +} + +#[derive(Debug)] +struct Unauthorized; +impl warp::reject::Reject for Unauthorized {} + +type SharedState = Arc>; + +const SECRET_CODE: &str = "aaahhh"; + +#[tokio::main] +async fn main() { + let state: SharedState = Arc::new(Mutex::new(AppState::default())); + let (player_tx, mut player_rx) = tokio::sync::mpsc::channel(32); + state.lock().await.player_channel = Some(player_tx); + + // Start the central "player" task + tokio::spawn(async move { + while let Some(msg) = player_rx.recv().await { + println!("[Player] Received action: {}", msg); + } + }); + + let state_filter = warp::any().map(move || state.clone()); + + let ws_route = warp::path("ws") + .and(warp::ws()) + .and(warp::query::>()) // extract query params + .and(state_filter.clone()) + .and_then(handle_ws_request); + // .map(|ws: warp::ws::Ws, state| { + // ws.on_upgrade(move |socket| handle_connection(socket, state)) + // }); + + let static_files = warp::fs::dir("./locallibrary"); + + let routes = ws_route.or(static_files); + + println!("websocket server running at ws://localhost:3030/ws"); + println!("serving ./locallibrary at http://localhost:3030/"); + + // tokio::spawn(spawn_webview()); + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} + +async fn handle_ws_request( + ws: warp::ws::Ws, + query: std::collections::HashMap, + state: SharedState, +) -> Result { + if let Some(code) = query.get("code") { + if code == SECRET_CODE { + return Ok(ws.on_upgrade(move |socket| handle_connection(socket, state))); + } + } + + Err(warp::reject::custom(Unauthorized)) +} + +async fn handle_connection(ws: WebSocket, state: SharedState) { + let (mut tx, mut rx) = ws.split(); + + while let Some(result) = rx.next().await { + match result { + Ok(msg) if msg.is_text() => { + let msg_text = msg.to_str().unwrap_or(""); + if let Ok(command) = serde_json::from_str::(msg_text) { + let response = handle_command(command, state.clone()).await; + if let Some(resp) = response { + let _ = tx.send(Message::text(resp)).await; + } + } + } + _ => break, + } + } +} + +async fn handle_command(cmd: CommandMessage, state: SharedState) -> Option { + match cmd { + CommandMessage::ReadFile { path } => { + let contents = tokio::fs::read_to_string(path).await.ok()?; + Some(contents) + } + CommandMessage::WriteFile { path, data } => { + tokio::fs::write(path, data).await.ok()?; + Some("ok".to_string()) + } + CommandMessage::SpawnProcess { command } => { + tokio::spawn(async move { + let _ = Command::new("sh").arg("-c").arg(&command).spawn(); + }); + Some("spawned".to_string()) + } + CommandMessage::ForwardToPlayer { action } => { + let state = state.lock().await; + if let Some(sender) = &state.player_channel { + let _ = sender.send(action.clone()).await; + } + Some("forwarded".to_string()) + } + CommandMessage::Sleep { seconds } => { + tokio::time::sleep(std::time::Duration::from_secs(seconds)).await; + Some(format!("slept for {} seconds", seconds)) + } + } +} + +// fn spawn_webview() { +// std::thread::spawn(|| { +// // web_view::builder() +// // .title("Rust WebSocket UI") +// // .content(web_view::Content::Url("http://localhost:3030")) +// // .size(800, 600) +// // .resizable(true) +// // .debug(true) +// // .user_data(()) +// // .invoke_handler(|_webview, _arg| Ok(())) +// // .run() +// // .unwrap(); + +// // hello_tauri_lib::run(); + +// let _ = Command::new("sh").arg("-c").arg("echo hold on plz").spawn(); +// }); +// } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_sleep_command() { + let state = Arc::new(Mutex::new(AppState::default())); + let start = std::time::Instant::now(); + let result = handle_command(CommandMessage::Sleep { seconds: 1 }, state).await; + assert_eq!(result.unwrap(), "slept for 1 seconds"); + assert!(start.elapsed().as_secs() >= 1); + } + + #[tokio::test] + async fn test_write_and_read_file() { + let state = Arc::new(Mutex::new(AppState::default())); + let path = "testfile.txt".to_string(); + let data = "hello, world".to_string(); + + let write_result = handle_command( + CommandMessage::WriteFile { + path: path.clone(), + data: data.clone(), + }, + state.clone(), + ) + .await; + assert_eq!(write_result.unwrap(), "ok"); + + let read_result = handle_command(CommandMessage::ReadFile { path }, state.clone()).await; + assert_eq!(read_result.unwrap(), data); + } + + #[tokio::test] + async fn test_forward_to_player() { + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + let mut state = AppState::default(); + state.player_channel = Some(tx); + let shared_state = Arc::new(Mutex::new(state)); + + let result = handle_command( + CommandMessage::ForwardToPlayer { + action: "test_action".to_string(), + }, + shared_state.clone(), + ) + .await; + assert_eq!(result.unwrap(), "forwarded"); + assert_eq!(rx.recv().await.unwrap(), "test_action"); + } +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 090bab0..b4aabb7 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -16,5 +16,11 @@ log = "0.4.17" env_logger = "0.9.0" url = "2.2.2" serde_json = "1.0" -serde = {version = "1.0", features = ["derive"]} -glob = "0.3.0" +serde = { version = "1.0", features = ["derive"] } +glob = "0.3.2" +# v2 stuff +futures = "0.3" +warp = "0.3" +rusqlite = { version = "0.30", features = ["bundled"] } + +shared = { path = "../shared" } diff --git a/server/examples/player.html b/server/examples/player.html deleted file mode 100644 index 13c01bb..0000000 --- a/server/examples/player.html +++ /dev/null @@ -1,217 +0,0 @@ - - - - - youoke.party player - - - - - -
      - -
        - -
        - - - - diff --git a/server/examples/sqlitedb.rs b/server/examples/sqlitedb.rs new file mode 100644 index 0000000..17d66fb --- /dev/null +++ b/server/examples/sqlitedb.rs @@ -0,0 +1,28 @@ +use rusqlite::Connection; +use serde_json::Value; +use shared::{db, db_worker::DbWorker, models::Job}; +use std::sync::Arc; +use warp::Filter; + +// use crate::db_worker::DbWorker; + +#[tokio::main] +async fn main() { + let db = DbWorker::start("queue.db"); + let db_filter = warp::any().map(move || db.clone()); + + let ws_route = warp::path("submit") + .and(warp::body::json()) + .and(db_filter) + .and_then(handle_submit); + + println!("πŸš€ Server running on http://localhost:3030"); + warp::serve(ws_route).run(([127, 0, 0, 1], 3030)).await; +} + +pub async fn handle_submit(json: Value, db: DbWorker) -> Result { + let payload = serde_json::to_string(&json).unwrap(); + + db.enque("chat", &payload); // enqueue is a synchronous send() + Ok(warp::reply::json(&"queued")) +} diff --git a/server/examples/v2.rs b/server/examples/v2.rs new file mode 100644 index 0000000..6c04030 --- /dev/null +++ b/server/examples/v2.rs @@ -0,0 +1,195 @@ +// add to Cargo.toml these: +// [dependencies] +// tokio = { version = "1", features = ["full"] } +// warp = "0.3" +// serde = { version = "1", features = ["derive"] } +// serde_json = "1" +// futures = "0.3" +// web-view = "0.7" + +// main.rs +use futures::{SinkExt, StreamExt}; +use serde::{Deserialize, Serialize}; +use std::{convert::Infallible, process::Command, sync::Arc}; +use tokio::sync::Mutex; +use warp::ws::{Message, WebSocket}; +use warp::Filter; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +enum CommandMessage { + ReadFile { path: String }, + WriteFile { path: String, data: String }, + SpawnProcess { command: String }, + ForwardToPlayer { action: String }, + Sleep { seconds: u64 }, +} + +#[derive(Default)] +struct AppState { + player_channel: Option>, +} + +#[derive(Debug)] +struct Unauthorized; + +impl warp::reject::Reject for Unauthorized {} + +type SharedState = Arc>; + +const SECRET_CODE: &str = "aaahhh"; + +#[tokio::main] +async fn main() { + let state: SharedState = Arc::new(Mutex::new(AppState::default())); + let (player_tx, mut player_rx) = tokio::sync::mpsc::channel(32); + state.lock().await.player_channel = Some(player_tx); + + // Start the central "player" task + tokio::spawn(async move { + while let Some(msg) = player_rx.recv().await { + println!("[Player] Received action: {}", msg); + } + }); + + let state_filter = warp::any().map(move || state.clone()); + + let ws_route = warp::path("ws") + .and(warp::ws()) + .and(warp::query::>()) // extract query params + .and(state_filter.clone()) + .and_then(handle_ws_request); + // .map(|ws: warp::ws::Ws, state| { + // ws.on_upgrade(move |socket| handle_connection(socket, state)) + // }); + + let hello_route = warp::path("hello") + .and(warp::get()) + .map(|| warp::reply::with_status("hello!", warp::http::StatusCode::OK)); + let static_files = warp::fs::dir("./library"); + + let routes = ws_route.or(static_files).or(hello_route); + + println!("websocket server running at ws://localhost:3030/ws"); + println!("serving ./library at http://localhost:3030/"); + + // tokio::spawn(spawn_webview()); + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} + +// pub async fn handle_submit(json: Value, db: DbWorker) -> Result { + +async fn handle_ws_request( + ws: warp::ws::Ws, + query: std::collections::HashMap, + state: SharedState, +) -> Result { + if let Some(code) = query.get("code") { + if code == SECRET_CODE { + return Ok(ws.on_upgrade(move |socket| handle_connection(socket, state))); + } + } + + Err(warp::reject::custom(Unauthorized)) +} + +async fn handle_connection(ws: WebSocket, state: SharedState) { + let (mut tx, mut rx) = ws.split(); + + while let Some(result) = rx.next().await { + match result { + Ok(msg) if msg.is_text() => { + let msg_text = msg.to_str().unwrap_or(""); + if let Ok(command) = serde_json::from_str::(msg_text) { + let response = handle_command(command, state.clone()).await; + if let Some(resp) = response { + let _ = tx.send(Message::text(resp)).await; + } + } + } + _ => break, + } + } +} + +async fn handle_command(cmd: CommandMessage, state: SharedState) -> Option { + match cmd { + CommandMessage::ReadFile { path } => { + let contents = tokio::fs::read_to_string(path).await.ok()?; + Some(contents) + } + CommandMessage::WriteFile { path, data } => { + tokio::fs::write(path, data).await.ok()?; + Some("ok".to_string()) + } + CommandMessage::SpawnProcess { command } => { + tokio::spawn(async move { + let _ = Command::new("sh").arg("-c").arg(&command).spawn(); + }); + Some("spawned".to_string()) + } + CommandMessage::ForwardToPlayer { action } => { + let state = state.lock().await; + if let Some(sender) = &state.player_channel { + let _ = sender.send(action.clone()).await; + } + Some("forwarded".to_string()) + } + CommandMessage::Sleep { seconds } => { + tokio::time::sleep(std::time::Duration::from_secs(seconds)).await; + Some(format!("slept for {} seconds", seconds)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_sleep_command() { + let state = Arc::new(Mutex::new(AppState::default())); + let start = std::time::Instant::now(); + let result = handle_command(CommandMessage::Sleep { seconds: 1 }, state).await; + assert_eq!(result.unwrap(), "slept for 1 seconds"); + assert!(start.elapsed().as_secs() >= 1); + } + + #[tokio::test] + async fn test_write_and_read_file() { + let state = Arc::new(Mutex::new(AppState::default())); + let path = "testfile.txt".to_string(); + let data = "hello, world".to_string(); + + let write_result = handle_command( + CommandMessage::WriteFile { + path: path.clone(), + data: data.clone(), + }, + state.clone(), + ) + .await; + assert_eq!(write_result.unwrap(), "ok"); + + let read_result = handle_command(CommandMessage::ReadFile { path }, state.clone()).await; + assert_eq!(read_result.unwrap(), data); + } + + #[tokio::test] + async fn test_forward_to_player() { + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + let mut state = AppState::default(); + state.player_channel = Some(tx); + let shared_state = Arc::new(Mutex::new(state)); + + let result = handle_command( + CommandMessage::ForwardToPlayer { + action: "test_action".to_string(), + }, + shared_state.clone(), + ) + .await; + assert_eq!(result.unwrap(), "forwarded"); + assert_eq!(rx.recv().await.unwrap(), "test_action"); + } +} diff --git a/server/src/main.rs b/server/src/main.rs index 22afa79..d03836f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -2,11 +2,12 @@ use std::{ collections::HashMap, env, fs::{canonicalize, create_dir_all, read_to_string}, - io::Error as IoError, + io::{self, Error as IoError}, net::SocketAddr, path::{Path, PathBuf}, process::{Command, Stdio}, sync::{Arc, Mutex}, + time::{SystemTime, UNIX_EPOCH}, }; use glob::glob; @@ -15,9 +16,18 @@ use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; use futures_util::{future, pin_mut, stream::TryStreamExt, StreamExt}; use tokio::net::{TcpListener, TcpStream}; -use tokio_tungstenite::accept_async; +use tokio_tungstenite::accept_hdr_async; + +use tokio_tungstenite::tungstenite::handshake::server::{ + ErrorResponse as HandshakeErrorResponse, Request as HandshakeRequest, + Response as HandshakeResponse, +}; use tungstenite::protocol::Message; +use warp::cors; +use warp::http::StatusCode; +use warp::Filter; + use log::*; use serde::{Deserialize, Serialize}; use serde_json; @@ -38,7 +48,9 @@ struct QueueItem { title: String, singer: String, filepath: String, + filename: String, duration: usize, + wallmessage: String, status: QueueItemStatus, } @@ -74,12 +86,16 @@ enum Request { id: String, status: QueueItemStatus, filepath: String, + filename: String, title: String, duration: usize, }, PlayerPlay, PlayerPause, PlayerSkip, + PlayerSetWallmessage { + wallmessage: String, + }, GetLibrary, Error, } @@ -141,10 +157,32 @@ async fn main() -> Result<(), IoError> { .into_string() .unwrap(); + let player_dir = match env::var_os("PLAYER_DIR") { + Some(val) => val.into_string().unwrap(), + None => "../player/public".to_string(), + }; + + if !Path::new(&player_dir).is_dir() { + panic!("player dir does not exist!") + } + let player_path = PathBuf::from(&player_dir); + let player_path = canonicalize(&player_path) + .unwrap() + .into_os_string() + .into_string() + .unwrap(); + let addr = match env::var_os("WS_ADDRESS") { Some(val) => val.into_string().unwrap(), None => "127.0.0.1:9001".to_string(), }; + + let http_address = match env::var_os("HTTP_ADDRESS") { + Some(val) => val.into_string().unwrap(), + None => "127.0.0.1:9002".to_string(), + }; + let http_addr: SocketAddr = http_address.parse().expect("Invalid HTTP_ADDRESS"); + let peer_map = PeerMap::new(Mutex::new(HashMap::new())); let queue: Vec = vec![]; @@ -177,6 +215,61 @@ async fn main() -> Result<(), IoError> { q_sender.clone(), )); + println!( + "serving /hello and library({}) and player({}) at http://{}", + &library_path, &player_path, &http_addr, + ); + + // warp server setup stuff for /hello code-check and static file hosting + let cors = cors() + .allow_any_origin() + .allow_methods(vec!["GET", "POST", "OPTIONS"]) // Add methods you need + .allow_headers(vec!["Content-Type"]); // Optional: allow custom headers + + let handshake_code = match env::var_os("HANDSHAKE_CODE") { + Some(val) => Arc::new(val.into_string().unwrap()), + None => Arc::new(generate_code()), + }; + + let code_filter = warp::any().map({ + let secret_code = handshake_code.clone(); // Clone into filter + move || secret_code.clone() + }); + + let hello_route = warp::path("hello") + .and(warp::get()) + .and(warp::query::>()) + .and(code_filter) + .map( + |query: HashMap, secret_code: Arc| match query.get("code") { + Some(code) if code == secret_code.as_str() => { + warp::reply::with_status("hello!", StatusCode::OK) + } + _ => warp::reply::with_status("unauthorized", StatusCode::UNAUTHORIZED), + }, + ) + .with(&cors); + + let player_route = warp::path("player") + .and(warp::fs::dir(player_path)) + .with(&cors); + + let static_files = warp::fs::dir(library_path.clone()).with(&cors); + + let routes = hello_route.or(static_files).or(player_route); + + tokio::task::spawn(warp::serve(routes).run(http_addr)); + + println!(""); + println!("- - - - - - - - - -"); + println!("-> HANDSHAKE CODE"); + println!("-> {}", handshake_code.as_str()); + println!( + "-> http://localhost:3000?href={}&name={}&code={}", + "localhost%3A9001", "localdev", &handshake_code + ); + println!("- - - - - - - - - -"); + println!(""); // spawn the handling of each connection in a separate task. while let Ok((stream, addr)) = listener.accept().await { tokio::spawn(connection_handler( @@ -185,6 +278,7 @@ async fn main() -> Result<(), IoError> { addr, q_sender.clone(), library_path.clone(), + handshake_code.clone(), )); } @@ -197,11 +291,40 @@ async fn connection_handler( addr: SocketAddr, q_sender: UnboundedSender, library_path: String, + handshake_code: Arc, ) { info!("incoming TCP connection from: {}", addr); - let ws_stream = accept_async(raw_stream) - .await - .expect("error during the websocket handshake occurred"); + + // let check_handshake_code = |req: &HandshakeRequest, resp: HandshakeResponse| { + let check_handshake_code = move |req: &HandshakeRequest, + resp: HandshakeResponse| + -> Result { + info!("zomg gonna check_handshake_code"); + if let Some(uri) = req.uri().path_and_query() { + if let Ok(parsed_url) = url::Url::parse(&format!("http://localhost{}", uri)) { + if let Some(code) = parsed_url.query_pairs().find(|(k, _)| k == "code") { + info!("zomg checking code {}", code.1); + if code.1 == *handshake_code { + info!("zomg okay good handshake bro!"); + return Ok(resp); + } + } + } + } + + Err(HandshakeErrorResponse::new(Some( + "Unauthorized".to_string(), + ))) + }; + + // let ws_stream = accept_hdr_async(raw_stream, check_handshake_code).await?; + let ws_stream = match accept_hdr_async(raw_stream, check_handshake_code).await { + Ok(stream) => stream, + Err(err) => { + eprintln!("Handshake failed: {}", err); + return; // or future::ok(()) if you’re returning a future + } + }; info!("WebSocket connection established: {}", addr); let (tx, rx) = unbounded(); // insert the write (tx) part of this peer to the peer map @@ -280,6 +403,23 @@ async fn connection_handler( _ => {} } } + Request::PlayerSetWallmessage { wallmessage } => { + info!("PlayerSetWallmessage wallmessage:{}", &wallmessage); + // broadcast the player msg to everyone (but maybe only the player truely needz this?) + let peers = peer_map.lock().unwrap(); + let broadcast_recipients = peers.iter().map(|(_, ws_sink)| ws_sink); + for recp in broadcast_recipients { + let msg = serde_json::to_value(Request::PlayerSetWallmessage { + wallmessage: wallmessage.clone(), + }) + .unwrap() + .to_string(); + + recp.unbounded_send(Message::Text(msg)).unwrap_or_default(); + } + // #TODO: could sending this back to the sender be like, a confirm? πŸ€” + // q_sender.unbounded_send(request).unwrap_or_default(); + } _ => { q_sender.unbounded_send(request).unwrap_or_default(); } @@ -309,6 +449,10 @@ async fn queue_handler( Request::Error | Request::PlayerPause | Request::PlayerPlay | Request::GetLibrary => { false } // note: stop here if any of these (no queue response needed) + Request::PlayerSetWallmessage { wallmessage } => { + info!("{}", wallmessage); + false + } Request::PlayerSkip => { if queue.len() > 0 { queue.remove(0); @@ -334,7 +478,9 @@ async fn queue_handler( status: QueueItemStatus::Downloading, duration: 0, filepath: "".to_owned(), + filename: "".to_owned(), title: "".to_owned(), + wallmessage: "".to_owned(), }); f_sender @@ -375,6 +521,7 @@ async fn queue_handler( id, status, filepath, + filename, title, duration, } => { @@ -389,6 +536,7 @@ async fn queue_handler( queue_item.filepath = filepath; queue_item.title = title; queue_item.duration = duration; + queue_item.filename = filename; } None => {} }, @@ -453,7 +601,8 @@ async fn file_handler( Ok(contents) => { match serde_json::from_str::(&contents) { Ok(parsed) => { - let mut filepath = format!("{}.{}", parsed.id, parsed.ext); + let filename = format!("{}.{}", parsed.id, parsed.ext); + let mut filepath = filename.clone(); //format!("{parsed.id}{parsed.ext}"); // validate filename is really a path & file on disk if !Path::new(&filepath).is_file() { @@ -474,6 +623,7 @@ async fn file_handler( id: id.clone(), status: QueueItemStatus::Ready, filepath: filepath, + filename: filename, title: parsed.title, duration: parsed.duration, }) @@ -488,6 +638,7 @@ async fn file_handler( id: id.clone(), status: QueueItemStatus::Ready, filepath: filepath, + filename: filename, title: parsed.title, duration: parsed.duration, }) @@ -542,7 +693,7 @@ async fn download_handler( .arg(&id) // note: this handles video IDz that start with a dash (-) .output() // note: sleep for debuggin. - // let response: Request = match Command::new("sleep").arg("1").output() + // let response: Request = match Command::new("sleep").arg("1").output() { Ok(output) => { info!("download_handler yt-dlp output: {:#?}", output); @@ -565,7 +716,8 @@ async fn download_handler( let parsed: YoutubeDlJSON = serde_json::from_str(&contents) .expect("download_handler panic! can't parse to JSON"); // let mut filepath = parsed._filename; - let mut filepath = format!("{}.{}", parsed.id, parsed.ext); + let filename = format!("{}.{}", parsed.id, parsed.ext); + let mut filepath = filename.clone(); // validate filename is really a path & file on disk if !Path::new(&filepath).is_file() { info!("ugh ref file not on filesystem gonna try to find {}/{}*[!json]", &library_path, &id); @@ -584,6 +736,7 @@ async fn download_handler( id: id, status: QueueItemStatus::Ready, filepath: filepath, + filename: filename, title: parsed.title, duration: parsed.duration, } @@ -610,6 +763,17 @@ async fn download_handler( Ok(()) } +fn generate_code() -> String { + // #todo: use rand crate + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .subsec_nanos(); + + let code = (nanos % 1_000_000).to_string(); + format!("{:0>6}", code) // zero-pad to 6 digits +} + #[cfg(test)] mod tests { use super::*; @@ -686,8 +850,10 @@ mod tests { title: "".to_owned(), singer: "frankie frankie".to_owned(), filepath: "".to_owned(), + filename: "".to_owned(), duration: 0, status: QueueItemStatus::Downloading, + wallmessage: "".to_owned(), }; let queue = vec![q_item]; diff --git a/shared/Cargo.toml b/shared/Cargo.toml new file mode 100644 index 0000000..59889af --- /dev/null +++ b/shared/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "shared" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rusqlite = { version = "0.30", features = ["bundled"] } +thiserror = "1.0" +chrono = "0.4" diff --git a/shared/src/db.rs b/shared/src/db.rs new file mode 100644 index 0000000..6d5d49c --- /dev/null +++ b/shared/src/db.rs @@ -0,0 +1,52 @@ +use crate::models::Job; +use chrono::Utc; +use rusqlite::{params, Connection}; + +pub fn init_db(conn: &Connection) { + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS job_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + status TEXT NOT NULL, + job_type TEXT NOT NULL, + payload TEXT NOT NULL, + created_at TEXT, + updated_at TEXT + );", + ) + .unwrap(); +} + +pub fn enqueue(conn: &Connection, job_type: &str, payload: &str) { + let now = Utc::now().to_rfc3339(); + conn.execute( + "INSERT INTO job_queue (status, job_type, payload, created_at, updated_at) VALUES ('pending', ?1, ?2, ?3, ?3)", + params![job_type, payload, now], + ).unwrap(); +} + +pub fn fetch_pending(conn: &Connection, limit: usize) -> Vec { + let mut stmt = conn.prepare( + "SELECT id, status, job_type, payload FROM job_queue WHERE status = 'pending' ORDER BY created_at LIMIT ?1", + ).unwrap(); + + let rows = stmt + .query_map(params![limit], |row| { + Ok(Job { + id: row.get(0)?, + status: row.get(1)?, + job_type: row.get(2)?, + payload: row.get(3)?, + }) + }) + .unwrap(); + + rows.filter_map(Result::ok).collect() +} + +pub fn update_status(conn: &Connection, id: i64, status: &str) { + conn.execute( + "UPDATE job_queue SET status = ?1, updated_at = ?2 WHERE id = ?3", + params![status, Utc::now().to_rfc3339(), id], + ) + .unwrap(); +} diff --git a/shared/src/db_worker.rs b/shared/src/db_worker.rs new file mode 100644 index 0000000..2a1ba36 --- /dev/null +++ b/shared/src/db_worker.rs @@ -0,0 +1,78 @@ +use rusqlite::Connection; +use std::sync::mpsc; +use std::thread; + +use crate::db; +use crate::models::Job; + +#[derive(Clone)] +pub struct DbWorker { + tx: mpsc::Sender, +} + +enum DbCommand { + FetchPending { + limit: usize, + respond_to: mpsc::Sender>, + }, + UpdateStatus { + id: i64, + status: String, + }, + Enque { + job_type: String, + payload: String, + }, +} + +impl DbWorker { + pub fn start(db_path: &str) -> Self { + let (tx, rx) = mpsc::channel::(); + let path = db_path.to_string(); + + thread::spawn(move || { + let conn = Connection::open(path).unwrap(); + db::init_db(&conn); + + for cmd in rx { + match cmd { + DbCommand::FetchPending { limit, respond_to } => { + let jobs = db::fetch_pending(&conn, limit); + let _ = respond_to.send(jobs); + } + DbCommand::UpdateStatus { id, status } => { + db::update_status(&conn, id, &status); + } + DbCommand::Enque { job_type, payload } => { + db::enqueue(&conn, &job_type, &payload); + } + } + } + }); + + DbWorker { tx } + } + + pub fn fetch_pending(&self, limit: usize) -> Vec { + let (tx, rx) = mpsc::channel(); + let _ = self.tx.send(DbCommand::FetchPending { + limit, + respond_to: tx, + }); + rx.recv().unwrap_or_else(|_| vec![]) + } + + pub fn update_status(&self, id: i64, status: &str) { + let _ = self.tx.send(DbCommand::UpdateStatus { + id, + status: status.to_string(), + }); + } + + pub fn enque(&self, job_type: &str, status: &str) { + let _ = self.tx.send(DbCommand::Enque { + job_type: job_type.to_string(), + payload: status.to_string(), + }); + } +} diff --git a/shared/src/lib.rs b/shared/src/lib.rs new file mode 100644 index 0000000..ccab64d --- /dev/null +++ b/shared/src/lib.rs @@ -0,0 +1,3 @@ +pub mod db; +pub mod db_worker; +pub mod models; diff --git a/shared/src/models.rs b/shared/src/models.rs new file mode 100644 index 0000000..913dbf1 --- /dev/null +++ b/shared/src/models.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone)] +pub struct Job { + pub id: i64, + pub job_type: String, + pub status: String, + pub payload: String, +} diff --git a/worker/Cargo.toml b/worker/Cargo.toml new file mode 100644 index 0000000..440f261 --- /dev/null +++ b/worker/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "youoke-worker" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["full"] } +tokio-tungstenite = "0.17.2" +futures = "0.3" +futures-util = "0.3.23" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rusqlite = { version = "0.30", features = ["bundled"] } + +shared = { path = "../shared" } diff --git a/worker/src/job_handlers.rs b/worker/src/job_handlers.rs new file mode 100644 index 0000000..28920b7 --- /dev/null +++ b/worker/src/job_handlers.rs @@ -0,0 +1,36 @@ +use shared::models::Job; +use tokio::time::{sleep, Duration}; + +pub async fn process_job(job: Job) -> Result<(), String> { + match job.job_type.as_str() { + "auth" => process_auth(job).await, + "chat" => process_chat(job).await, + "email" => process_email(job).await, + "fook" => process_fook(job).await, + other => Err(format!("Unknown job_type: {}", other)), + } +} + +async fn process_auth(job: Job) -> Result<(), String> { + println!("auth job {}: {}", job.id, job.payload); + sleep(Duration::from_secs(1)).await; + Ok(()) +} + +async fn process_chat(job: Job) -> Result<(), String> { + println!("chat job {}: {}", job.id, job.payload); + sleep(Duration::from_secs(2)).await; + Ok(()) +} + +async fn process_email(job: Job) -> Result<(), String> { + println!("email job {}: {}", job.id, job.payload); + sleep(Duration::from_secs(3)).await; + Ok(()) +} + +async fn process_fook(job: Job) -> Result<(), String> { + println!("fook job {}: {}", job.id, job.payload); + sleep(Duration::from_secs(3)).await; + Ok(()) +} diff --git a/worker/src/main.rs b/worker/src/main.rs new file mode 100644 index 0000000..ef38c52 --- /dev/null +++ b/worker/src/main.rs @@ -0,0 +1,37 @@ +mod job_handlers; + +use job_handlers::process_job; +use shared::db_worker::DbWorker; + +use tokio::time::sleep; + +#[tokio::main] +async fn main() { + let db = DbWorker::start("queue.db"); + + for _ in 0..4 { + let db = db.clone(); + tokio::spawn(async move { + loop { + let jobs = db.fetch_pending(1); + for job in jobs { + db.update_status(job.id, "in_progress"); + + match process_job(job.clone()).await { + Ok(_) => db.update_status(job.id, "done"), + Err(e) => { + eprintln!("Job {} failed: {}", job.id, e); + db.update_status(job.id, "failed"); + } + } + } + + sleep(std::time::Duration::from_millis(300)).await; + } + }); + } + + loop { + sleep(std::time::Duration::from_secs(60)).await; + } +}