diff options
| -rw-r--r-- | Cargo.lock | 826 | ||||
| -rw-r--r-- | Cargo.toml | 51 | ||||
| -rw-r--r-- | src/audio_player.rs | 328 | ||||
| -rw-r--r-- | src/bot/master.rs | 363 | ||||
| -rw-r--r-- | src/bot/music.rs | 500 | ||||
| -rw-r--r-- | src/log_bridge.rs | 101 | ||||
| -rw-r--r-- | src/main.rs | 154 | ||||
| -rw-r--r-- | src/playlist.rs | 16 | ||||
| -rw-r--r-- | src/teamspeak/mod.rs | 245 | ||||
| -rw-r--r-- | src/web_server.rs | 34 | ||||
| -rw-r--r-- | src/web_server/api.rs | 17 | ||||
| -rw-r--r-- | src/web_server/bot_data.rs | 47 | ||||
| -rw-r--r-- | src/web_server/bot_executor.rs | 63 | ||||
| -rw-r--r-- | src/web_server/default.rs | 17 | ||||
| -rw-r--r-- | src/web_server/tmtu.rs | 17 | ||||
| -rw-r--r-- | src/youtube_dl.rs | 15 |
16 files changed, 1575 insertions, 1219 deletions
@@ -2,42 +2,27 @@ # It is not intended for manual editing. [[package]] name = "actix" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4af87564ff659dee8f9981540cac9418c45e910c8072fdedd643a262a38fcaf" +checksum = "1be241f88f3b1e7e9a3fbe3b5a8a0f6915b5a1d7ee0d9a248d3376d01068cc60" dependencies = [ - "actix-http", "actix-rt", "actix_derive", "bitflags", "bytes", "crossbeam-channel", "derive_more", - "futures", - "lazy_static", + "futures-channel", + "futures-util", "log", - "parking_lot 0.10.2", + "once_cell", + "parking_lot", "pin-project", "smallvec", "tokio", - "tokio-util 0.2.0", - "trust-dns-proto 0.18.0-alpha.2", - "trust-dns-resolver 0.18.0-alpha.2", -] - -[[package]] -name = "actix-codec" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "log", - "tokio", - "tokio-util 0.2.0", + "tokio-util", + "trust-dns-proto", + "trust-dns-resolver", ] [[package]] @@ -53,35 +38,34 @@ dependencies = [ "log", "pin-project", "tokio", - "tokio-util 0.3.1", + "tokio-util", ] [[package]] name = "actix-connect" -version = "1.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c" +checksum = "177837a10863f15ba8d3ae3ec12fac1099099529ed20083a27fdfe247381d0dc" dependencies = [ - "actix-codec 0.2.0", + "actix-codec", "actix-rt", "actix-service", - "actix-utils 1.0.6", + "actix-utils", "derive_more", "either", - "futures", + "futures-util", "http", "log", - "trust-dns-proto 0.18.0-alpha.2", - "trust-dns-resolver 0.18.0-alpha.2", + "trust-dns-proto", + "trust-dns-resolver", ] [[package]] name = "actix-files" -version = "0.2.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193b22cb1f7b4ff12a4eb2415d6d19e47e44ea93e05930b30d05375ea29d3529" +checksum = "5fc0a9181e93c91dc7eb401a0debaed5c8294e12019c307c72fd7a1731b672fc" dependencies = [ - "actix-http", "actix-service", "actix-web", "bitflags", @@ -98,26 +82,25 @@ dependencies = [ [[package]] name = "actix-http" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16664cc4fdea8030837ad5a845eb231fb93fc3c5c171edfefb52fad92ce9019" +checksum = "05dd80ba8f27c4a34357c07e338c8f5c38f8520e6d626ca1727d8fecc41b0cab" dependencies = [ - "actix-codec 0.2.0", + "actix-codec", "actix-connect", "actix-rt", "actix-service", "actix-threadpool", - "actix-utils 1.0.6", - "base64 0.11.0", + "actix-utils", + "base64", "bitflags", "brotli2", "bytes", - "chrono", + "cookie", "copyless", "derive_more", "either", "encoding_rs", - "failure", "flate2", "futures-channel", "futures-core", @@ -127,6 +110,7 @@ dependencies = [ "http", "httparse", "indexmap", + "itoa", "language-tags", "lazy_static", "log", @@ -138,9 +122,9 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sha1", + "sha-1", "slab", - "time 0.1.44", + "time 0.2.22", ] [[package]] @@ -187,10 +171,10 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e" dependencies = [ - "actix-codec 0.3.0", + "actix-codec", "actix-rt", "actix-service", - "actix-utils 2.0.0", + "actix-utils", "futures-channel", "futures-util", "log", @@ -212,6 +196,19 @@ dependencies = [ ] [[package]] +name = "actix-slog" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c697a62a2f51c5c26af6b1dded0622f15bec690da191615947e0c1b2b7b75198" +dependencies = [ + "actix-web", + "chrono", + "futures", + "pin-project", + "slog", +] + +[[package]] name = "actix-testing" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -236,42 +233,20 @@ dependencies = [ "lazy_static", "log", "num_cpus", - "parking_lot 0.11.0", + "parking_lot", "threadpool", ] [[package]] name = "actix-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e5b4faaf105e9a6d389c606c298dcdb033061b00d532af9df56ff3a54995a8" -dependencies = [ - "actix-codec 0.2.0", - "actix-rt", - "actix-service", - "actix-utils 1.0.6", - "derive_more", - "either", - "futures", - "log", -] - -[[package]] -name = "actix-utils" -version = "1.0.6" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf8f5631bf01adec2267808f00e228b761c60c0584cc9fa0b5364f41d147f4e" +checksum = "24789b7d7361cf5503a504ebe1c10806896f61e96eca9a7350e23001aca715fb" dependencies = [ - "actix-codec 0.2.0", - "actix-rt", + "actix-codec", "actix-service", - "bitflags", - "bytes", - "either", - "futures", - "log", - "pin-project", - "slab", + "actix-utils", + "futures-util", ] [[package]] @@ -280,7 +255,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a" dependencies = [ - "actix-codec 0.3.0", + "actix-codec", "actix-rt", "actix-service", "bitflags", @@ -296,11 +271,11 @@ dependencies = [ [[package]] name = "actix-web" -version = "2.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3158e822461040822f0dbf1735b9c2ce1f95f93b651d7a7aded00b1efbb1f635" +checksum = "c1b12fe25e11cd9ed2ef2e428427eb6178a1b363f3f7f0dab8278572f11b2da1" dependencies = [ - "actix-codec 0.2.0", + "actix-codec", "actix-http", "actix-macros", "actix-router", @@ -310,31 +285,34 @@ dependencies = [ "actix-testing", "actix-threadpool", "actix-tls", - "actix-utils 1.0.6", + "actix-utils", "actix-web-codegen", "awc", "bytes", "derive_more", "encoding_rs", - "futures", + "futures-channel", + "futures-core", + "futures-util", "fxhash", "log", "mime", - "net2", "pin-project", "regex", "serde", "serde_json", "serde_urlencoded", - "time 0.1.44", + "socket2", + "time 0.2.22", + "tinyvec 1.0.1", "url", ] [[package]] name = "actix-web-codegen" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a71bf475cbe07281d0b3696abb48212db118e7e23219f13596ce865235ff5766" +checksum = "750ca8fb60bbdc79491991650ba5d2ae7cd75f3fc00ead51390cfe9efda0d4d8" dependencies = [ "proc-macro2", "quote", @@ -410,9 +388,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +checksum = "b476ce7103678b0c6d3d395dbbae31d48ff910bd28be979ba5d48c6351131d0d" dependencies = [ "memchr", ] @@ -428,9 +406,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" +checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" [[package]] name = "arc-swap" @@ -471,9 +449,9 @@ dependencies = [ [[package]] name = "askama_actix" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddbc230d919b403e31373600425d49df06a19fb8af7a8964292ca7b5364db366" +checksum = "20653490b9baa13ed557b0ea6dae1fc9bbc2349ace67efeaaffbaea1c76f518a" dependencies = [ "actix-web", "askama", @@ -566,15 +544,15 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "awc" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7601d4d1d7ef2335d6597a41b5fe069f6ab799b85f53565ab390e7b7065aac5" +checksum = "150e00c06683ab44c5f97d033950e5d87a7a042d06d77f5eecb443cbd23d0575" dependencies = [ - "actix-codec 0.2.0", + "actix-codec", "actix-http", "actix-rt", "actix-service", - "base64 0.11.0", + "base64", "bytes", "derive_more", "futures-core", @@ -589,12 +567,12 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1931848a574faa8f7c71a12ea00453ff5effbb5f51afe7f77d7a48cace6ac1" +checksum = "707b586e0e2f247cbde68cdd2c3ce69ea7b7be43e1c5b426e37c9319c4b9838e" dependencies = [ "addr2line", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide", "object", @@ -602,16 +580,22 @@ dependencies = [ ] [[package]] -name = "base-x" -version = "0.2.6" +name = "barrage" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1" +checksum = "756c265ddc2c445724b688455c42ec724590635fa71b47eaff7b260dc95fa7c9" +dependencies = [ + "concurrent-queue", + "event-listener", + "loom", + "spinny", +] [[package]] -name = "base64" -version = "0.11.0" +name = "base-x" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1" [[package]] name = "base64" @@ -643,6 +627,15 @@ dependencies = [ ] [[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] name = "block-cipher" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -684,16 +677,19 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.4.0" +name = "buf-min" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +checksum = "b6ae7069aad07c7cdefe6a22a671f00650728bd2331a4cc62e1e5d0becdf9ca4" +dependencies = [ + "bytes", +] [[package]] -name = "byte-slice-cast" -version = "0.3.5" +name = "bumpalo" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0a5e3906bcbf133e33c1d4d95afc664ad37fbdb9f6568d8043e7ea8c27d93d3" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" [[package]] name = "byteorder" @@ -717,10 +713,25 @@ dependencies = [ ] [[package]] +name = "cache-padded" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" + +[[package]] +name = "catty" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c8b5dc109078e97a1303c505ce1eeb4f8c4c5459d462040d143520828e80c12" +dependencies = [ + "spinning_top", +] + +[[package]] name = "cc" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c" +checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d" [[package]] name = "cfg-if" @@ -729,6 +740,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[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.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -758,15 +775,6 @@ dependencies = [ [[package]] name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags", -] - -[[package]] -name = "cloudabi" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" @@ -785,6 +793,15 @@ dependencies = [ ] [[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] name = "const_fn" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -797,6 +814,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] +name = "cookie" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1373a16a4937bc34efec7b391f9c1500c30b8478a701a4f44c9165cc0475a6e0" +dependencies = [ + "percent-encoding", + "time 0.2.22", + "version_check 0.9.2", +] + +[[package]] name = "copyless" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -819,12 +847,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" [[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] name = "crc32fast" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", +] + +[[package]] +name = "crossbeam" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", ] [[package]] @@ -838,13 +886,50 @@ dependencies = [ ] [[package]] +name = "crossbeam-deque" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "crossbeam-utils", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] name = "crossbeam-utils" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ "autocfg", - "cfg-if", + "cfg-if 0.1.10", "lazy_static", ] @@ -938,7 +1023,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "dirs-sys", ] @@ -989,7 +1074,7 @@ version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a51b8cf747471cb9499b6d59e59b0444f4c90eba8968c4e44874e92b5b64ace2" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", ] [[package]] @@ -1005,26 +1090,10 @@ dependencies = [ ] [[package]] -name = "failure" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" +name = "event-listener" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] +checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" [[package]] name = "flakebi-ring" @@ -1047,13 +1116,23 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da80be589a72651dcda34d8b35bcdc9b7254ad06325611074d9cc0fbb19f60ee" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "crc32fast", "libc", "miniz_oxide", ] [[package]] +name = "flume" +version = "0.9.1" +source = "git+https://github.com/zesterer/flume#9c907f78e0aa8bce39478dd16d49cf30ae7f3f47" +dependencies = [ + "futures-core", + "futures-sink", + "spinning_top", +] + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1092,9 +1171,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +checksum = "5d8e3078b7b2a8a671cb7a3d17b4760e4181ea243227776ba83fd043b4ca034e" dependencies = [ "futures-channel", "futures-core", @@ -1107,9 +1186,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +checksum = "a7a4d35f7401e948629c9c3d6638fb9bf94e0b2121e96c3b428cc4e631f3eb74" dependencies = [ "futures-core", "futures-sink", @@ -1117,15 +1196,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" +checksum = "d674eaa0056896d5ada519900dbf97ead2e46a7b6621e8160d79e2f2e1e2784b" [[package]] name = "futures-executor" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +checksum = "cc709ca1da6f66143b8c9bec8e6260181869893714e9b5a490b169b0414144ab" dependencies = [ "futures-core", "futures-task", @@ -1134,15 +1213,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" +checksum = "5fc94b64bb39543b4e432f1790b6bf18e3ee3b74653c5449f63310e9a74b123c" [[package]] name = "futures-macro" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +checksum = "f57ed14da4603b2554682e9f2ff3c65d7567b53188db96cb71538217fc64581b" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -1152,24 +1231,30 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" +checksum = "0d8764258ed64ebc5d9ed185cf86a95db5cac810269c5d20ececb32e0088abbd" [[package]] name = "futures-task" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +checksum = "4dd26820a9f3637f1302da8bceba3ff33adbe53464b54ca24d4e2d4f1db30f94" dependencies = [ "once_cell", ] [[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] name = "futures-util" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +checksum = "8a894a0acddba51a2d49a6f4263b1e64b8c579ece8af50fa86503d52cd1eea34" dependencies = [ "futures-channel", "futures-core", @@ -1195,6 +1280,19 @@ dependencies = [ ] [[package]] +name = "generator" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e13c8f4607ff74f6d0fa37007cb95492531333f46bb9744f772d9e7830855c" +dependencies = [ + "cc", + "libc", + "log", + "rustc_version", + "winapi 0.3.9", +] + +[[package]] name = "generic-array" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1210,7 +1308,7 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -1301,12 +1399,12 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566e1062d60c9df234da5051c970426d8351c8f3afc909f632fad995df1af956" +checksum = "da0bc44d8c3ce6c3dca434a3f256a851c55f0ea5c409413b5b406039989db6ff" dependencies = [ "bitflags", - "cfg-if", + "cfg-if 1.0.0", "futures-channel", "futures-core", "futures-util", @@ -1358,9 +1456,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0463453d581b3642f89e9afcc807851ea6e2a94e2df06ae977ebc7e11b0a18c" +checksum = "54e05de1343acac0ca8236dc92a9ad06d054aa87616e7b9eb044b5b5f8f255e3" dependencies = [ "array-init", "bitflags", @@ -1446,7 +1544,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.3.1", + "tokio-util", "tracing", ] @@ -1467,9 +1565,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" dependencies = [ "libc", ] @@ -1603,7 +1701,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63312a18f7ea8760cdd0a7c5aac1a619752a246b833545e3e36d1f81f7cd9e66" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", ] [[package]] @@ -1687,16 +1785,16 @@ checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" dependencies = [ "arrayvec", "bitflags", - "cfg-if", + "cfg-if 0.1.10", "ryu", "static_assertions", ] [[package]] name = "libc" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98" +checksum = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743" [[package]] name = "linked-hash-map" @@ -1706,15 +1804,6 @@ checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "lock_api" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "lock_api" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" @@ -1728,7 +1817,7 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "serde", ] @@ -1752,7 +1841,7 @@ dependencies = [ "libc", "log", "log-mdc", - "parking_lot 0.11.0", + "parking_lot", "serde", "serde-value", "serde_derive", @@ -1764,6 +1853,18 @@ dependencies = [ ] [[package]] +name = "loom" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0e8460f2f2121162705187214720353c517b97bdfb3494c0b1e33d83ebe4bed" +dependencies = [ + "cfg-if 0.1.10", + "futures-util", + "generator", + "scoped-tls", +] + +[[package]] name = "lru-cache" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1797,6 +1898,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg", +] + +[[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1814,9 +1924,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c60c0dfe32c10b43a144bad8fc83538c52f58302c92300ea7ec7bf7b38d5a7b9" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" dependencies = [ "adler", "autocfg", @@ -1828,7 +1938,7 @@ version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "fuchsia-zircon", "fuchsia-zircon-sys", "iovec", @@ -1916,7 +2026,7 @@ version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ebc3ec692ed7c9a255596c67808dee269f64655d8baf7b4f0638e51ba1d6853" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "libc", "winapi 0.3.9", ] @@ -2017,9 +2127,9 @@ dependencies = [ [[package]] name = "object" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" +checksum = "37fd5004feb2ce328a52b0b3d01dbf4ffff72583493900ed15f22d4111c51693" [[package]] name = "omnom" @@ -2049,7 +2159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" dependencies = [ "bitflags", - "cfg-if", + "cfg-if 0.1.10", "foreign-types", "lazy_static", "libc", @@ -2086,37 +2196,13 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" -dependencies = [ - "lock_api 0.3.4", - "parking_lot_core 0.7.2", -] - -[[package]] -name = "parking_lot" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" dependencies = [ "instant", - "lock_api 0.4.1", - "parking_lot_core 0.8.0", -] - -[[package]] -name = "parking_lot_core" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" -dependencies = [ - "cfg-if", - "cloudabi 0.0.3", - "libc", - "redox_syscall", - "smallvec", - "winapi 0.3.9", + "lock_api", + "parking_lot_core", ] [[package]] @@ -2125,8 +2211,8 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" dependencies = [ - "cfg-if", - "cloudabi 0.1.0", + "cfg-if 0.1.10", + "cloudabi", "instant", "libc", "redox_syscall", @@ -2148,18 +2234,18 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b9e280448854bd91559252582173b3bd1f8e094a0e644791c0628ca9b1f144f" +checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8c8b352676bc6a4c3d71970560b913cea444a7a921cc2e2d920225e4b91edaa" +checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" dependencies = [ "proc-macro2", "quote", @@ -2180,9 +2266,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] name = "pokebot" @@ -2191,10 +2277,11 @@ dependencies = [ "actix", "actix-files", "actix-rt", + "actix-slog", "actix-web", "askama", "askama_actix", - "byte-slice-cast", + "async-trait", "derive_more", "futures", "glib", @@ -2207,14 +2294,25 @@ dependencies = [ "rand", "serde", "serde_json", + "slog", + "slog-async", + "slog-scope", + "slog-stdlog", "structopt", "tokio", "toml", "tsclientlib", "tsproto-packets", + "xtra", ] [[package]] +name = "pollster" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9824e18e85003f0b5a38fa1932ae8be8c2aac9447c2f28ab6f9704dbe0a1ab58" + +[[package]] name = "ppv-lite86" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2376,9 +2474,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.3.9" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +checksum = "8963b85b8ce3074fecffde43b4b0dded83ce2f367dc8d363afc56679f3ee820b" dependencies = [ "aho-corasick", "memchr", @@ -2397,9 +2495,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" +checksum = "8cab7a364d15cde1e505267766a2d3c4e22a843e1a601f0fa7564c0f82ced11c" [[package]] name = "remove_dir_all" @@ -2437,7 +2535,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9eaa17ac5d7b838b7503d118fa16ad88f440498bf9ffe5424e621f93190d61e" dependencies = [ - "base64 0.12.3", + "base64", "bytes", "encoding_rs", "futures-core", @@ -2482,7 +2580,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" dependencies = [ - "base64 0.12.3", + "base64", "blake2b_simd", "constant_time_eq", "crossbeam-utils", @@ -2490,9 +2588,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" +checksum = "b2610b7f643d18c87dff3b489950269617e6601a51f1f05aa5daefee36f64f0b" [[package]] name = "rustc_version" @@ -2520,6 +2618,12 @@ dependencies = [ ] [[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2595,9 +2699,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a230ea9107ca2220eea9d46de97eddcb04cd00e92d13dda78e478dd33fa82bd4" +checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" dependencies = [ "itoa", "ryu", @@ -2629,6 +2733,19 @@ dependencies = [ ] [[package]] +name = "sha-1" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a36ea86c864a3f16dd2687712dd6646f7019f301e57537c7f4dc9f5916770" +dependencies = [ + "block-buffer", + "cfg-if 0.1.10", + "cpuid-bool", + "digest", + "opaque-debug", +] + +[[package]] name = "sha1" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2689,6 +2806,29 @@ dependencies = [ ] [[package]] +name = "slog-scope" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c44c89dd8b0ae4537d1ae318353eaf7840b4869c536e31c41e963d1ea523ee6" +dependencies = [ + "arc-swap", + "lazy_static", + "slog", +] + +[[package]] +name = "slog-stdlog" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d87903baf655da2d82bc3ac3f7ef43868c58bf712b3a661fda72009304c23" +dependencies = [ + "crossbeam", + "log", + "slog", + "slog-scope", +] + +[[package]] name = "slog-term" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2713,7 +2853,7 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "libc", "redox_syscall", "winapi 0.3.9", @@ -2726,6 +2866,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] +name = "spinning_top" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e529d73e80d64b5f2631f9035113347c578a1c9c7774b83a2b880788459ab36" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spinny" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351aa083e2a6d649dd3dcac94e6b6c2db4141fba9b457abbbdfb4254fa3a754" +dependencies = [ + "lock_api", + "loom", + "once_cell", +] + +[[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2733,9 +2893,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "standback" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a71ea1ea5f8747d1af1979bfb7e65c3a025a70609f04ceb78425bc5adad8e6" +checksum = "f4e0831040d2cf2bdfd51b844be71885783d489898a192f254ae25d57cce725c" dependencies = [ "version_check 0.9.2", ] @@ -2813,9 +2973,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.18" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33f6461027d7f08a13715659b2948e1602c31a3756aeae9378bfe7518c72e82" +checksum = "126d630294ec449fae0b16f964e35bf3c74f940da9dca17ee9b905f7b3112eb8" dependencies = [ "clap", "lazy_static", @@ -2824,9 +2984,9 @@ dependencies = [ [[package]] name = "structopt-derive" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92e775028122a4b3dd55d58f14fc5120289c69bee99df1d117ae30f84b225c9" +checksum = "65e51c492f9e23a220534971ff5afc14037289de430e3c83f9daf6a1b6ae91e8" dependencies = [ "heck", "proc-macro-error", @@ -2861,24 +3021,12 @@ checksum = "343f3f510c2915908f155e94f17220b19ccfacf2a64a2a5d8004f2c3e311e7fd" [[package]] name = "syn" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "synstructure" -version = "0.12.4" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +checksum = "e03e57e4fcbfe7749842d53e24ccb9aa12b7252dbe5e91d2acad31834c8b8fdd" dependencies = [ "proc-macro2", "quote", - "syn", "unicode-xid", ] @@ -2932,7 +3080,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "libc", "rand", "redox_syscall", @@ -2961,18 +3109,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +checksum = "318234ffa22e0920fe9a40d7b8369b5f649d490980cf7aadcf1eb91594869b42" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +checksum = "cae2447b6282786c3493999f40a9be2a6ad20cb8bd268b0a0dbf5a065535c0ab" dependencies = [ "proc-macro2", "quote", @@ -3065,6 +3213,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117" [[package]] +name = "tinyvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b78a366903f506d2ad52ca8dc552102ffdd3e937ba8a227f024dc1d1eae28575" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] name = "tokio" version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3080,6 +3243,7 @@ dependencies = [ "mio", "mio-named-pipes", "mio-uds", + "num_cpus", "pin-project-lite", "signal-hook-registry", "slab", @@ -3110,26 +3274,13 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "log", "pin-project-lite", @@ -3138,9 +3289,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" dependencies = [ "serde", ] @@ -3157,7 +3308,7 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "log", "pin-project-lite", "tracing-core", @@ -3180,26 +3331,6 @@ checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" [[package]] name = "trust-dns-proto" -version = "0.18.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a7f3a2ab8a919f5eca52a468866a67ed7d3efa265d48a652a9a3452272b413f" -dependencies = [ - "async-trait", - "enum-as-inner", - "failure", - "futures", - "idna", - "lazy_static", - "log", - "rand", - "smallvec", - "socket2", - "tokio", - "url", -] - -[[package]] -name = "trust-dns-proto" version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdd7061ba6f4d4d9721afedffbfd403f20f39a4301fee1b70d6fcd09cca69f28" @@ -3220,31 +3351,12 @@ dependencies = [ [[package]] name = "trust-dns-resolver" -version = "0.18.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f" -dependencies = [ - "cfg-if", - "failure", - "futures", - "ipconfig", - "lazy_static", - "log", - "lru-cache", - "resolv-conf", - "smallvec", - "tokio", - "trust-dns-proto 0.18.0-alpha.2", -] - -[[package]] -name = "trust-dns-resolver" version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f23cdfdc3d8300b3c50c9e84302d3bd6d860fb9529af84ace6cf9665f181b77" dependencies = [ "backtrace", - "cfg-if", + "cfg-if 0.1.10", "futures", "ipconfig", "lazy_static", @@ -3254,7 +3366,7 @@ dependencies = [ "smallvec", "thiserror", "tokio", - "trust-dns-proto 0.19.5", + "trust-dns-proto", ] [[package]] @@ -3266,9 +3378,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "ts-bookkeeping" version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib#aba8899a418146cf91a35c20c6c107c42231d09a" +source = "git+https://github.com/ReSpeak/tsclientlib#5b6da0feb88a07237448afe2f34d38cbe06f3249" dependencies = [ - "base64 0.12.3", + "base64", "derive_more", "heck", "itertools", @@ -3287,10 +3399,10 @@ dependencies = [ [[package]] name = "tsclientlib" version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib#aba8899a418146cf91a35c20c6c107c42231d09a" +source = "git+https://github.com/ReSpeak/tsclientlib#5b6da0feb88a07237448afe2f34d38cbe06f3249" dependencies = [ "audiopus", - "base64 0.12.3", + "base64", "futures", "git-testament", "itertools", @@ -3305,8 +3417,8 @@ dependencies = [ "thiserror", "time 0.2.22", "tokio", - "trust-dns-proto 0.19.5", - "trust-dns-resolver 0.19.5", + "trust-dns-proto", + "trust-dns-resolver", "ts-bookkeeping", "tsproto", "tsproto-packets", @@ -3317,10 +3429,10 @@ dependencies = [ [[package]] name = "tsproto" version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib#aba8899a418146cf91a35c20c6c107c42231d09a" +source = "git+https://github.com/ReSpeak/tsclientlib#5b6da0feb88a07237448afe2f34d38cbe06f3249" dependencies = [ "aes", - "base64 0.12.3", + "base64", "bitflags", "curve25519-dalek", "eax", @@ -3350,10 +3462,10 @@ dependencies = [ [[package]] name = "tsproto-packets" version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib#aba8899a418146cf91a35c20c6c107c42231d09a" +source = "git+https://github.com/ReSpeak/tsclientlib#5b6da0feb88a07237448afe2f34d38cbe06f3249" dependencies = [ "arrayref", - "base64 0.12.3", + "base64", "bitflags", "num-derive", "num-traits", @@ -3366,9 +3478,9 @@ dependencies = [ [[package]] name = "tsproto-structs" version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib#aba8899a418146cf91a35c20c6c107c42231d09a" +source = "git+https://github.com/ReSpeak/tsclientlib#5b6da0feb88a07237448afe2f34d38cbe06f3249" dependencies = [ - "base64 0.12.3", + "base64", "csv", "heck", "lazy_static", @@ -3380,10 +3492,10 @@ dependencies = [ [[package]] name = "tsproto-types" version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib#aba8899a418146cf91a35c20c6c107c42231d09a" +source = "git+https://github.com/ReSpeak/tsclientlib#5b6da0feb88a07237448afe2f34d38cbe06f3249" dependencies = [ "arrayref", - "base64 0.12.3", + "base64", "bitflags", "curve25519-dalek", "flakebi-ring", @@ -3441,7 +3553,7 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" dependencies = [ - "tinyvec", + "tinyvec 0.3.4", ] [[package]] @@ -3490,18 +3602,19 @@ dependencies = [ [[package]] name = "v_escape" -version = "0.7.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6" +checksum = "039a44473286eb84e4e74f90165feff67c802dbeced7ee4c5b00d719b0d0475e" dependencies = [ + "buf-min", "v_escape_derive", ] [[package]] name = "v_escape_derive" -version = "0.5.6" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae" +checksum = "c860ad1273f4eee7006cee05db20c9e60e5d24cba024a32e1094aa8e574f3668" dependencies = [ "nom 4.2.3", "proc-macro2", @@ -3511,11 +3624,11 @@ dependencies = [ [[package]] name = "v_htmlescape" -version = "0.4.5" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41" +checksum = "11d7c2a33ed7cf0dc1b42bcf39e01b6512f9df08f09e1cd8a49d9dc49a6a9482" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "v_escape", ] @@ -3577,7 +3690,7 @@ version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "serde", "serde_json", "wasm-bindgen-macro", @@ -3604,7 +3717,7 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "js-sys", "wasm-bindgen", "web-sys", @@ -3651,9 +3764,9 @@ dependencies = [ [[package]] name = "widestring" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a763e303c0e0f23b0da40888724762e802a8ffefbc22de4127ef42493c2ea68c" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" [[package]] name = "winapi" @@ -3718,6 +3831,23 @@ dependencies = [ ] [[package]] +name = "xtra" +version = "0.5.0-beta.6" +source = "git+https://github.com/Restioson/xtra#540f5c136b9a83ea2238968c81ba057b6c948c6b" +dependencies = [ + "async-trait", + "barrage", + "catty", + "flume", + "futures-core", + "futures-sink", + "futures-timer", + "futures-util", + "pollster", + "tokio", +] + +[[package]] name = "yaml-rust" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5,32 +5,41 @@ authors = ["Jokler <jokler@protonmail.com>"] edition = "2018" license = "GPL-3.0-or-later" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib", features = ["unstable"] } tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib" } -log = "0.4.11" -log4rs = "0.13.0" -toml = "0.5.6" -structopt = "0.3.16" +toml = "0.5.7" +structopt = "0.3.20" humantime = "2.0.1" -tokio = { version = "0.2.22", features = ["tcp", "io-util", "sync", "process"] } -futures = "0.3.5" +slog = "2.5.2" +slog-async = "2.5.0" +slog-scope = "4.3.0" +slog-stdlog = "4.0.0" + +log = "0.4.11" +log4rs = { version = "0.13.0" } + +tokio = { version = "0.2.22", features = ["rt-threaded", "process", "blocking", "io-std"] } +futures = "0.3.6" +# git version for async Actor trait +xtra = { git = "https://github.com/Restioson/xtra", features = ["with-tokio-0_2"] } +async-trait = "0.1.41" -glib = "0.10.1" -gstreamer = "0.16.2" -gstreamer-app = "0.16.0" -gstreamer-audio = "0.16.2" -byte-slice-cast = "0.3.5" -serde_json = "1.0.57" -serde = "1.0.114" -actix = "0.9.0" +glib = "0.10.2" +gstreamer = "0.16.4" +gstreamer-app = "0.16.3" +gstreamer-audio = "0.16.4" + +serde = "1.0.116" +serde_json = "1.0.59" +rand = { version = "0.7.3", features = ["small_rng"] } +derive_more = "0.99.11" + +actix = "0.10.0" actix-rt = "1.1.1" -actix-web = "2.0.0" -actix-files = "0.2.2" +actix-web = "3.1.0" +actix-files = "0.4.0" +actix-slog = "0.2.1" +askama_actix = "0.11.1" askama = "0.10.3" -rand = { version = "0.7.3", features = ["small_rng"] } -derive_more = "0.99.9" -askama_actix = "0.10.0" diff --git a/src/audio_player.rs b/src/audio_player.rs index 1f6649f..23581f9 100644 --- a/src/audio_player.rs +++ b/src/audio_player.rs @@ -7,36 +7,31 @@ use gstreamer as gst; use gstreamer_app::{AppSink, AppSinkCallbacks}; use gstreamer_audio::{StreamVolume, StreamVolumeFormat}; -use crate::bot::{MusicBotMessage, State}; use glib::BoolError; -use log::{debug, error, info, warn}; -use std::sync::{Arc, RwLock}; -use tokio::sync::mpsc::UnboundedSender; +use slog::{debug, error, info, warn, Logger}; +use xtra::WeakAddress; +use crate::bot::{MusicBot, MusicBotMessage, State}; use crate::command::{Seek, VolumeChange}; use crate::youtube_dl::AudioMetadata; static GST_INIT: Once = Once::new(); -#[derive(PartialEq, Eq, Debug, Clone, Copy)] -pub enum PollResult { - Continue, - Quit, -} - pub struct AudioPlayer { pipeline: gst::Pipeline, bus: gst::Bus, http_src: gst::Element, - volume_f64: RwLock<f64>, + volume_f64: f64, volume: gst::Element, - sender: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, - currently_playing: RwLock<Option<AudioMetadata>>, + currently_playing: Option<AudioMetadata>, + + logger: Logger, } fn make_element(factoryname: &str, display_name: &str) -> Result<gst::Element, AudioPlayerError> { - Ok(gst::ElementFactory::make(factoryname, Some(display_name))?) + Ok(gst::ElementFactory::make(factoryname, Some(display_name)) + .map_err(|_| AudioPlayerError::MissingPlugin(factoryname.to_string()))?) } fn link_elements(a: &gst::Element, b: &gst::Element) -> Result<(), AudioPlayerError> { @@ -49,11 +44,12 @@ fn add_decode_bin_new_pad_callback( decode_bin: &gst::Element, audio_bin: gst::Bin, ghost_pad: gst::GhostPad, + logger: Logger, ) { decode_bin.connect_pad_added(move |_, new_pad| { - debug!("New pad received on decode bin"); + debug!(logger, "New pad received on decode bin"); let name = if let Some(caps) = new_pad.get_current_caps() { - debug!("Pad caps: {}", caps.to_string()); + debug!(logger, "Found caps"; "caps" => caps.to_string()); if let Some(structure) = caps.get_structure(0) { Some(structure.get_name().to_string()) } else { @@ -68,7 +64,7 @@ fn add_decode_bin_new_pad_callback( peer.unlink(&ghost_pad).unwrap(); } - info!("Found raw audio, linking audio bin"); + info!(logger, "Found raw audio, linking audio bin"); new_pad.link(&ghost_pad).unwrap(); audio_bin.sync_state_with_parent().unwrap(); @@ -77,27 +73,15 @@ fn add_decode_bin_new_pad_callback( } impl AudioPlayer { - pub fn new( - sender: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, - callback: Option<Box<dyn FnMut(&[u8]) + Send>>, - ) -> Result<Self, AudioPlayerError> { + pub fn new(logger: Logger) -> Result<Self, AudioPlayerError> { GST_INIT.call_once(|| gst::init().unwrap()); - info!("Creating audio player"); + info!(logger, "Creating audio player"); let pipeline = gst::Pipeline::new(Some("TeamSpeak Audio Player")); let bus = pipeline.get_bus().unwrap(); let http_src = make_element("souphttpsrc", "http source")?; - let decode_bin = make_element("decodebin", "decode bin")?; - pipeline.add_many(&[&http_src, &decode_bin])?; - - link_elements(&http_src, &decode_bin)?; - - let (audio_bin, volume, ghost_pad) = Self::create_audio_bin(callback)?; - - add_decode_bin_new_pad_callback(&decode_bin, audio_bin.clone(), ghost_pad); - - pipeline.add(&audio_bin)?; + let volume = make_element("volume", "volume")?; // The documentation says that we have to make sure to handle // all messages if auto flushing is deactivated. @@ -111,26 +95,31 @@ impl AudioPlayer { pipeline, bus, http_src, + logger, - volume_f64: RwLock::new(0.0), + volume_f64: 0.0, volume, - sender, - currently_playing: RwLock::new(None), + currently_playing: None, }) } - fn create_audio_bin( + pub fn setup_with_audio_callback( + &self, callback: Option<Box<dyn FnMut(&[u8]) + Send>>, - ) -> Result<(gst::Bin, gst::Element, gst::GhostPad), AudioPlayerError> { + ) -> Result<(), AudioPlayerError> { + let decode_bin = make_element("decodebin", "decode bin")?; + self.pipeline.add_many(&[&self.http_src, &decode_bin])?; + + link_elements(&self.http_src, &decode_bin)?; + let audio_bin = gst::Bin::new(Some("audio bin")); let queue = make_element("queue", "audio queue")?; let convert = make_element("audioconvert", "audio converter")?; - let volume = make_element("volume", "volume")?; let resample = make_element("audioresample", "audio resampler")?; let pads = queue.get_sink_pads(); let queue_sink_pad = pads.first().unwrap(); - audio_bin.add_many(&[&queue, &convert, &volume, &resample])?; + audio_bin.add_many(&[&queue, &convert, &self.volume, &resample])?; if let Some(mut callback) = callback { let opus_enc = make_element("opusenc", "opus encoder")?; @@ -160,49 +149,64 @@ impl AudioPlayer { audio_bin.add_many(&[&opus_enc, &sink])?; - gst::Element::link_many(&[&queue, &convert, &volume, &resample, &opus_enc, &sink])?; + gst::Element::link_many(&[ + &queue, + &convert, + &self.volume, + &resample, + &opus_enc, + &sink, + ])?; } else { let sink = make_element("autoaudiosink", "auto audio sink")?; audio_bin.add_many(&[&sink])?; - gst::Element::link_many(&[&queue, &convert, &volume, &resample, &sink])?; + gst::Element::link_many(&[&queue, &convert, &self.volume, &resample, &sink])?; }; let ghost_pad = GhostPad::with_target(Some("audio bin sink"), queue_sink_pad).unwrap(); ghost_pad.set_active(true)?; audio_bin.add_pad(&ghost_pad)?; - Ok((audio_bin, volume, ghost_pad)) + add_decode_bin_new_pad_callback( + &decode_bin, + audio_bin.clone(), + ghost_pad, + self.logger.clone(), + ); + + self.pipeline.add(&audio_bin)?; + + Ok(()) } - pub fn set_metadata(&self, data: AudioMetadata) -> Result<(), AudioPlayerError> { + pub fn set_metadata(&mut self, data: AudioMetadata) -> Result<(), AudioPlayerError> { self.set_source_url(data.url.clone())?; - let mut currently_playing = self.currently_playing.write().unwrap(); - *currently_playing = Some(data); + self.currently_playing = Some(data); Ok(()) } fn set_source_url(&self, location: String) -> Result<(), AudioPlayerError> { - info!("Setting location URI: {}", location); + info!(self.logger, "Setting source"; "url" => &location); self.http_src.set_property("location", &location)?; Ok(()) } - pub fn change_volume(&self, volume: VolumeChange) -> Result<(), AudioPlayerError> { + pub fn change_volume(&mut self, volume: VolumeChange) -> Result<(), AudioPlayerError> { let new_volume = match volume { - VolumeChange::Positive(vol) => self.volume() + vol, - VolumeChange::Negative(vol) => self.volume() - vol, + VolumeChange::Positive(vol) => self.volume_f64 + vol, + VolumeChange::Negative(vol) => self.volume_f64 - vol, VolumeChange::Absolute(vol) => vol, }; let new_volume = new_volume.max(0.0).min(1.0); - *self.volume_f64.write().unwrap() = new_volume; + self.volume_f64 = new_volume; let db = 50.0 * new_volume.log10(); - info!("Setting volume: {} -> {} dB", new_volume, db); + info!(self.logger, "Setting volume"; "volume" => new_volume, "db" => db); let linear = StreamVolume::convert_volume(StreamVolumeFormat::Db, StreamVolumeFormat::Linear, db); @@ -212,36 +216,10 @@ impl AudioPlayer { Ok(()) } - pub fn is_started(&self) -> bool { - let (_, current, pending) = self.pipeline.get_state(gst::ClockTime(None)); - - match (current, pending) { - (gst::State::Null, gst::State::VoidPending) => false, - (_, gst::State::Null) => false, - (gst::State::Ready, gst::State::VoidPending) => false, - _ => true, - } - } - - pub fn volume(&self) -> f64 { - *self.volume_f64.read().unwrap() - } - - pub fn position(&self) -> Option<Duration> { - self.pipeline - .query_position::<gst::ClockTime>() - .and_then(|t| t.0.map(Duration::from_nanos)) - } - - pub fn currently_playing(&self) -> Option<AudioMetadata> { - self.currently_playing.read().unwrap().clone() - } + pub fn reset(&mut self) -> Result<(), AudioPlayerError> { + info!(self.logger, "Setting pipeline state"; "to" => "null"); - pub fn reset(&self) -> Result<(), AudioPlayerError> { - info!("Setting pipeline state to null"); - - let mut currently_playing = self.currently_playing.write().unwrap(); - *currently_playing = None; + self.currently_playing = None; self.pipeline.set_state(gst::State::Null)?; @@ -249,7 +227,7 @@ impl AudioPlayer { } pub fn play(&self) -> Result<(), AudioPlayerError> { - info!("Setting pipeline state to playing"); + info!(self.logger, "Setting pipeline state"; "to" => "playing"); self.pipeline.set_state(gst::State::Playing)?; @@ -257,7 +235,7 @@ impl AudioPlayer { } pub fn pause(&self) -> Result<(), AudioPlayerError> { - info!("Setting pipeline state to paused"); + info!(self.logger, "Setting pipeline state"; "to" => "paused"); self.pipeline.set_state(gst::State::Paused)?; @@ -289,7 +267,7 @@ impl AudioPlayer { }; let time = humantime::format_duration(absolute); - info!("Seeking to {}", time); + info!(self.logger, "Seeking"; "time" => %time); self.pipeline.seek_simple( gst::SeekFlags::FLUSH, @@ -300,121 +278,125 @@ impl AudioPlayer { } pub fn stop_current(&self) -> Result<(), AudioPlayerError> { - info!("Stopping pipeline, sending EOS"); + info!(self.logger, "Stopping pipeline, sending EOS"); self.bus.post(&gst::message::Eos::new())?; Ok(()) } - pub fn quit(&self, reason: String) { - info!("Quitting audio player"); - - if self - .bus - .post(&gst::message::Application::new(gst::Structure::new_empty( - "quit", - ))) - .is_err() - { - warn!("Tried to send \"quit\" app event on flushing bus."); + pub fn is_started(&self) -> bool { + let (_, current, pending) = self.pipeline.get_state(gst::ClockTime(None)); + + match (current, pending) { + (gst::State::Null, gst::State::VoidPending) => false, + (_, gst::State::Null) => false, + (gst::State::Ready, gst::State::VoidPending) => false, + _ => true, } + } - let sender = self.sender.read().unwrap(); - sender.send(MusicBotMessage::Quit(reason)).unwrap(); + pub fn volume(&self) -> f64 { + self.volume_f64 } - fn send_state(&self, state: State) { - info!("Sending state {:?} to application", state); - let sender = self.sender.read().unwrap(); - sender.send(MusicBotMessage::StateChange(state)).unwrap(); + pub fn position(&self) -> Option<Duration> { + self.pipeline + .query_position::<gst::ClockTime>() + .and_then(|t| t.0.map(Duration::from_nanos)) } - pub fn poll(&self) -> PollResult { - debug!("Polling GStreamer"); - 'outer: loop { - while let Some(msg) = self.bus.timed_pop(gst::ClockTime(None)) { - use gst::MessageView; - - match msg.view() { - MessageView::StateChanged(state) => { - if let Some(src) = state.get_src() { - if src.get_name() != self.pipeline.get_name() { - continue; - } - } + pub fn currently_playing(&self) -> Option<AudioMetadata> { + self.currently_playing.clone() + } + + pub fn register_bot(&self, bot: WeakAddress<MusicBot>) { + let pipeline_name = self.pipeline.get_name(); + debug!(self.logger, "Setting sync handler on gstreamer bus"); - let old = state.get_old(); - let current = state.get_current(); - let pending = state.get_pending(); - - match (old, current, pending) { - (gst::State::Paused, gst::State::Playing, gst::State::VoidPending) => { - self.send_state(State::Playing) - } - (gst::State::Playing, gst::State::Paused, gst::State::VoidPending) => { - self.send_state(State::Paused) - } - (_, gst::State::Ready, gst::State::Null) => { - self.send_state(State::Stopped) - } - (_, gst::State::Null, gst::State::VoidPending) => { - self.send_state(State::Stopped) - } - _ => { - debug!( - "Pipeline transitioned from {:?} to {:?}, with {:?} pending", - old, current, pending - ); - } + let logger = self.logger.clone(); + let handle = tokio::runtime::Handle::current(); + self.bus.set_sync_handler(move |_, msg| { + use gst::MessageView; + + match msg.view() { + MessageView::StateChanged(state) => { + if let Some(src) = state.get_src() { + if src.get_name() != pipeline_name { + return gst::BusSyncReply::Drop; } } - MessageView::Eos(..) => { - info!("End of stream reached"); - self.reset().unwrap(); - break 'outer; - } - MessageView::Warning(warn) => { - warn!( - "Warning from {:?}: {} ({:?})", - warn.get_src().map(|s| s.get_path_string()), - warn.get_error(), - warn.get_debug() - ); - break 'outer; - } - MessageView::Error(err) => { - error!( - "Error from {:?}: {} ({:?})", - err.get_src().map(|s| s.get_path_string()), - err.get_error(), - err.get_debug() - ); - break 'outer; - } - MessageView::Application(content) => { - if let Some(s) = content.get_structure() { - if s.get_name() == "quit" { - self.reset().unwrap(); - return PollResult::Quit; - } + let old = state.get_old(); + let current = state.get_current(); + let pending = state.get_pending(); + + match (old, current, pending) { + (gst::State::Paused, gst::State::Playing, gst::State::VoidPending) => { + send_state(&handle, &bot, State::Playing); + } + (gst::State::Playing, gst::State::Paused, gst::State::VoidPending) => { + send_state(&handle, &bot, State::Paused); + } + (_, gst::State::Ready, gst::State::Null) => { + send_state(&handle, &bot, State::Stopped); + } + (_, gst::State::Null, gst::State::VoidPending) => { + send_state(&handle, &bot, State::Stopped); + } + _ => { + debug!( + logger, + "Pipeline transitioned"; + "from" => ?old, + "to" => ?current, + "pending" => ?pending + ); } } - _ => { - //debug!("Unhandled message on bus: {:?}", msg) - } - }; + } + MessageView::Eos(..) => { + info!(logger, "End of stream reached"); + + send_state(&handle, &bot, State::EndOfStream); + } + MessageView::Warning(warn) => { + warn!( + logger, + "Received warning from bus"; + "source" => ?warn.get_src().map(|s| s.get_path_string()), + "error" => %warn.get_error(), + "debug" => ?warn.get_debug() + ); + } + MessageView::Error(err) => { + error!( + logger, + "Received error from bus"; + "source" => ?err.get_src().map(|s| s.get_path_string()), + "error" => %err.get_error(), + "debug" => ?err.get_debug() + ); + + send_state(&handle, &bot, State::EndOfStream); + } + _ => { + //debug!("Unhandled message on bus: {:?}", msg) + } } - } - debug!("Left GStreamer message loop"); - PollResult::Continue + gst::BusSyncReply::Drop + }); } } +fn send_state(handle: &tokio::runtime::Handle, addr: &WeakAddress<MusicBot>, state: State) { + handle.spawn(addr.send(MusicBotMessage::StateChange(state))); +} + #[derive(Debug)] pub enum AudioPlayerError { + MissingPlugin(String), GStreamerError(glib::error::BoolError), StateChangeFailed, SeekError, diff --git a/src/bot/master.rs b/src/bot/master.rs index 1480e17..94332ac 100644 --- a/src/bot/master.rs +++ b/src/bot/master.rs @@ -1,41 +1,52 @@ use std::collections::HashMap; -use std::future::Future; -use std::sync::{Arc, RwLock}; -use log::info; +use async_trait::async_trait; +use futures::future; use rand::{rngs::SmallRng, seq::SliceRandom, SeedableRng}; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc::UnboundedSender; -use tsclientlib::{ClientId, Connection, Identity, MessageTarget}; +use slog::{error, info, o, trace, Logger}; +use tsclientlib::{ClientId, ConnectOptions, Connection, Identity, MessageTarget}; +use xtra::{spawn::Tokio, Actor, Address, Context, Handler, Message, WeakAddress}; use crate::audio_player::AudioPlayerError; use crate::teamspeak::TeamSpeakConnection; use crate::Args; -use crate::bot::{MusicBot, MusicBotArgs, MusicBotMessage}; +use crate::bot::{GetBotData, GetChannel, GetName, MusicBot, MusicBotArgs, MusicBotMessage}; pub struct MasterBot { - config: Arc<MasterConfig>, - music_bots: Arc<RwLock<MusicBots>>, + config: MasterConfig, + my_addr: Option<WeakAddress<Self>>, teamspeak: TeamSpeakConnection, - sender: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, + available_names: Vec<String>, + available_ids: Vec<Identity>, + connected_bots: HashMap<String, Address<MusicBot>>, + rng: SmallRng, + logger: Logger, } -struct MusicBots { - rng: SmallRng, - available_names: Vec<usize>, - available_ids: Vec<usize>, - connected_bots: HashMap<String, Arc<MusicBot>>, +#[derive(Debug, Serialize, Deserialize)] +pub struct MasterArgs { + #[serde(default = "default_name")] + pub master_name: String, + pub address: String, + pub channel: Option<String>, + #[serde(default = "default_verbose")] + pub verbose: u8, + pub domain: String, + pub bind_address: String, + pub names: Vec<String>, + pub id: Option<Identity>, + pub ids: Option<Vec<Identity>>, } impl MasterBot { - pub async fn new(args: MasterArgs) -> (Arc<Self>, impl Future) { - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - let tx = Arc::new(RwLock::new(tx)); - info!("Starting in TeamSpeak mode"); + pub async fn spawn(args: MasterArgs, logger: Logger) -> Address<Self> { + info!(logger, "Starting in TeamSpeak mode"); let mut con_config = Connection::build(args.address.clone()) + .logger(logger.clone()) .version(tsclientlib::Version::Linux_3_3_2) .name(args.master_name.clone()) .identity(args.id.expect("identity should exist")) @@ -47,168 +58,116 @@ impl MasterBot { con_config = con_config.channel(channel); } - let connection = TeamSpeakConnection::new(tx.clone(), con_config) - .await - .unwrap(); + let connection = TeamSpeakConnection::new(logger.clone()).await.unwrap(); + trace!(logger, "Created teamspeak connection"); - let config = Arc::new(MasterConfig { + let config = MasterConfig { master_name: args.master_name, address: args.address, - names: args.names, - ids: args.ids.expect("identies should exists"), - local: args.local, verbose: args.verbose, - }); - - let name_count = config.names.len(); - let id_count = config.ids.len(); + }; - let music_bots = Arc::new(RwLock::new(MusicBots { + let bot_addr = Self { + config, + my_addr: None, + teamspeak: connection, + logger: logger.clone(), rng: SmallRng::from_entropy(), - available_names: (0..name_count).collect(), - available_ids: (0..id_count).collect(), + available_names: args.names, + available_ids: args.ids.expect("identities"), connected_bots: HashMap::new(), - })); + } + .create(None) + .spawn(&mut Tokio::Global); - let bot = Arc::new(Self { - config, - music_bots, - teamspeak: connection, - sender: tx.clone(), - }); - - let cbot = bot.clone(); - let msg_loop = async move { - 'outer: loop { - while let Some(msg) = rx.recv().await { - match msg { - MusicBotMessage::Quit(reason) => { - let mut cteamspeak = cbot.teamspeak.clone(); - cteamspeak.disconnect(&reason).await; - break 'outer; - } - MusicBotMessage::ClientDisconnected { id, .. } => { - if id == cbot.my_id().await { - // TODO Reconnect since quit was not called - break 'outer; - } - } - _ => cbot.on_message(msg).await.unwrap(), - } - } - } - }; + bot_addr.send(Connect(con_config)).await.unwrap().unwrap(); + trace!(logger, "Spawned master bot actor"); - (bot, msg_loop) + bot_addr } - async fn build_bot_args_for(&self, id: ClientId) -> Result<MusicBotArgs, BotCreationError> { - let mut cteamspeak = self.teamspeak.clone(); - let channel = match cteamspeak.channel_of_user(id).await { + async fn bot_args_for_client( + &mut self, + user_id: ClientId, + ) -> Result<MusicBotArgs, BotCreationError> { + let channel = match self.teamspeak.channel_of_user(user_id).await { Some(channel) => channel, None => return Err(BotCreationError::UnfoundUser), }; - if channel == cteamspeak.my_channel().await { + if channel == self.teamspeak.current_channel().await.unwrap() { return Err(BotCreationError::MasterChannel( self.config.master_name.clone(), )); } - let MusicBots { - ref mut rng, - ref mut available_names, - ref mut available_ids, - ref connected_bots, - } = &mut *self.music_bots.write().expect("RwLock was not poisoned"); - - for bot in connected_bots.values() { - if bot.my_channel().await == channel { - return Err(BotCreationError::MultipleBots(bot.name().to_owned())); + for bot in self.connected_bots.values() { + if bot.send(GetChannel).await.unwrap() == Some(channel) { + return Err(BotCreationError::MultipleBots( + bot.send(GetName).await.unwrap(), + )); } } - let channel_path = cteamspeak - .channel_path_of_user(id) + let channel_path = self + .teamspeak + .channel_path_of_user(user_id) .await .expect("can find poke sender"); - available_names.shuffle(rng); - let name_index = match available_names.pop() { + self.available_names.shuffle(&mut self.rng); + let name = match self.available_names.pop() { Some(v) => v, None => { return Err(BotCreationError::OutOfNames); } }; - let name = self.config.names[name_index].clone(); - available_ids.shuffle(rng); - let id_index = match available_ids.pop() { + self.available_ids.shuffle(&mut self.rng); + let identity = match self.available_ids.pop() { Some(v) => v, None => { return Err(BotCreationError::OutOfIdentities); } }; - let id = self.config.ids[id_index].clone(); - - let cmusic_bots = self.music_bots.clone(); - let disconnect_cb = Box::new(move |n, name_index, id_index| { - let mut music_bots = cmusic_bots.write().expect("RwLock was not poisoned"); - music_bots.connected_bots.remove(&n); - music_bots.available_names.push(name_index); - music_bots.available_ids.push(id_index); - }); - - info!("Connecting to {} on {}", channel_path, self.config.address); - Ok(MusicBotArgs { - name, - name_index, - id_index, - local: self.config.local, + name: name.clone(), + master: self.my_addr.clone(), address: self.config.address.clone(), - id, + identity, + local: false, channel: channel_path, verbose: self.config.verbose, - disconnect_cb, + logger: self.logger.new(o!("musicbot" => name)), }) } - async fn spawn_bot_for(&self, id: ClientId) { - match self.build_bot_args_for(id).await { + async fn spawn_bot_for_client(&mut self, id: ClientId) { + match self.bot_args_for_client(id).await { Ok(bot_args) => { - let (bot, fut) = MusicBot::new(bot_args).await; - tokio::spawn(fut); - let mut music_bots = self.music_bots.write().expect("RwLock was not poisoned"); - music_bots - .connected_bots - .insert(bot.name().to_string(), bot); - } - Err(e) => { - let mut cteamspeak = self.teamspeak.clone(); - cteamspeak.send_message_to_user(id, e.to_string()).await + let name = bot_args.name.clone(); + let bot = MusicBot::spawn(bot_args).await; + self.connected_bots.insert(name, bot); } + Err(e) => self.teamspeak.send_message_to_user(id, e.to_string()).await, } } - async fn on_message(&self, message: MusicBotMessage) -> Result<(), AudioPlayerError> { + async fn on_message(&mut self, message: MusicBotMessage) -> Result<(), AudioPlayerError> { match message { MusicBotMessage::TextMessage(message) => { if let MessageTarget::Poke(who) = message.target { - info!("Poked by {}, creating bot for their channel", who); - self.spawn_bot_for(who).await; + info!( + self.logger, + "Poked, creating bot"; "user" => %who + ); + self.spawn_bot_for_client(who).await; } } - MusicBotMessage::ChannelAdded(id) => { - let mut cteamspeak = self.teamspeak.clone(); - cteamspeak.subscribe(id).await; - } MusicBotMessage::ClientAdded(id) => { - let mut cteamspeak = self.teamspeak.clone(); - - if id == cteamspeak.my_id().await { - cteamspeak + if id == self.teamspeak.my_id().await { + self.teamspeak .set_description(String::from("Poke me if you want a music bot!")) .await; } @@ -219,41 +178,17 @@ impl MasterBot { Ok(()) } - async fn my_id(&self) -> ClientId { - let mut cteamspeak = self.teamspeak.clone(); + pub async fn bot_data(&self, name: String) -> Option<crate::web_server::BotData> { + let bot = self.connected_bots.get(&name)?; - cteamspeak.my_id().await + bot.send(GetBotData).await.ok() } - pub fn bot_data(&self, name: String) -> Option<crate::web_server::BotData> { - let music_bots = self.music_bots.read().unwrap(); - let bot = music_bots.connected_bots.get(&name)?; - - Some(crate::web_server::BotData { - name, - state: bot.state(), - volume: bot.volume(), - position: bot.position(), - currently_playing: bot.currently_playing(), - playlist: bot.playlist_to_vec(), - }) - } - - pub fn bot_datas(&self) -> Vec<crate::web_server::BotData> { - let music_bots = self.music_bots.read().unwrap(); - - let len = music_bots.connected_bots.len(); + pub async fn bot_datas(&self) -> Vec<crate::web_server::BotData> { + let len = self.connected_bots.len(); let mut result = Vec::with_capacity(len); - for (name, bot) in &music_bots.connected_bots { - let bot_data = crate::web_server::BotData { - name: name.clone(), - state: bot.state(), - volume: bot.volume(), - position: bot.position(), - currently_playing: bot.currently_playing(), - playlist: bot.playlist_to_vec(), - }; - + for bot in self.connected_bots.values() { + let bot_data = bot.send(GetBotData).await.unwrap(); result.push(bot_data); } @@ -261,24 +196,96 @@ impl MasterBot { } pub fn bot_names(&self) -> Vec<String> { - let music_bots = self.music_bots.read().unwrap(); - - let len = music_bots.connected_bots.len(); + let len = self.connected_bots.len(); let mut result = Vec::with_capacity(len); - for name in music_bots.connected_bots.keys() { + for name in self.connected_bots.keys() { result.push(name.clone()); } result } - pub fn quit(&self, reason: String) { - let music_bots = self.music_bots.read().unwrap(); - for bot in music_bots.connected_bots.values() { - bot.quit(reason.clone()) + fn on_bot_disconnect(&mut self, name: String, id: Identity) { + self.connected_bots.remove(&name); + self.available_names.push(name); + self.available_ids.push(id); + } + + pub async fn quit(&mut self, reason: String) -> Result<(), tsclientlib::Error> { + let futures = self + .connected_bots + .values() + .map(|b| b.send(Quit(reason.clone()))); + for res in future::join_all(futures).await { + if let Err(e) = res { + error!(self.logger, "Failed to shut down bot"; "error" => %e); + } } - let sender = self.sender.read().unwrap(); - sender.send(MusicBotMessage::Quit(reason)).unwrap(); + self.teamspeak.disconnect(&reason).await + } +} + +#[async_trait] +impl Actor for MasterBot { + async fn started(&mut self, ctx: &mut Context<Self>) { + self.my_addr = Some(ctx.address().unwrap().downgrade()); + } +} + +pub struct Connect(pub ConnectOptions); +impl Message for Connect { + type Result = Result<(), tsclientlib::Error>; +} + +#[async_trait] +impl Handler<Connect> for MasterBot { + async fn handle( + &mut self, + opt: Connect, + ctx: &mut Context<Self>, + ) -> Result<(), tsclientlib::Error> { + let addr = ctx.address().unwrap(); + self.teamspeak.connect_for_bot(opt.0, addr.downgrade())?; + Ok(()) + } +} + +pub struct Quit(pub String); +impl Message for Quit { + type Result = Result<(), tsclientlib::Error>; +} + +#[async_trait] +impl Handler<Quit> for MasterBot { + async fn handle(&mut self, q: Quit, _: &mut Context<Self>) -> Result<(), tsclientlib::Error> { + self.quit(q.0).await + } +} + +pub struct BotDisonnected { + pub name: String, + pub identity: Identity, +} + +impl Message for BotDisonnected { + type Result = (); +} + +#[async_trait] +impl Handler<BotDisonnected> for MasterBot { + async fn handle(&mut self, dc: BotDisonnected, _: &mut Context<Self>) { + self.on_bot_disconnect(dc.name, dc.identity); + } +} + +#[async_trait] +impl Handler<MusicBotMessage> for MasterBot { + async fn handle( + &mut self, + msg: MusicBotMessage, + _: &mut Context<Self>, + ) -> Result<(), AudioPlayerError> { + self.on_message(msg).await } } @@ -313,31 +320,10 @@ impl std::fmt::Display for BotCreationError { } } -#[derive(Debug, Serialize, Deserialize)] -pub struct MasterArgs { - #[serde(default = "default_name")] - pub master_name: String, - #[serde(default = "default_local")] - pub local: bool, - pub address: String, - pub channel: Option<String>, - #[serde(default = "default_verbose")] - pub verbose: u8, - pub domain: String, - pub bind_address: String, - pub names: Vec<String>, - pub id: Option<Identity>, - pub ids: Option<Vec<Identity>>, -} - fn default_name() -> String { String::from("PokeBot") } -fn default_local() -> bool { - false -} - fn default_verbose() -> u8 { 0 } @@ -345,7 +331,6 @@ fn default_verbose() -> u8 { impl MasterArgs { pub fn merge(self, args: Args) -> Self { let address = args.address.unwrap_or(self.address); - let local = args.local || self.local; let channel = args.master_channel.or(self.channel); let verbose = if args.verbose > 0 { args.verbose @@ -357,7 +342,6 @@ impl MasterArgs { master_name: self.master_name, names: self.names, ids: self.ids, - local, address, domain: self.domain, bind_address: self.bind_address, @@ -371,8 +355,5 @@ impl MasterArgs { pub struct MasterConfig { pub master_name: String, pub address: String, - pub names: Vec<String>, - pub ids: Vec<Identity>, - pub local: bool, pub verbose: u8, } diff --git a/src/bot/music.rs b/src/bot/music.rs index 90305d0..a57b66c 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -1,25 +1,22 @@ -use std::future::Future; -use std::io::BufRead; -use std::sync::{Arc, RwLock}; -use std::thread; -use std::time::Duration; +use async_trait::async_trait; -use log::{debug, info}; use serde::Serialize; +use slog::{debug, info, Logger}; use structopt::StructOpt; -use tokio::sync::mpsc::UnboundedSender; use tsclientlib::{data, ChannelId, ClientId, Connection, Identity, Invoker, MessageTarget}; +use xtra::{spawn::Tokio, Actor, Address, Context, Handler, Message, WeakAddress}; -use crate::audio_player::{AudioPlayer, AudioPlayerError, PollResult}; +use crate::audio_player::{AudioPlayer, AudioPlayerError}; +use crate::bot::{BotDisonnected, Connect, MasterBot, Quit}; use crate::command::Command; use crate::command::VolumeChange; use crate::playlist::Playlist; use crate::teamspeak as ts; -use crate::youtube_dl::AudioMetadata; +use crate::youtube_dl::{self, AudioMetadata}; use ts::TeamSpeakConnection; #[derive(Debug)] -pub struct Message { +pub struct ChatMessage { pub target: MessageTarget, pub invoker: Invoker, pub text: String, @@ -33,6 +30,10 @@ pub enum State { EndOfStream, } +impl Message for State { + type Result = (); +} + impl std::fmt::Display for State { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { match self { @@ -47,7 +48,7 @@ impl std::fmt::Display for State { #[derive(Debug)] pub enum MusicBotMessage { - TextMessage(Message), + TextMessage(ChatMessage), ClientChannel { client: ClientId, old_channel: ChannelId, @@ -59,112 +60,95 @@ pub enum MusicBotMessage { client: Box<data::Client>, }, StateChange(State), - Quit(String), +} + +impl Message for MusicBotMessage { + type Result = Result<(), AudioPlayerError>; } pub struct MusicBot { name: String, - player: Arc<AudioPlayer>, + identity: Identity, + player: AudioPlayer, teamspeak: Option<TeamSpeakConnection>, - playlist: Arc<RwLock<Playlist>>, - state: Arc<RwLock<State>>, + master: Option<WeakAddress<MasterBot>>, + playlist: Playlist, + state: State, + logger: Logger, } pub struct MusicBotArgs { pub name: String, - pub name_index: usize, - pub id_index: usize, + pub master: Option<WeakAddress<MasterBot>>, pub local: bool, pub address: String, - pub id: Identity, + pub identity: Identity, pub channel: String, pub verbose: u8, - pub disconnect_cb: Box<dyn FnMut(String, usize, usize) + Send + Sync>, + pub logger: Logger, } impl MusicBot { - pub async fn new(args: MusicBotArgs) -> (Arc<Self>, impl Future<Output = ()>) { - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - let tx = Arc::new(RwLock::new(tx)); - let (player, connection) = if args.local { - info!("Starting in CLI mode"); - let audio_player = AudioPlayer::new(tx.clone(), None).unwrap(); - - (audio_player, None) - } else { - info!("Starting in TeamSpeak mode"); - - let con_config = Connection::build(args.address) - .version(tsclientlib::Version::Linux_3_3_2) - .name(format!("🎵 {}", args.name)) - .identity(args.id) - .log_commands(args.verbose >= 1) - .log_packets(args.verbose >= 2) - .log_udp_packets(args.verbose >= 3) - .channel(args.channel); - - let connection = TeamSpeakConnection::new(tx.clone(), con_config) - .await - .unwrap(); - let mut cconnection = connection.clone(); - let audio_player = AudioPlayer::new( - tx.clone(), - Some(Box::new(move |samples| { - let mut rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(cconnection.send_audio_packet(samples)); - })), - ) - .unwrap(); - - (audio_player, Some(connection)) - }; - + pub async fn spawn(args: MusicBotArgs) -> Address<Self> { + let mut player = AudioPlayer::new(args.logger.clone()).unwrap(); player.change_volume(VolumeChange::Absolute(0.5)).unwrap(); - let player = Arc::new(player); - let playlist = Arc::new(RwLock::new(Playlist::new())); - spawn_gstreamer_thread(player.clone(), tx.clone()); + let playlist = Playlist::new(args.logger.clone()); - if args.local { - spawn_stdin_reader(tx); - } + let teamspeak = if args.local { + info!(args.logger, "Starting in CLI mode"); + player.setup_with_audio_callback(None).unwrap(); - let bot = Arc::new(Self { + None + } else { + Some(TeamSpeakConnection::new(args.logger.clone()).await.unwrap()) + }; + let bot = Self { name: args.name.clone(), + master: args.master, + identity: args.identity.clone(), player, - teamspeak: connection, + teamspeak, playlist, - state: Arc::new(RwLock::new(State::EndOfStream)), - }); - - let cbot = bot.clone(); - let mut disconnect_cb = args.disconnect_cb; - let name = args.name; - let name_index = args.name_index; - let id_index = args.id_index; - let msg_loop = async move { - 'outer: loop { - while let Some(msg) = rx.recv().await { - if let MusicBotMessage::Quit(reason) = msg { - if let Some(ts) = &cbot.teamspeak { - let mut ts = ts.clone(); - ts.disconnect(&reason).await; - } - disconnect_cb(name, name_index, id_index); - break 'outer; - } - cbot.on_message(msg).await.unwrap(); - } - } - debug!("Left message loop"); + state: State::EndOfStream, + logger: args.logger.clone(), }; - bot.update_name(State::EndOfStream).await; + let bot_addr = bot.create(None).spawn(&mut Tokio::Global); + + info!( + args.logger, + "Connecting"; + "name" => &args.name, + "channel" => &args.channel, + "address" => &args.address, + ); + + let opt = Connection::build(args.address) + .logger(args.logger.clone()) + .version(tsclientlib::Version::Linux_3_3_2) + .name(format!("🎵 {}", args.name)) + .identity(args.identity) + .log_commands(args.verbose >= 1) + .log_packets(args.verbose >= 2) + .log_udp_packets(args.verbose >= 3) + .channel(args.channel); + bot_addr.send(Connect(opt)).await.unwrap().unwrap(); + bot_addr + .send(MusicBotMessage::StateChange(State::EndOfStream)) + .await + .unwrap() + .unwrap(); + + if args.local { + debug!(args.logger, "Spawning stdin reader thread"); + spawn_stdin_reader(bot_addr.downgrade()); + } - (bot, msg_loop) + bot_addr } - async fn start_playing_audio(&self, metadata: AudioMetadata) { + async fn start_playing_audio(&mut self, metadata: AudioMetadata) { let duration = if let Some(duration) = metadata.duration { format!("({})", ts::bold(&humantime::format_duration(duration))) } else { @@ -184,25 +168,16 @@ impl MusicBot { self.player.play().unwrap(); } - pub async fn add_audio(&self, url: String, user: String) { - match crate::youtube_dl::get_audio_download_from_url(url).await { + pub async fn add_audio(&mut self, url: String, user: String) { + match youtube_dl::get_audio_download_from_url(url, &self.logger).await { Ok(mut metadata) => { metadata.added_by = user; - info!("Found audio url: {}", metadata.url); + info!(self.logger, "Found source"; "url" => &metadata.url); - // RWLockGuard can not be kept around or the compiler complains that - // it might cross the await boundary - self.playlist - .write() - .expect("RwLock was not poisoned") - .push(metadata.clone()); + self.playlist.push(metadata.clone()); if !self.player.is_started() { - let entry = self - .playlist - .write() - .expect("RwLock was not poisoned") - .pop(); + let entry = self.playlist.pop(); if let Some(request) = entry { self.start_playing_audio(request).await; } @@ -222,7 +197,7 @@ impl MusicBot { } } Err(e) => { - info!("Failed to find audio url: {}", e); + info!(self.logger, "Failed to find audio url"; "error" => &e); self.send_message(format!("Failed to find url: {}", e)) .await; @@ -235,74 +210,50 @@ impl MusicBot { } pub fn state(&self) -> State { - *self.state.read().expect("RwLock was not poisoned") + self.state } - pub fn volume(&self) -> f64 { + pub async fn volume(&self) -> f64 { self.player.volume() } - pub fn position(&self) -> Option<Duration> { - self.player.position() - } - - pub fn currently_playing(&self) -> Option<AudioMetadata> { - self.player.currently_playing() - } + pub async fn current_channel(&mut self) -> Option<ChannelId> { + let ts = self.teamspeak.as_mut().expect("current_channel needs ts"); - pub fn playlist_to_vec(&self) -> Vec<AudioMetadata> { - self.playlist.read().unwrap().to_vec() + ts.current_channel().await } - pub async fn my_channel(&self) -> ChannelId { - let ts = self.teamspeak.as_ref().expect("my_channel needs ts"); - - let mut ts = ts.clone(); - ts.my_channel().await - } + async fn user_count(&mut self, channel: ChannelId) -> u32 { + let ts = self.teamspeak.as_mut().expect("user_count needs ts"); - async fn user_count(&self, channel: ChannelId) -> u32 { - let ts = self.teamspeak.as_ref().expect("user_count needs ts"); - - let mut ts = ts.clone(); ts.user_count(channel).await } - async fn send_message(&self, text: String) { - debug!("Sending message to TeamSpeak: {}", text); + async fn send_message(&mut self, text: String) { + debug!(self.logger, "Sending message to TeamSpeak"; "message" => &text); - if let Some(ts) = &self.teamspeak { - let mut ts = ts.clone(); + if let Some(ts) = &mut self.teamspeak { ts.send_message_to_channel(text).await; } } - async fn set_nickname(&self, name: String) { - info!("Setting TeamSpeak nickname: {}", name); + async fn set_nickname(&mut self, name: String) { + info!(self.logger, "Setting TeamSpeak nickname"; "name" => &name); - if let Some(ts) = &self.teamspeak { - let mut ts = ts.clone(); + if let Some(ts) = &mut self.teamspeak { ts.set_nickname(name).await; } } - async fn set_description(&self, desc: String) { - info!("Setting TeamSpeak description: {}", desc); + async fn set_description(&mut self, desc: String) { + info!(self.logger, "Setting TeamSpeak description"; "description" => &desc); - if let Some(ts) = &self.teamspeak { - let mut ts = ts.clone(); + if let Some(ts) = &mut self.teamspeak { ts.set_description(desc).await; } } - async fn subscribe(&self, id: ChannelId) { - if let Some(ts) = &self.teamspeak { - let mut ts = ts.clone(); - ts.subscribe(id).await; - } - } - - async fn on_text(&self, message: Message) -> Result<(), AudioPlayerError> { + async fn on_text(&mut self, message: ChatMessage) -> Result<(), AudioPlayerError> { let msg = message.text; if msg.starts_with('!') { let tokens = msg[1..].split_whitespace().collect::<Vec<_>>(); @@ -319,13 +270,15 @@ impl MusicBot { Ok(()) } - async fn on_command(&self, command: Command, invoker: Invoker) -> Result<(), AudioPlayerError> { + async fn on_command( + &mut self, + command: Command, + invoker: Invoker, + ) -> Result<(), AudioPlayerError> { match command { Command::Play => { - let playlist = self.playlist.read().expect("RwLock was not poisoned"); - if !self.player.is_started() { - if !playlist.is_empty() { + if !self.playlist.is_empty() { self.player.stop_current()?; } } else { @@ -357,35 +310,32 @@ impl MusicBot { } } Command::Next => { - let playlist = self.playlist.read().expect("RwLock was not poisoned"); - if !playlist.is_empty() { - info!("Skipping to next track"); + if !self.playlist.is_empty() { + info!(self.logger, "Skipping to next track"); self.player.stop_current()?; } else { - info!("Playlist empty, cannot skip"); + info!(self.logger, "Playlist empty, cannot skip"); self.player.reset()?; } } Command::Clear => { - self.playlist - .write() - .expect("RwLock was not poisoned") - .clear(); + self.send_message(String::from("Cleared playlist")).await; + self.playlist.clear(); } Command::Volume { volume } => { self.player.change_volume(volume)?; self.update_name(self.state()).await; } Command::Leave => { - self.quit(String::from("Leaving")); + self.quit(String::from("Leaving"), true).await.unwrap(); } } Ok(()) } - async fn update_name(&self, state: State) { - let volume = (self.volume() * 100.0).round(); + async fn update_name(&mut self, state: State) { + let volume = (self.volume().await * 100.0).round(); let name = match state { State::EndOfStream => format!("🎵 {} ({}%)", self.name, volume), _ => format!("🎵 {} - {} ({}%)", self.name, state, volume), @@ -393,43 +343,39 @@ impl MusicBot { self.set_nickname(name).await; } - async fn on_state(&self, state: State) -> Result<(), AudioPlayerError> { - let current_state = *self.state.read().unwrap(); - if current_state != state { - match state { + async fn on_state(&mut self, new_state: State) -> Result<(), AudioPlayerError> { + if self.state != new_state { + match new_state { State::EndOfStream => { - let next_track = self - .playlist - .write() - .expect("RwLock was not poisoned") - .pop(); + self.player.reset()?; + let next_track = self.playlist.pop(); if let Some(request) = next_track { - info!("Advancing playlist"); + info!(self.logger, "Advancing playlist"); self.start_playing_audio(request).await; } else { - self.update_name(state).await; + self.update_name(new_state).await; self.set_description(String::new()).await; } } State::Stopped => { - if current_state != State::EndOfStream { - self.update_name(state).await; + if self.state != State::EndOfStream { + self.update_name(new_state).await; self.set_description(String::new()).await; } } - _ => self.update_name(state).await, + _ => self.update_name(new_state).await, } } - if !(current_state == State::EndOfStream && state == State::Stopped) { - *self.state.write().unwrap() = state; + if !(self.state == State::EndOfStream && new_state == State::Stopped) { + self.state = new_state; } Ok(()) } - async fn on_message(&self, message: MusicBotMessage) -> Result<(), AudioPlayerError> { + async fn on_message(&mut self, message: MusicBotMessage) -> Result<(), AudioPlayerError> { match message { MusicBotMessage::TextMessage(message) => { if MessageTarget::Channel == message.target { @@ -446,9 +392,6 @@ impl MusicBot { let old_channel = client.channel; self.on_client_left_channel(old_channel).await; } - MusicBotMessage::ChannelAdded(id) => { - self.subscribe(id).await; - } MusicBotMessage::StateChange(state) => { self.on_state(state).await?; } @@ -458,60 +401,163 @@ impl MusicBot { Ok(()) } - async fn on_client_left_channel(&self, old_channel: ChannelId) { - let my_channel = self.my_channel().await; - if old_channel == my_channel && self.user_count(my_channel).await <= 1 { - self.quit(String::from("Channel is empty")); + // FIXME logs an error if this music bot is the one leaving + async fn on_client_left_channel(&mut self, old_channel: ChannelId) { + let current_channel = match self.current_channel().await { + Some(c) => c, + None => { + return; + } + }; + if old_channel == current_channel && self.user_count(current_channel).await <= 1 { + self.quit(String::from("Channel is empty"), true) + .await + .unwrap(); + } + } + + pub async fn quit( + &mut self, + reason: String, + inform_master: bool, + ) -> Result<(), tsclientlib::Error> { + // FIXME logs errors if the bot is playing something because it tries to + // change its name and description + self.player.reset().unwrap(); + + let ts = self.teamspeak.as_mut().unwrap(); + ts.disconnect(&reason).await?; + + if inform_master { + if let Some(master) = &self.master { + master + .send(BotDisonnected { + name: self.name.clone(), + identity: self.identity.clone(), + }) + .await + .unwrap(); + } + } + + Ok(()) + } +} + +#[async_trait] +impl Actor for MusicBot { + async fn started(&mut self, ctx: &mut Context<Self>) { + let addr = ctx.address().unwrap().downgrade(); + self.player.register_bot(addr); + } +} + +#[async_trait] +impl Handler<Connect> for MusicBot { + async fn handle( + &mut self, + opt: Connect, + ctx: &mut Context<Self>, + ) -> Result<(), tsclientlib::Error> { + let addr = ctx.address().unwrap().downgrade(); + self.teamspeak + .as_mut() + .unwrap() + .connect_for_bot(opt.0, addr)?; + + let mut connection = self.teamspeak.as_ref().unwrap().clone(); + let handle = tokio::runtime::Handle::current(); + self.player + .setup_with_audio_callback(Some(Box::new(move |samples| { + handle.block_on(connection.send_audio_packet(samples)); + }))) + .unwrap(); + + Ok(()) + } +} + +pub struct GetName; +impl Message for GetName { + type Result = String; +} + +#[async_trait] +impl Handler<GetName> for MusicBot { + async fn handle(&mut self, _: GetName, _: &mut Context<Self>) -> String { + self.name().to_owned() + } +} + +pub struct GetBotData; +impl Message for GetBotData { + type Result = crate::web_server::BotData; +} + +#[async_trait] +impl Handler<GetBotData> for MusicBot { + async fn handle(&mut self, _: GetBotData, _: &mut Context<Self>) -> crate::web_server::BotData { + crate::web_server::BotData { + name: self.name.clone(), + playlist: self.playlist.to_vec(), + currently_playing: self.player.currently_playing(), + position: self.player.position(), + state: self.state(), + volume: self.volume().await, } } +} - pub fn quit(&self, reason: String) { - self.player.quit(reason); +pub struct GetChannel; +impl Message for GetChannel { + type Result = Option<ChannelId>; +} + +#[async_trait] +impl Handler<GetChannel> for MusicBot { + async fn handle(&mut self, _: GetChannel, _: &mut Context<Self>) -> Option<ChannelId> { + self.current_channel().await } } -fn spawn_stdin_reader(tx: Arc<RwLock<UnboundedSender<MusicBotMessage>>>) { - debug!("Spawning stdin reader thread"); - thread::Builder::new() - .name(String::from("stdin reader")) - .spawn(move || { - let stdin = ::std::io::stdin(); - let lock = stdin.lock(); - for line in lock.lines() { - let line = line.unwrap(); - - let message = MusicBotMessage::TextMessage(Message { - target: MessageTarget::Channel, - invoker: Invoker { - name: String::from("stdin"), - id: ClientId(0), - uid: None, - }, - text: line, - }); - - let tx = tx.read().unwrap(); - tx.send(message).unwrap(); - } - }) - .expect("Failed to spawn stdin reader thread"); +#[async_trait] +impl Handler<Quit> for MusicBot { + async fn handle(&mut self, q: Quit, _: &mut Context<Self>) -> Result<(), tsclientlib::Error> { + self.quit(q.0, false).await + } } -fn spawn_gstreamer_thread( - player: Arc<AudioPlayer>, - tx: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, -) { - thread::Builder::new() - .name(String::from("gstreamer polling")) - .spawn(move || loop { - if player.poll() == PollResult::Quit { - break; - } +#[async_trait] +impl Handler<MusicBotMessage> for MusicBot { + async fn handle( + &mut self, + msg: MusicBotMessage, + _: &mut Context<Self>, + ) -> Result<(), AudioPlayerError> { + self.on_message(msg).await + } +} - tx.read() - .unwrap() - .send(MusicBotMessage::StateChange(State::EndOfStream)) - .unwrap(); - }) - .expect("Failed to spawn gstreamer thread"); +fn spawn_stdin_reader(addr: WeakAddress<MusicBot>) { + use tokio::io::AsyncBufReadExt; + + tokio::task::spawn(async move { + let stdin = tokio::io::stdin(); + let reader = tokio::io::BufReader::new(stdin); + let mut lines = reader.lines(); + + while let Some(line) = lines.next_line().await.unwrap() { + let message = MusicBotMessage::TextMessage(ChatMessage { + target: MessageTarget::Channel, + invoker: Invoker { + name: String::from("stdin"), + id: ClientId(0), + uid: None, + }, + text: line, + }); + + addr.send(message).await.unwrap().unwrap(); + } + }); } diff --git a/src/log_bridge.rs b/src/log_bridge.rs new file mode 100644 index 0000000..35bcb01 --- /dev/null +++ b/src/log_bridge.rs @@ -0,0 +1,101 @@ +// TODO Temporary file until we have a better logging setup for slog + +use slog::{Drain, KV}; +use std::fmt::{self, Arguments, Write}; + +pub struct LogBridge<T>(pub T); + +impl<T: log::Log> Drain for LogBridge<T> { + type Ok = (); + type Err = slog::Error; + + fn log(&self, record: &slog::Record, kvs: &slog::OwnedKVList) -> Result<(), Self::Err> { + let mut target = record.tag(); + if target.is_empty() { + target = record.module(); + } + + let lazy = LazyLog::new(record, kvs); + + self.0.log( + &log::Record::builder() + .args(format_args!("{}", lazy)) + .level(level_to_log(record.level())) + .target(target) + .module_path_static(Some(record.module())) + .file_static(Some(record.file())) + .line(Some(record.line())) + .build(), + ); + + Ok(()) + } + + fn is_enabled(&self, level: slog::Level) -> bool { + let meta = log::Metadata::builder().level(level_to_log(level)).build(); + + self.0.enabled(&meta) + } +} + +fn level_to_log(level: slog::Level) -> log::Level { + match level { + slog::Level::Critical | slog::Level::Error => log::Level::Error, + slog::Level::Warning => log::Level::Warn, + slog::Level::Info => log::Level::Info, + slog::Level::Debug => log::Level::Debug, + slog::Level::Trace => log::Level::Trace, + } +} + +struct LazyLog<'a> { + record: &'a slog::Record<'a>, + kvs: &'a slog::OwnedKVList, +} + +impl<'a> LazyLog<'a> { + fn new(record: &'a slog::Record, kvs: &'a slog::OwnedKVList) -> Self { + LazyLog { record, kvs } + } +} + +impl<'a> fmt::Display for LazyLog<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.record.msg())?; + + let mut ser = StringSerializer::new(); + + self.kvs + .serialize(self.record, &mut ser) + .map_err(|_| fmt::Error)?; + self.record + .kv() + .serialize(self.record, &mut ser) + .map_err(|_| fmt::Error)?; + + write!(f, "{}", ser.finish()) + } +} + +struct StringSerializer { + inner: String, +} + +impl StringSerializer { + fn new() -> Self { + StringSerializer { + inner: String::new(), + } + } + + fn finish(self) -> String { + self.inner + } +} + +impl slog::Serializer for StringSerializer { + fn emit_arguments(&mut self, key: slog::Key, value: &Arguments) -> slog::Result { + write!(self.inner, ", {}: {}", key, value)?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index f755db0..51b1e38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,22 +2,25 @@ use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; use std::thread; -use std::time::Duration; -use log::{debug, error, info}; +use slog::{debug, error, info, o, Drain, Logger}; use structopt::clap::AppSettings; use structopt::StructOpt; +#[cfg(unix)] +use tokio::signal::unix::*; use tsclientlib::Identity; mod audio_player; mod bot; mod command; +mod log_bridge; mod playlist; mod teamspeak; mod web_server; mod youtube_dl; -use bot::{MasterArgs, MasterBot, MusicBot, MusicBotArgs}; +use bot::{MasterArgs, MasterBot, MusicBot, MusicBotArgs, Quit}; +use log_bridge::LogBridge; #[derive(StructOpt, Debug)] #[structopt(global_settings = &[AppSettings::ColoredHelp])] @@ -51,6 +54,10 @@ pub struct Args { help = "The channel the master bot should connect to" )] master_channel: Option<String>, + // 0. Print nothing + // 1. Print command string + // 2. Print packets + // 3. Print udp packets #[structopt( short = "v", long = "verbose", @@ -58,25 +65,44 @@ pub struct Args { parse(from_occurrences) )] verbose: u8, - // 0. Print nothing - // 1. Print command string - // 2. Print packets - // 3. Print udp packets } #[tokio::main] async fn main() { - if let Err(e) = run().await { - println!("Error: {}", e); + let root_logger = { + let config = log4rs::load_config_file("log4rs.yml", Default::default()).unwrap(); + let drain = LogBridge(log4rs::Logger::new(config)).fuse(); + // slog_async adds a channel because log4rs if not unwind safe + let drain = slog_async::Async::new(drain).build().fuse(); + + Logger::root(drain, o!()) + }; + + let scope_guard = slog_scope::set_global_logger(root_logger.clone()); + // On SIGTERM the logger resets for some reason which makes the bot panic + // if it tries to log anything + scope_guard.cancel_reset(); + + slog_stdlog::init().unwrap(); + + if let Err(e) = run(root_logger.clone()).await { + error!(root_logger, "{}", e); } } -async fn run() -> Result<(), Box<dyn std::error::Error>> { - log4rs::init_file("log4rs.yml", Default::default()).unwrap(); - +async fn run(root_logger: Logger) -> Result<(), Box<dyn std::error::Error>> { // Parse command line options let args = Args::from_args(); + // Set up signal handlers + let ctrl_c = tokio::task::spawn(tokio::signal::ctrl_c()); + #[cfg(unix)] + let (sighup, sigterm, sigquit) = ( + tokio::task::spawn(hangup()), + tokio::task::spawn(terminate()), + tokio::task::spawn(quit()), + ); + let mut file = File::open(&args.config_path)?; let mut toml = String::new(); file.read_to_string(&mut toml)?; @@ -107,14 +133,14 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> { if let Some(level) = args.wanted_level { if let Some(id) = &mut config.id { - info!("Upgrading master identity"); + info!(root_logger, "Upgrading master identity"); id.upgrade_level(level).expect("can upgrade level"); } if let Some(ids) = &mut config.ids { let len = ids.len(); for (i, id) in ids.iter_mut().enumerate() { - info!("Upgrading bot identity {}/{}", i + 1, len); + info!(root_logger, "Upgrading bot identity"; "current" => i + 1, "amount" => len); id.upgrade_level(level).expect("can upgrade level"); } } @@ -127,53 +153,101 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> { } if config.id.is_none() || config.ids.is_none() { - error!("Failed to find required identites, try running with `-g`"); + error!( + root_logger, + "Failed to find required identites, try running with `-g`" + ); return Ok(()); } + let local = args.local; let bot_args = config.merge(args); - info!("Starting PokeBot!"); - debug!("Received CLI arguments: {:?}", std::env::args()); + info!(root_logger, "Starting PokeBot!"); + debug!(root_logger, "Received CLI arguments"; "args" => ?std::env::args()); - if bot_args.local { + if local { let name = bot_args.names[0].clone(); - let id = bot_args.ids.expect("identies should exists")[0].clone(); - - let disconnect_cb = Box::new(move |_, _, _| {}); + let identity = bot_args.ids.expect("identies should exists")[0].clone(); let bot_args = MusicBotArgs { name, - name_index: 0, - id_index: 0, + master: None, local: true, address: bot_args.address.clone(), - id, + identity, channel: String::from("local"), verbose: bot_args.verbose, - disconnect_cb, + logger: root_logger, }; - MusicBot::new(bot_args).await.1.await; + MusicBot::spawn(bot_args).await; + + ctrl_c.await??; } else { let domain = bot_args.domain.clone(); let bind_address = bot_args.bind_address.clone(); - let (bot, fut) = MasterBot::new(bot_args).await; - - thread::spawn(|| { - let web_args = web_server::WebServerArgs { - domain, - bind_address, - bot, - }; - if let Err(e) = web_server::start(web_args) { - error!("Error in web server: {}", e); + let bot_name = bot_args.master_name.clone(); + let bot_logger = root_logger.new(o!("master" => bot_name.clone())); + let bot = MasterBot::spawn(bot_args, bot_logger).await; + + let web_args = web_server::WebServerArgs { + domain, + bind_address, + bot: bot.downgrade(), + }; + spawn_web_server(web_args, root_logger.new(o!("webserver" => bot_name))); + + #[cfg(unix)] + tokio::select! { + res = ctrl_c => { + res??; + info!(root_logger, "Received signal, shutting down"; "signal" => "SIGINT"); } - }); + _ = sigterm => { + info!(root_logger, "Received signal, shutting down"; "signal" => "SIGTERM"); + } + _ = sighup => { + info!(root_logger, "Received signal, shutting down"; "signal" => "SIGHUP"); + } + _ = sigquit => { + info!(root_logger, "Received signal, shutting down"; "signal" => "SIGQUIT"); + } + }; + + #[cfg(windows)] + ctrl_c.await??; - fut.await; - // Keep tokio running while the bot disconnects - tokio::time::delay_for(Duration::from_secs(1)).await; + bot.send(Quit(String::from("Stopping"))) + .await + .unwrap() + .unwrap(); } Ok(()) } + +pub fn spawn_web_server(args: web_server::WebServerArgs, logger: Logger) { + thread::spawn(move || { + if let Err(e) = web_server::start(args, logger.clone()) { + error!(logger, "Error in web server"; "error" => %e); + } + }); +} + +#[cfg(unix)] +pub async fn terminate() -> std::io::Result<()> { + signal(SignalKind::terminate())?.recv().await; + Ok(()) +} + +#[cfg(unix)] +pub async fn hangup() -> std::io::Result<()> { + signal(SignalKind::hangup())?.recv().await; + Ok(()) +} + +#[cfg(unix)] +pub async fn quit() -> std::io::Result<()> { + signal(SignalKind::quit())?.recv().await; + Ok(()) +} diff --git a/src/playlist.rs b/src/playlist.rs index 445f8a5..31fcfc0 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -1,29 +1,35 @@ use std::collections::VecDeque; -use log::info; +use slog::{info, Logger}; use crate::youtube_dl::AudioMetadata; pub struct Playlist { data: VecDeque<AudioMetadata>, + logger: Logger, } impl Playlist { - pub fn new() -> Self { + pub fn new(logger: Logger) -> Self { Self { data: VecDeque::new(), + logger, } } pub fn push(&mut self, data: AudioMetadata) { - info!("Adding {:?} to playlist", &data.title); + info!(self.logger, "Adding to playlist"; "title" => &data.title); self.data.push_front(data) } pub fn pop(&mut self) -> Option<AudioMetadata> { let res = self.data.pop_back(); - info!("Popping {:?} from playlist", res.as_ref().map(|r| &r.title)); + info!( + self.logger, + "Popping from playlist"; + "title" => res.as_ref().map(|r| &r.title) + ); res } @@ -45,6 +51,6 @@ impl Playlist { pub fn clear(&mut self) { self.data.clear(); - info!("Cleared playlist") + info!(self.logger, "Cleared playlist") } } diff --git a/src/teamspeak/mod.rs b/src/teamspeak/mod.rs index beb3f44..7a68d38 100644 --- a/src/teamspeak/mod.rs +++ b/src/teamspeak/mod.rs @@ -1,7 +1,5 @@ -use std::sync::{Arc, RwLock}; - use futures::stream::StreamExt; -use tokio::sync::mpsc::UnboundedSender; +use xtra::{Actor, Handler, WeakAddress}; use tsclientlib::data::exts::{M2BClientEditExt, M2BClientUpdateExt}; use tsclientlib::{ @@ -10,9 +8,9 @@ use tsclientlib::{ ChannelId, ClientId, ConnectOptions, DisconnectOptions, MessageTarget, OutCommandExt, Reason, }; -use log::{debug, error}; +use slog::{debug, error, info, trace, Logger}; -use crate::bot::{Message, MusicBotMessage}; +use crate::bot::{ChatMessage, MusicBotMessage}; mod bbcode; @@ -20,7 +18,8 @@ pub use bbcode::*; #[derive(Clone)] pub struct TeamSpeakConnection { - handle: SyncConnectionHandle, + handle: Option<SyncConnectionHandle>, + logger: Logger, } fn get_message(event: &Event) -> Option<MusicBotMessage> { @@ -31,7 +30,7 @@ fn get_message(event: &Event) -> Option<MusicBotMessage> { target, invoker: sender, message: msg, - } => Some(MusicBotMessage::TextMessage(Message { + } => Some(MusicBotMessage::TextMessage(ChatMessage { target: *target, invoker: sender.clone(), text: msg.clone(), @@ -86,50 +85,82 @@ fn get_message(event: &Event) -> Option<MusicBotMessage> { } impl TeamSpeakConnection { - pub async fn new( - tx: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, + pub async fn new(logger: Logger) -> Result<TeamSpeakConnection, tsclientlib::Error> { + Ok(TeamSpeakConnection { + handle: None, + logger, + }) + } + + pub fn connect_for_bot<T: Actor + Handler<MusicBotMessage>>( + &mut self, options: ConnectOptions, - ) -> Result<TeamSpeakConnection, tsclientlib::Error> { + bot: WeakAddress<T>, + ) -> Result<(), tsclientlib::Error> { + info!(self.logger, "Starting TeamSpeak connection"); + let conn = options.connect()?; - let conn = SyncConnection::from(conn); - let mut handle = conn.get_handle(); - - tokio::spawn(conn.for_each(move |i| { - let tx = tx.clone(); - async move { - match i { - Ok(SyncStreamItem::ConEvents(events)) => { + let mut conn = SyncConnection::from(conn); + let handle = conn.get_handle(); + self.handle = Some(handle); + + let ev_logger = self.logger.clone(); + tokio::spawn(async move { + while let Some(item) = conn.next().await { + use SyncStreamItem::*; + + match item { + Ok(ConEvents(events)) => { for event in &events { if let Some(msg) = get_message(event) { - let tx = tx.read().expect("RwLock was not poisoned"); - // Ignore the result because the receiver might get dropped first. - let _ = tx.send(msg); + tokio::spawn(bot.send(msg)); } } } - Err(e) => error!("Error occured during event reading: {}", e), - Ok(SyncStreamItem::DisconnectedTemporarily) => debug!("Temporary disconnect!"), - _ => (), + Err(e) => error!(ev_logger, "Error occured during event reading: {}", e), + Ok(DisconnectedTemporarily(r)) => { + debug!(ev_logger, "Temporary disconnect"; "reason" => ?r) + } + Ok(Audio(_)) => { + trace!(ev_logger, "Audio received"); + } + Ok(IdentityLevelIncreasing(_)) => { + trace!(ev_logger, "Identity level increasing"); + } + Ok(IdentityLevelIncreased) => { + trace!(ev_logger, "Identity level increased"); + } + Ok(NetworkStatsUpdated) => { + trace!(ev_logger, "Network stats updated"); + } } } - })); - - handle.wait_until_connected().await?; - - let mut chandle = handle.clone(); - chandle - .with_connection(|mut conn| { - conn.get_state() - .expect("is connected") - .server - .set_subscribed(true) - .send(&mut conn) - .unwrap() - }) - .await - .unwrap(); - - Ok(TeamSpeakConnection { handle }) + }); + + let mut handle = self.handle.clone(); + tokio::spawn(async move { + handle + .as_mut() + .expect("connect_for_bot was called") + .wait_until_connected() + .await + .unwrap(); + handle + .as_mut() + .expect("connect_for_bot was called") + .with_connection(|mut conn| { + conn.get_state() + .expect("can get state") + .server + .set_subscribed(true) + .send(&mut conn) + }) + .await + .and_then(|v| v) + .unwrap(); + }); + + Ok(()) } pub async fn send_audio_packet(&mut self, samples: &[u8]) { @@ -140,22 +171,25 @@ impl TeamSpeakConnection { data: samples, }); - self.handle - .with_connection(|conn| { - if let Err(e) = conn - .get_tsproto_client_mut() + if let Err(e) = self + .handle + .as_mut() + .expect("connect_for_bot was called") + .with_connection(move |conn| { + conn.get_tsproto_client_mut() .expect("can get tsproto client") .send_packet(packet) - { - error!("Failed to send voice packet: {}", e); - } }) .await - .unwrap(); + { + error!(self.logger, "Failed to send voice packet: {}", e); + } } pub async fn channel_of_user(&mut self, id: ClientId) -> Option<ChannelId> { self.handle + .as_mut() + .expect("connect_for_bot was called") .with_connection(move |conn| { conn.get_state() .expect("can get state") @@ -164,30 +198,28 @@ impl TeamSpeakConnection { .map(|c| c.channel) }) .await - .unwrap() + .map_err(|e| error!(self.logger, "Failed to get channel of user"; "error" => %e)) + .ok() + .and_then(|v| v) } pub async fn channel_path_of_user(&mut self, id: ClientId) -> Option<String> { self.handle + .as_mut() + .expect("connect_for_bot was called") .with_connection(move |conn| { let state = conn.get_state().expect("can get state"); let channel_id = state.clients.get(&id)?.channel; - let mut channel = state - .channels - .get(&channel_id) - .expect("can find user channel"); + let mut channel = state.channels.get(&channel_id)?; let mut names = vec![&channel.name[..]]; // Channel 0 is the root channel while channel.parent != ChannelId(0) { names.push("/"); - channel = state - .channels - .get(&channel.parent) - .expect("can find user channel"); + channel = state.channels.get(&channel.parent)?; names.push(&channel.name); } @@ -199,11 +231,15 @@ impl TeamSpeakConnection { Some(path) }) .await - .unwrap() + .map_err(|e| error!(self.logger, "Failed to get channel path of user"; "error" => %e)) + .ok() + .and_then(|v| v) } - pub async fn my_channel(&mut self) -> ChannelId { + pub async fn current_channel(&mut self) -> Option<ChannelId> { self.handle + .as_mut() + .expect("connect_for_bot was called") .with_connection(move |conn| { let state = conn.get_state().expect("can get state"); state @@ -213,11 +249,14 @@ impl TeamSpeakConnection { .channel }) .await - .unwrap() + .map_err(|e| error!(self.logger, "Failed to get channel"; "error" => %e)) + .ok() } pub async fn my_id(&mut self) -> ClientId { self.handle + .as_mut() + .expect("connect_for_bot was called") .with_connection(move |conn| conn.get_state().expect("can get state").own_client) .await .unwrap() @@ -225,6 +264,8 @@ impl TeamSpeakConnection { pub async fn user_count(&mut self, channel: ChannelId) -> u32 { self.handle + .as_mut() + .expect("connect_for_bot was called") .with_connection(move |conn| { let state = conn.get_state().expect("can get state"); let mut count = 0; @@ -241,88 +282,90 @@ impl TeamSpeakConnection { } pub async fn set_nickname(&mut self, name: String) { - self.handle + if let Err(e) = self + .handle + .as_mut() + .expect("connect_for_bot was called") .with_connection(move |mut conn| { conn.get_state() .expect("can get state") .client_update() .set_name(&name) .send(&mut conn) - .map_err(|e| error!("Failed to set nickname: {}", e)) }) .await - .unwrap() - .unwrap(); + .and_then(|v| v) + { + error!(self.logger, "Failed to set nickname: {}", e); + } } pub async fn set_description(&mut self, desc: String) { - self.handle + if let Err(e) = self + .handle + .as_mut() + .expect("connect_for_bot was called") .with_connection(move |mut conn| { let state = conn.get_state().expect("can get state"); - let _ = state + state .clients .get(&state.own_client) .expect("can get myself") .edit() .set_description(&desc) .send(&mut conn) - .map_err(|e| error!("Failed to change description: {}", e)); }) .await - .unwrap() + .and_then(|v| v) + { + error!(self.logger, "Failed to change description: {}", e); + } } pub async fn send_message_to_channel(&mut self, text: String) { - self.handle + if let Err(e) = self + .handle + .as_mut() + .expect("connect_for_bot was called") .with_connection(move |mut conn| { - let _ = conn - .get_state() + conn.get_state() .expect("can get state") .send_message(MessageTarget::Channel, &text) .send(&mut conn) - .map_err(|e| error!("Failed to send message: {}", e)); }) .await - .unwrap() + .and_then(|v| v) + { + error!(self.logger, "Failed to send message: {}", e); + } } pub async fn send_message_to_user(&mut self, client: ClientId, text: String) { - self.handle + if let Err(e) = self + .handle + .as_mut() + .expect("connect_for_bot was called") .with_connection(move |mut conn| { - let _ = conn - .get_state() + conn.get_state() .expect("can get state") .send_message(MessageTarget::Client(client), &text) .send(&mut conn) - .map_err(|e| error!("Failed to send message: {}", e)); }) .await - .unwrap() + .and_then(|v| v) + { + error!(self.logger, "Failed to send message: {}", e); + } } - pub async fn subscribe(&mut self, id: ChannelId) { - self.handle - .with_connection(move |mut conn| { - let channel = match conn.get_state().expect("can get state").channels.get(&id) { - Some(c) => c, - None => { - error!("Failed to find channel to subscribe to"); - return; - } - }; - - if let Err(e) = channel.set_subscribed(true).send(&mut conn) { - error!("Failed to send subscribe packet: {}", e); - } - }) - .await - .unwrap() - } - - pub async fn disconnect(&mut self, reason: &str) { + pub async fn disconnect(&mut self, reason: &str) -> Result<(), tsclientlib::Error> { let opt = DisconnectOptions::new() .reason(Reason::Clientdisconnect) .message(reason); - self.handle.disconnect(opt).await.unwrap(); + self.handle + .as_mut() + .expect("connect_for_bot was called") + .disconnect(opt) + .await } } diff --git a/src/web_server.rs b/src/web_server.rs index d731fae..8e5d446 100644 --- a/src/web_server.rs +++ b/src/web_server.rs @@ -1,38 +1,38 @@ -use std::sync::Arc; use std::time::Duration; -use actix::{Actor, Addr}; -use actix_web::{get, middleware::Logger, post, web, App, HttpServer, Responder}; -use askama::Template; -use askama_actix::TemplateIntoResponse; +use actix_slog::StructuredLogger; +use actix_web::{get, post, web, App, HttpServer, Responder}; +use askama_actix::{Template, TemplateIntoResponse}; use serde::{Deserialize, Serialize}; +use slog::Logger; +use xtra::WeakAddress; use crate::bot::MasterBot; use crate::youtube_dl::AudioMetadata; mod api; -mod bot_executor; +mod bot_data; mod default; mod front_end_cookie; mod tmtu; -pub use bot_executor::*; +pub use bot_data::*; use front_end_cookie::FrontEnd; pub struct WebServerArgs { pub domain: String, pub bind_address: String, - pub bot: Arc<MasterBot>, + pub bot: WeakAddress<MasterBot>, } #[actix_rt::main] -pub async fn start(args: WebServerArgs) -> std::io::Result<()> { - let cbot = args.bot.clone(); - let bot_addr: Addr<BotExecutor> = BotExecutor(cbot.clone()).start(); +pub async fn start(args: WebServerArgs, logger: Logger) -> std::io::Result<()> { + let bot = args.bot; + let bind_address = args.bind_address; HttpServer::new(move || { App::new() - .data(bot_addr.clone()) - .wrap(Logger::default()) + .data(bot.clone()) + .wrap(StructuredLogger::new(logger.clone())) .service(index) .service(get_bot) .service(post_front_end) @@ -44,12 +44,10 @@ pub async fn start(args: WebServerArgs) -> std::io::Result<()> { .service(web::scope("/docs").service(get_api_docs)) .service(actix_files::Files::new("/static", "web_server/static/")) }) - .bind(args.bind_address)? + .bind(bind_address)? .run() .await?; - args.bot.quit(String::from("Stopping")); - Ok(()) } @@ -75,7 +73,7 @@ pub struct BotData { } #[get("/")] -async fn index(bot: web::Data<Addr<BotExecutor>>, front: FrontEnd) -> impl Responder { +async fn index(bot: web::Data<WeakAddress<MasterBot>>, front: FrontEnd) -> impl Responder { match front { FrontEnd::Default => default::index(bot).await, FrontEnd::Tmtu => tmtu::index(bot).await, @@ -84,7 +82,7 @@ async fn index(bot: web::Data<Addr<BotExecutor>>, front: FrontEnd) -> impl Respo #[get("/bot/{name}")] async fn get_bot( - bot: web::Data<Addr<BotExecutor>>, + bot: web::Data<WeakAddress<MasterBot>>, name: web::Path<String>, front: FrontEnd, ) -> impl Responder { diff --git a/src/web_server/api.rs b/src/web_server/api.rs index 4deedad..b1d50c4 100644 --- a/src/web_server/api.rs +++ b/src/web_server/api.rs @@ -1,22 +1,23 @@ -use actix::Addr; use actix_web::{get, web, HttpResponse, Responder, ResponseError}; use derive_more::Display; use serde::Serialize; +use xtra::WeakAddress; -use crate::web_server::{BotDataListRequest, BotDataRequest, BotExecutor}; +use crate::web_server::{BotDataListRequest, BotDataRequest}; +use crate::MasterBot; #[get("/bots")] -pub async fn get_bot_list(bot: web::Data<Addr<BotExecutor>>) -> impl Responder { - let bot_datas = match bot.send(BotDataListRequest).await.unwrap() { - Ok(data) => data, - Err(_) => Vec::with_capacity(0), - }; +pub async fn get_bot_list(bot: web::Data<WeakAddress<MasterBot>>) -> impl Responder { + let bot_datas = bot.send(BotDataListRequest).await.unwrap(); web::Json(bot_datas) } #[get("/bots/{name}")] -pub async fn get_bot(bot: web::Data<Addr<BotExecutor>>, name: web::Path<String>) -> impl Responder { +pub async fn get_bot( + bot: web::Data<WeakAddress<MasterBot>>, + name: web::Path<String>, +) -> impl Responder { if let Some(bot_data) = bot.send(BotDataRequest(name.into_inner())).await.unwrap() { Ok(web::Json(bot_data)) } else { diff --git a/src/web_server/bot_data.rs b/src/web_server/bot_data.rs new file mode 100644 index 0000000..af0c5e1 --- /dev/null +++ b/src/web_server/bot_data.rs @@ -0,0 +1,47 @@ +use async_trait::async_trait; + +use xtra::{Context, Handler, Message}; + +use crate::bot::MasterBot; +use crate::web_server::BotData; + +pub struct BotNameListRequest; + +impl Message for BotNameListRequest { + type Result = Vec<String>; +} + +#[async_trait] +impl Handler<BotNameListRequest> for MasterBot { + async fn handle(&mut self, _: BotNameListRequest, _: &mut Context<Self>) -> Vec<String> { + self.bot_names() + } +} + +pub struct BotDataListRequest; + +impl Message for BotDataListRequest { + type Result = Vec<BotData>; +} + +#[async_trait] +impl Handler<BotDataListRequest> for MasterBot { + async fn handle(&mut self, _: BotDataListRequest, _: &mut Context<Self>) -> Vec<BotData> { + self.bot_datas().await + } +} + +pub struct BotDataRequest(pub String); + +impl Message for BotDataRequest { + type Result = Option<BotData>; +} + +#[async_trait] +impl Handler<BotDataRequest> for MasterBot { + async fn handle(&mut self, r: BotDataRequest, _: &mut Context<Self>) -> Option<BotData> { + let name = r.0; + + self.bot_data(name).await + } +} diff --git a/src/web_server/bot_executor.rs b/src/web_server/bot_executor.rs deleted file mode 100644 index 0d3e7b7..0000000 --- a/src/web_server/bot_executor.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::sync::Arc; - -use actix::{Actor, Context, Handler, Message}; - -use crate::bot::MasterBot; -use crate::web_server::BotData; - -pub struct BotExecutor(pub Arc<MasterBot>); - -impl Actor for BotExecutor { - type Context = Context<Self>; -} - -pub struct BotNameListRequest; - -impl Message for BotNameListRequest { - // A plain Vec does not work for some reason - type Result = Result<Vec<String>, ()>; -} - -impl Handler<BotNameListRequest> for BotExecutor { - type Result = Result<Vec<String>, ()>; - - fn handle(&mut self, _: BotNameListRequest, _: &mut Self::Context) -> Self::Result { - let bot = &self.0; - - Ok(bot.bot_names()) - } -} - -pub struct BotDataListRequest; - -impl Message for BotDataListRequest { - // A plain Vec does not work for some reason - type Result = Result<Vec<BotData>, ()>; -} - -impl Handler<BotDataListRequest> for BotExecutor { - type Result = Result<Vec<BotData>, ()>; - - fn handle(&mut self, _: BotDataListRequest, _: &mut Self::Context) -> Self::Result { - let bot = &self.0; - - Ok(bot.bot_datas()) - } -} - -pub struct BotDataRequest(pub String); - -impl Message for BotDataRequest { - type Result = Option<BotData>; -} - -impl Handler<BotDataRequest> for BotExecutor { - type Result = Option<BotData>; - - fn handle(&mut self, r: BotDataRequest, _: &mut Self::Context) -> Self::Result { - let name = r.0; - let bot = &self.0; - - bot.bot_data(name) - } -} diff --git a/src/web_server/default.rs b/src/web_server/default.rs index 542dade..6b15784 100644 --- a/src/web_server/default.rs +++ b/src/web_server/default.rs @@ -1,9 +1,9 @@ -use actix::Addr; use actix_web::{http::header, web, Error, HttpResponse}; -use askama::Template; -use askama_actix::TemplateIntoResponse; +use askama_actix::{Template, TemplateIntoResponse}; +use xtra::WeakAddress; -use crate::web_server::{filters, BotData, BotDataRequest, BotExecutor, BotNameListRequest}; +use crate::web_server::{filters, BotData, BotDataRequest, BotNameListRequest}; +use crate::MasterBot; #[derive(Template)] #[template(path = "index.htm")] @@ -12,8 +12,8 @@ struct OverviewTemplate<'a> { bot: Option<&'a BotData>, } -pub async fn index(bot: web::Data<Addr<BotExecutor>>) -> Result<HttpResponse, Error> { - let bot_names = bot.send(BotNameListRequest).await.unwrap().unwrap(); +pub async fn index(bot: web::Data<WeakAddress<MasterBot>>) -> Result<HttpResponse, Error> { + let bot_names = bot.send(BotNameListRequest).await.unwrap(); OverviewTemplate { bot_names: &bot_names, @@ -23,10 +23,10 @@ pub async fn index(bot: web::Data<Addr<BotExecutor>>) -> Result<HttpResponse, Er } pub async fn get_bot( - bot: web::Data<Addr<BotExecutor>>, + bot: web::Data<WeakAddress<MasterBot>>, name: String, ) -> Result<HttpResponse, Error> { - let bot_names = bot.send(BotNameListRequest).await.unwrap().unwrap(); + let bot_names = bot.send(BotNameListRequest).await.unwrap(); if let Some(bot) = bot.send(BotDataRequest(name)).await.unwrap() { OverviewTemplate { @@ -35,7 +35,6 @@ pub async fn get_bot( } .into_response() } else { - // TODO to 404 or not to 404 Ok(HttpResponse::Found().header(header::LOCATION, "/").finish()) } } diff --git a/src/web_server/tmtu.rs b/src/web_server/tmtu.rs index 33a14af..e9eea98 100644 --- a/src/web_server/tmtu.rs +++ b/src/web_server/tmtu.rs @@ -1,9 +1,9 @@ -use actix::Addr; use actix_web::{http::header, web, Error, HttpResponse}; -use askama::Template; -use askama_actix::TemplateIntoResponse; +use askama_actix::{Template, TemplateIntoResponse}; +use xtra::WeakAddress; -use crate::web_server::{filters, BotData, BotDataRequest, BotExecutor, BotNameListRequest}; +use crate::web_server::{filters, BotData, BotDataRequest, BotNameListRequest}; +use crate::MasterBot; #[derive(Template)] #[template(path = "tmtu/index.htm")] @@ -12,8 +12,8 @@ struct TmtuTemplate { bot: Option<BotData>, } -pub async fn index(bot: web::Data<Addr<BotExecutor>>) -> Result<HttpResponse, Error> { - let bot_names = bot.send(BotNameListRequest).await.unwrap().unwrap(); +pub async fn index(bot: web::Data<WeakAddress<MasterBot>>) -> Result<HttpResponse, Error> { + let bot_names = bot.send(BotNameListRequest).await.unwrap(); TmtuTemplate { bot_names, @@ -23,10 +23,10 @@ pub async fn index(bot: web::Data<Addr<BotExecutor>>) -> Result<HttpResponse, Er } pub async fn get_bot( - bot: web::Data<Addr<BotExecutor>>, + bot: web::Data<WeakAddress<MasterBot>>, name: String, ) -> Result<HttpResponse, Error> { - let bot_names = bot.send(BotNameListRequest).await.unwrap().unwrap(); + let bot_names = bot.send(BotNameListRequest).await.unwrap(); if let Some(bot) = bot.send(BotDataRequest(name)).await.unwrap() { TmtuTemplate { @@ -35,7 +35,6 @@ pub async fn get_bot( } .into_response() } else { - // TODO to 404 or not to 404 Ok(HttpResponse::Found().header(header::LOCATION, "/").finish()) } } diff --git a/src/youtube_dl.rs b/src/youtube_dl.rs index 496d0b4..ea10107 100644 --- a/src/youtube_dl.rs +++ b/src/youtube_dl.rs @@ -5,7 +5,7 @@ use tokio::process::Command; use serde::{Deserialize, Serialize}; -use log::debug; +use slog::{debug, Logger}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct AudioMetadata { @@ -28,13 +28,16 @@ where Ok(dur.map(Duration::from_secs_f64)) } -pub async fn get_audio_download_from_url(uri: String) -> Result<AudioMetadata, String> { +pub async fn get_audio_download_from_url( + url: String, + logger: &Logger, +) -> Result<AudioMetadata, String> { //youtube-dl sometimes just fails, so we give it a second try - let ytdl_output = match run_youtube_dl(&uri).await { + let ytdl_output = match run_youtube_dl(&url, &logger).await { Ok(o) => o, Err(e) => { if e.contains("Unable to extract video data") { - run_youtube_dl(&uri).await? + run_youtube_dl(&url, &logger).await? } else { return Err(e); } @@ -46,14 +49,14 @@ pub async fn get_audio_download_from_url(uri: String) -> Result<AudioMetadata, S Ok(output) } -async fn run_youtube_dl(url: &str) -> Result<String, String> { +async fn run_youtube_dl(url: &str, logger: &Logger) -> Result<String, String> { let ytdl_args = ["--no-playlist", "-f", "bestaudio/best", "-j", &url]; let mut cmd = Command::new("youtube-dl"); cmd.args(&ytdl_args); cmd.stdin(Stdio::null()); - debug!("yt-dl command: {:?}", cmd); + debug!(logger, "running yt-dl"; "command" => ?cmd); let ytdl_output = cmd.output().await.unwrap(); if !ytdl_output.status.success() { |
