diff options
| author | Felix Kaaman <tmtu@tmtu.ee> | 2020-02-22 23:27:01 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-02-22 23:27:01 +0100 |
| commit | 763b8c6579f3ae571f7287c72b9fb4f8b6e89349 (patch) | |
| tree | bc55a0e79107a93bc3e605a0cae32926dc4c52fc | |
| parent | 2792ba9c8a7120a91b3bd2c6075e737690e73405 (diff) | |
| parent | 326cfa543c6263818aad7dec4a869bc8139ec14c (diff) | |
| download | pokebot-763b8c6579f3ae571f7287c72b9fb4f8b6e89349.tar.gz pokebot-763b8c6579f3ae571f7287c72b9fb4f8b6e89349.zip | |
Merge pull request #33 from Mavulp/webserver
Webserver
| -rw-r--r-- | Cargo.lock | 746 | ||||
| -rw-r--r-- | Cargo.toml | 6 | ||||
| -rw-r--r-- | askama.toml | 3 | ||||
| -rw-r--r-- | config.toml.example | 4 | ||||
| -rw-r--r-- | log4rs.yml | 4 | ||||
| -rw-r--r-- | src/audio_player.rs | 57 | ||||
| -rw-r--r-- | src/bot/master.rs | 83 | ||||
| -rw-r--r-- | src/bot/music.rs | 105 | ||||
| -rw-r--r-- | src/main.rs | 21 | ||||
| -rw-r--r-- | src/playlist.rs | 10 | ||||
| -rw-r--r-- | src/teamspeak/bbcode.rs | 6 | ||||
| -rw-r--r-- | src/teamspeak/mod.rs | 6 | ||||
| -rw-r--r-- | src/web_server.rs | 122 | ||||
| -rw-r--r-- | src/web_server/api.rs | 48 | ||||
| -rw-r--r-- | src/web_server/bot_executor.rs | 63 | ||||
| -rw-r--r-- | src/web_server/default.rs | 24 | ||||
| -rw-r--r-- | src/web_server/front_end_cookie.rs | 60 | ||||
| -rw-r--r-- | src/web_server/tmtu.rs | 41 | ||||
| -rw-r--r-- | src/youtube_dl.rs | 19 | ||||
| -rw-r--r-- | web_server/static/fonts/.gitkeep | 0 | ||||
| -rw-r--r-- | web_server/static/style.css | 63 | ||||
| -rw-r--r-- | web_server/templates/base.htm | 15 | ||||
| -rw-r--r-- | web_server/templates/docs/api.htm | 126 | ||||
| -rw-r--r-- | web_server/templates/index.htm | 36 | ||||
| -rw-r--r-- | web_server/templates/song.htm | 7 | ||||
| -rw-r--r-- | web_server/templates/tmtu/index.htm | 145 |
26 files changed, 1734 insertions, 86 deletions
@@ -1,6 +1,319 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] +name = "actix" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4af87564ff659dee8f9981540cac9418c45e910c8072fdedd643a262a38fcaf" +dependencies = [ + "actix-http", + "actix-rt", + "actix_derive", + "bitflags", + "bytes 0.5.3", + "crossbeam-channel", + "derive_more 0.99.2", + "futures 0.3.1", + "lazy_static", + "log", + "parking_lot 0.10.0", + "pin-project", + "smallvec 1.1.0", + "tokio 0.2.7", + "tokio-util", + "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 0.5.3", + "futures-core", + "futures-sink", + "log", + "tokio 0.2.7", + "tokio-util", +] + +[[package]] +name = "actix-connect" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more 0.99.2", + "either", + "futures 0.3.1", + "http 0.2.0", + "log", + "trust-dns-proto 0.18.0-alpha.2", + "trust-dns-resolver 0.18.0-alpha.2", +] + +[[package]] +name = "actix-files" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301482841d3d74483a446ead63cb7d362e187d2c8b603f13d91995621ea53c46" +dependencies = [ + "actix-http", + "actix-service", + "actix-web", + "bitflags", + "bytes 0.5.3", + "derive_more 0.99.2", + "futures 0.3.1", + "log", + "mime", + "mime_guess", + "percent-encoding 2.1.0", + "v_htmlescape", +] + +[[package]] +name = "actix-http" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c16664cc4fdea8030837ad5a845eb231fb93fc3c5c171edfefb52fad92ce9019" +dependencies = [ + "actix-codec", + "actix-connect", + "actix-rt", + "actix-service", + "actix-threadpool", + "actix-utils", + "base64 0.11.0", + "bitflags", + "brotli2", + "bytes 0.5.3", + "chrono", + "copyless", + "derive_more 0.99.2", + "either", + "encoding_rs", + "failure", + "flate2", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "h2 0.2.1", + "http 0.2.0", + "httparse", + "indexmap", + "language-tags", + "lazy_static", + "log", + "mime", + "percent-encoding 2.1.0", + "pin-project", + "rand 0.7.3", + "regex", + "serde", + "serde_json", + "serde_urlencoded 0.6.1", + "sha1", + "slab", + "time", +] + +[[package]] +name = "actix-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21705adc76bbe4bc98434890e73a89cd00c6015e5704a60bb6eea6c3b72316b6" +dependencies = [ + "quote 1.0.2", + "syn 1.0.13", +] + +[[package]] +name = "actix-router" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7a10ca4d94e8c8e7a87c5173aba1b97ba9a6563ca02b0e1cd23531093d3ec8" +dependencies = [ + "bytestring", + "http 0.2.0", + "log", + "regex", + "serde", +] + +[[package]] +name = "actix-rt" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6a0a55507046441a496b2f0d26a84a65e67c8cafffe279072412f624b5fb6d" +dependencies = [ + "actix-macros", + "actix-threadpool", + "copyless", + "futures 0.3.1", + "tokio 0.2.7", +] + +[[package]] +name = "actix-server" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d3455eaac03ca3e49d7b822eb35c884b861f715627254ccbe4309d08f1841a" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "futures 0.3.1", + "log", + "mio", + "mio-uds", + "net2", + "num_cpus", + "slab", +] + +[[package]] +name = "actix-service" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e4fc95dfa7e24171b2d0bb46b85f8ab0e8499e4e3caec691fc4ea65c287564" +dependencies = [ + "futures-util", + "pin-project", +] + +[[package]] +name = "actix-testing" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48494745b72d0ea8ff0cf874aaf9b622a3ee03d7081ee0c04edea4f26d32c911" +dependencies = [ + "actix-macros", + "actix-rt", + "actix-server", + "actix-service", + "futures 0.3.1", + "log", + "net2", +] + +[[package]] +name = "actix-threadpool" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4082192601de5f303013709ff84d81ca6a1bc4af7fb24f367a500a23c6e84e" +dependencies = [ + "derive_more 0.99.2", + "futures-channel", + "lazy_static", + "log", + "num_cpus", + "parking_lot 0.10.0", + "threadpool", +] + +[[package]] +name = "actix-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e5b4faaf105e9a6d389c606c298dcdb033061b00d532af9df56ff3a54995a8" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more 0.99.2", + "either", + "futures 0.3.1", + "log", +] + +[[package]] +name = "actix-utils" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf8f5631bf01adec2267808f00e228b761c60c0584cc9fa0b5364f41d147f4e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "bitflags", + "bytes 0.5.3", + "either", + "futures 0.3.1", + "log", + "pin-project", + "slab", +] + +[[package]] +name = "actix-web" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3158e822461040822f0dbf1735b9c2ce1f95f93b651d7a7aded00b1efbb1f635" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-testing", + "actix-threadpool", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "awc", + "bytes 0.5.3", + "derive_more 0.99.2", + "encoding_rs", + "futures 0.3.1", + "fxhash", + "log", + "mime", + "net2", + "pin-project", + "regex", + "serde", + "serde_json", + "serde_urlencoded 0.6.1", + "time", + "url 2.1.0", +] + +[[package]] +name = "actix-web-codegen" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0878b30e62623770a4713a6338329fd0119703bafc211d3e4144f4d4a7bdd5" +dependencies = [ + "proc-macro2 1.0.7", + "quote 1.0.2", + "syn 1.0.13", +] + +[[package]] +name = "actix_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95aceadaf327f18f0df5962fedc1bde2f870566a0b9f65c89508a3b1f79334c" +dependencies = [ + "proc-macro2 1.0.7", + "quote 1.0.2", + "syn 1.0.13", +] + +[[package]] name = "adler32" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -99,6 +412,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" [[package]] +name = "askama" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a1fb9e41eb366cbcd267da2094be5b7e62fdbca9f82091e7503e80f885050d" +dependencies = [ + "actix-web", + "askama_derive", + "askama_escape", + "askama_shared", + "bytes 0.5.3", + "futures 0.3.1", + "mime", + "mime_guess", +] + +[[package]] +name = "askama_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1012c270085fa35ece6a48a569544fde85b6d9ee41074c7b706cc912a03f939" +dependencies = [ + "askama_shared", + "nom 5.1.0", + "proc-macro2 1.0.7", + "quote 1.0.2", + "syn 1.0.13", +] + +[[package]] +name = "askama_escape" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a577aeba5fec1aafb9f195d98cfcc38a78b588e4ebf9b15f62ca1c7aa33795a" + +[[package]] +name = "askama_shared" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee517f4e33c27b129928e71d8a044d54c513e72e0b72ec5c4f5f1823e9de353" +dependencies = [ + "askama_escape", + "humansize", + "num-traits", + "serde", + "toml", +] + +[[package]] +name = "async-trait" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8df72488e87761e772f14ae0c2480396810e51b2c2ade912f97f0f7e5b95e3c" +dependencies = [ + "proc-macro2 1.0.7", + "quote 1.0.2", + "syn 1.0.13", +] + +[[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -116,6 +488,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" [[package]] +name = "awc" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7601d4d1d7ef2335d6597a41b5fe069f6ab799b85f53565ab390e7b7065aac5" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-service", + "base64 0.11.0", + "bytes 0.5.3", + "derive_more 0.99.2", + "futures-core", + "log", + "mime", + "percent-encoding 2.1.0", + "rand 0.7.3", + "serde", + "serde_json", + "serde_urlencoded 0.6.1", +] + +[[package]] name = "backtrace" version = "0.3.40" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -147,6 +542,12 @@ dependencies = [ ] [[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] name = "bit-vec" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -179,6 +580,26 @@ dependencies = [ ] [[package]] +name = "brotli-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "brotli2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" +dependencies = [ + "brotli-sys", + "libc", +] + +[[package]] name = "bstr" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -226,6 +647,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10004c15deb332055f7a4a208190aed362cf9a7c2f6ab70a305fba50e1105f38" [[package]] +name = "bytestring" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc267467f58ef6cc8874064c62a0423eb0d099362c8a23edd1c6d044f46eead4" +dependencies = [ + "bytes 0.5.3", +] + +[[package]] name = "c2-chacha" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -346,6 +776,12 @@ dependencies = [ ] [[package]] +name = "copyless" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff9c56c9fb2a49c05ef0e431485a22400af20d33226dc0764d891d09e724127" + +[[package]] name = "core-foundation" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -371,6 +807,15 @@ dependencies = [ ] [[package]] +name = "crossbeam-channel" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acec9a3b0b3559f15aee4f90746c4e5e293b701c0f7d3925d24e01645267b68c" +dependencies = [ + "crossbeam-utils 0.7.0", +] + +[[package]] name = "crossbeam-deque" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -501,6 +946,17 @@ dependencies = [ ] [[package]] +name = "derive_more" +version = "0.99.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2159be042979966de68315bce7034bb000c775f22e3e834e1c52ff78f041cae8" +dependencies = [ + "proc-macro2 1.0.7", + "quote 1.0.2", + "syn 1.0.13", +] + +[[package]] name = "digest" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -576,6 +1032,18 @@ dependencies = [ ] [[package]] +name = "enum-as-inner" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900a6c7fbe523f4c2884eaf26b57b81bb69b6810a01a236390a7ac021d09492e" +dependencies = [ + "heck", + "proc-macro2 1.0.7", + "quote 1.0.2", + "syn 1.0.13", +] + +[[package]] name = "error-chain" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -854,6 +1322,15 @@ dependencies = [ ] [[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] name = "generic-array" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1067,7 +1544,7 @@ dependencies = [ "bytes 0.4.12", "fnv", "futures 0.1.29", - "http", + "http 0.1.21", "indexmap", "log", "slab", @@ -1076,6 +1553,25 @@ dependencies = [ ] [[package]] +name = "h2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9433d71e471c1736fd5a61b671fc0b148d7a2992f666c958d03cd8feb3b88d1" +dependencies = [ + "bytes 0.5.3", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.0", + "indexmap", + "log", + "slab", + "tokio 0.2.7", + "tokio-util", +] + +[[package]] name = "heck" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1115,6 +1611,17 @@ dependencies = [ ] [[package]] +name = "http" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b" +dependencies = [ + "bytes 0.5.3", + "fnv", + "itoa", +] + +[[package]] name = "http-body" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1122,7 +1629,7 @@ checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" dependencies = [ "bytes 0.4.12", "futures 0.1.29", - "http", + "http 0.1.21", "tokio-buf", ] @@ -1133,6 +1640,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" [[package]] +name = "humansize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" + +[[package]] name = "humantime" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1156,8 +1669,8 @@ dependencies = [ "bytes 0.4.12", "futures 0.1.29", "futures-cpupool", - "h2", - "http", + "h2 0.1.26", + "http 0.1.21", "http-body", "httparse", "iovec", @@ -1277,6 +1790,12 @@ dependencies = [ ] [[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + +[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1528,6 +2047,16 @@ dependencies = [ ] [[package]] +name = "nom" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c433f4d505fe6ce7ff78523d2fa13a0b9f2690e181fc26168bcbe5ccc5d14e07" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] name = "num-bigint" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1687,6 +2216,16 @@ dependencies = [ ] [[package]] +name = "parking_lot" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e98c49ab0b7ce5b222f2cc9193fc4efe11c6d0bd4f648e374684a6857b1cfc" +dependencies = [ + "lock_api 0.3.3", + "parking_lot_core 0.7.0", +] + +[[package]] name = "parking_lot_core" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1727,6 +2266,20 @@ dependencies = [ ] [[package]] +name = "parking_lot_core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7582838484df45743c8434fbff785e8edf260c28748353d44bc0da32e0ceabf1" +dependencies = [ + "cfg-if", + "cloudabi", + "libc", + "redox_syscall", + "smallvec 1.1.0", + "winapi 0.3.8", +] + +[[package]] name = "paste" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1761,6 +2314,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] +name = "pin-project" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" +dependencies = [ + "proc-macro2 1.0.7", + "quote 1.0.2", + "syn 1.0.13", +] + +[[package]] name = "pin-project-lite" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1782,7 +2355,13 @@ checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" name = "pokebot" version = "0.1.1" dependencies = [ + "actix", + "actix-files", + "actix-rt", + "actix-web", + "askama", "byte-slice-cast", + "derive_more 0.99.2", "futures 0.1.29", "futures-preview", "futures-util", @@ -2155,14 +2734,14 @@ version = "0.9.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f88643aea3c1343c804950d7bf983bd2067f5ab59db6d613a08e05572f2714ab" dependencies = [ - "base64", + "base64 0.10.1", "bytes 0.4.12", "cookie", "cookie_store", "encoding_rs", "flate2", "futures 0.1.29", - "http", + "http 0.1.21", "hyper", "hyper-tls", "log", @@ -2171,7 +2750,7 @@ dependencies = [ "native-tls", "serde", "serde_json", - "serde_urlencoded", + "serde_urlencoded 0.5.5", "time", "tokio 0.1.22", "tokio-executor", @@ -2199,7 +2778,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca4eaef519b494d1f2848fc602d18816fed808a981aedf4f1f00ceb7c9d32cf" dependencies = [ - "base64", + "base64 0.10.1", "blake2b_simd", "crossbeam-utils 0.6.6", ] @@ -2337,6 +2916,18 @@ dependencies = [ ] [[package]] +name = "serde_urlencoded" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" +dependencies = [ + "dtoa", + "itoa", + "serde", + "url 2.1.0", +] + +[[package]] name = "serde_yaml" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2349,6 +2940,12 @@ dependencies = [ ] [[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] name = "signal-hook" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2567,7 +3164,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58898aa9e9462043aa48c62021ba284a6906d210bde13602e65be8329c364d33" dependencies = [ - "nom", + "nom 4.2.3", "proc-macro2 0.4.30", "quote 0.6.13", "syn 0.15.44", @@ -2633,6 +3230,15 @@ dependencies = [ ] [[package]] +name = "threadpool" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865" +dependencies = [ + "num_cpus", +] + +[[package]] name = "time" version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2675,11 +3281,17 @@ checksum = "e61fe9bd8b36108a3c679bb6de916e39595a7a7f18c5e314c0014bb1b6ba13f7" dependencies = [ "bytes 0.5.3", "fnv", + "futures-core", "iovec", "lazy_static", + "libc", "memchr", "mio", + "mio-uds", "pin-project-lite", + "signal-hook-registry", + "slab", + "winapi 0.3.8", ] [[package]] @@ -2888,6 +3500,20 @@ dependencies = [ ] [[package]] +name = "tokio-util" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" +dependencies = [ + "bytes 0.5.3", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio 0.2.7", +] + +[[package]] name = "toml" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2909,7 +3535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5559ebdf6c2368ddd11e20b11d6bbaf9e46deb803acd7815e93f5a7b4a6d2901" dependencies = [ "byteorder", - "enum-as-inner", + "enum-as-inner 0.2.1", "failure", "futures 0.1.29", "idna 0.1.5", @@ -2928,6 +3554,26 @@ dependencies = [ ] [[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 0.3.0", + "failure", + "futures 0.3.1", + "idna 0.2.0", + "lazy_static", + "log", + "rand 0.7.3", + "smallvec 1.1.0", + "socket2", + "tokio 0.2.7", + "url 2.1.0", +] + +[[package]] name = "trust-dns-resolver" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2944,7 +3590,26 @@ dependencies = [ "smallvec 0.6.13", "tokio 0.1.22", "tokio-executor", - "trust-dns-proto", + "trust-dns-proto 0.7.4", +] + +[[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 0.3.1", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "resolv-conf", + "smallvec 1.1.0", + "tokio 0.2.7", + "trust-dns-proto 0.18.0-alpha.2", ] [[package]] @@ -2967,9 +3632,9 @@ name = "ts-bookkeeping" version = "0.1.0" source = "git+https://github.com/ReSpeak/tsclientlib#7d82374088c11d3d3171ed07bfef16f4d9482b26" dependencies = [ - "base64", + "base64 0.10.1", "chrono", - "derive_more", + "derive_more 0.14.1", "failure", "heck", "itertools", @@ -2986,11 +3651,11 @@ name = "tsclientlib" version = "0.1.0" source = "git+https://github.com/ReSpeak/tsclientlib#7d82374088c11d3d3171ed07bfef16f4d9482b26" dependencies = [ - "base64", + "base64 0.10.1", "bytes 0.4.12", "chashmap", "chrono", - "derive_more", + "derive_more 0.14.1", "failure", "futures 0.1.29", "futures 0.3.1", @@ -3009,8 +3674,8 @@ dependencies = [ "tokio 0.1.22", "tokio 0.2.7", "tokio-threadpool", - "trust-dns-proto", - "trust-dns-resolver", + "trust-dns-proto 0.7.4", + "trust-dns-resolver 0.11.1", "ts-bookkeeping", "tsproto", "tsproto-packets", @@ -3024,13 +3689,13 @@ source = "git+https://github.com/ReSpeak/tsclientlib#7d82374088c11d3d3171ed07bfe dependencies = [ "aes", "arrayref", - "base64", + "base64 0.10.1", "bitflags", "byteorder", "bytes 0.4.12", "chrono", "curve25519-dalek", - "derive_more", + "derive_more 0.14.1", "eax", "failure", "flakebi-ring", @@ -3061,13 +3726,13 @@ version = "0.1.0" source = "git+https://github.com/ReSpeak/tsclientlib#7d82374088c11d3d3171ed07bfef16f4d9482b26" dependencies = [ "arrayref", - "base64", + "base64 0.10.1", "bitflags", "byteorder", "bytes 0.4.12", - "derive_more", + "derive_more 0.14.1", "failure", - "nom", + "nom 4.2.3", "num-derive", "num-traits", "rental", @@ -3080,7 +3745,7 @@ name = "tsproto-structs" version = "0.1.0" source = "git+https://github.com/ReSpeak/tsclientlib#7d82374088c11d3d3171ed07bfef16f4d9482b26" dependencies = [ - "base64", + "base64 0.10.1", "csv", "heck", "lazy_static", @@ -3095,7 +3760,7 @@ name = "tsproto-types" version = "0.1.0" source = "git+https://github.com/ReSpeak/tsclientlib#7d82374088c11d3d3171ed07bfef16f4d9482b26" dependencies = [ - "base64", + "base64 0.10.1", "bitflags", "chrono", "heck", @@ -3219,6 +3884,37 @@ dependencies = [ ] [[package]] +name = "v_escape" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6" +dependencies = [ + "v_escape_derive", +] + +[[package]] +name = "v_escape_derive" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae" +dependencies = [ + "nom 4.2.3", + "proc-macro2 1.0.7", + "quote 1.0.2", + "syn 1.0.13", +] + +[[package]] +name = "v_htmlescape" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41" +dependencies = [ + "cfg-if", + "v_escape", +] + +[[package]] name = "vcpkg" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3348,7 +4044,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" dependencies = [ - "nom", + "nom 4.2.3", ] [[package]] @@ -34,4 +34,10 @@ gstreamer-audio = "0.15.0" byte-slice-cast = "0.3.5" serde_json = "1.0.44" serde = "1.0.104" +actix = "0.9.0" +actix-rt = "1.0.0" +actix-web = "2.0.0" +actix-files = "0.2.1" +askama = { version = "0.9.0", features = ["with-actix-web"] } rand = { version = "0.7.3", features = ["small_rng"] } +derive_more = "0.99.2" diff --git a/askama.toml b/askama.toml new file mode 100644 index 0000000..fceb790 --- /dev/null +++ b/askama.toml @@ -0,0 +1,3 @@ +[general] +# Directories to search for templates, relative to the crate root. +dirs = ["web_server/templates"] diff --git a/config.toml.example b/config.toml.example index 8545dfe..50cec56 100644 --- a/config.toml.example +++ b/config.toml.example @@ -5,6 +5,10 @@ address = "localhost" # Channel for the master bot channel = "Lobby" +# Web server settings +domain = "localhost" +bind_address = "127.0.0.1:45538" + # Names for the music bots names = ["MusicBot"] @@ -23,3 +23,7 @@ root: loggers: tokio_reactor: level: warn + actix_web: + level: trace + actix_server: + level: debug diff --git a/src/audio_player.rs b/src/audio_player.rs index 9ed645d..d231c72 100644 --- a/src/audio_player.rs +++ b/src/audio_player.rs @@ -10,9 +10,11 @@ use gstreamer_audio::{StreamVolume, StreamVolumeFormat}; use crate::bot::{MusicBotMessage, State}; use glib::BoolError; use log::{debug, error, info, warn}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, RwLock}; use tokio02::sync::mpsc::UnboundedSender; +use crate::youtube_dl::AudioMetadata; + static GST_INIT: Once = Once::new(); #[derive(Copy, Clone, Debug)] @@ -33,8 +35,10 @@ pub struct AudioPlayer { bus: gst::Bus, http_src: gst::Element, + volume_f64: RwLock<f64>, volume: gst::Element, - sender: Arc<Mutex<UnboundedSender<MusicBotMessage>>>, + sender: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, + currently_playing: RwLock<Option<AudioMetadata>>, } fn make_element(factoryname: &str, display_name: &str) -> Result<gst::Element, AudioPlayerError> { @@ -83,7 +87,7 @@ fn add_decode_bin_new_pad_callback( impl AudioPlayer { pub fn new( - sender: Arc<Mutex<UnboundedSender<MusicBotMessage>>>, + sender: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, callback: Option<Box<dyn FnMut(&[u8]) + Send>>, ) -> Result<Self, AudioPlayerError> { GST_INIT.call_once(|| gst::init().unwrap()); @@ -104,6 +108,12 @@ impl AudioPlayer { pipeline.add(&audio_bin)?; + // The documentation says that we have to make sure to handle + // all messages if auto flushing is deactivated. + // I hope our way of reading messages is good enough. + // + // https://gstreamer.freedesktop.org/documentation/gstreamer/gstpipeline.html#gst_pipeline_set_auto_flush_bus + pipeline.set_auto_flush_bus(false); pipeline.set_state(gst::State::Ready)?; Ok(AudioPlayer { @@ -111,8 +121,10 @@ impl AudioPlayer { bus, http_src, + volume_f64: RwLock::new(0.0), volume, sender, + currently_playing: RwLock::new(None), }) } @@ -173,7 +185,16 @@ impl AudioPlayer { Ok((audio_bin, volume, ghost_pad)) } - pub fn set_source_url(&self, location: String) -> Result<(), AudioPlayerError> { + pub fn set_metadata(&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); + + Ok(()) + } + + fn set_source_url(&self, location: String) -> Result<(), AudioPlayerError> { info!("Setting location URI: {}", location); self.http_src.set_property("location", &location)?; @@ -181,6 +202,7 @@ impl AudioPlayer { } pub fn set_volume(&self, volume: f64) -> Result<(), AudioPlayerError> { + *self.volume_f64.write().unwrap() = volume; let db = 50.0 * volume.log10(); info!("Setting volume: {} -> {} dB", volume, db); @@ -203,9 +225,26 @@ impl AudioPlayer { } } + 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(|v| Duration::from_nanos(v))) + } + + pub fn currently_playing(&self) -> Option<AudioMetadata> { + self.currently_playing.read().unwrap().clone() + } + 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.pipeline.set_state(gst::State::Null)?; Ok(()) @@ -273,20 +312,20 @@ impl AudioPlayer { pub fn quit(&self, reason: String) { info!("Quitting audio player"); - if let Err(e) = self + if let Err(_) = self .bus .post(&gst::Message::new_application(gst::Structure::new_empty("quit")).build()) { - warn!("Failed to send \"quit\" app event: {}", e); + warn!("Tried to send \"quit\" app event on flushing bus."); } - let sender = self.sender.lock().unwrap(); + let sender = self.sender.read().unwrap(); sender.send(MusicBotMessage::Quit(reason)).unwrap(); } fn send_state(&self, state: State) { info!("Sending state {:?} to application", state); - let sender = self.sender.lock().unwrap(); + let sender = self.sender.read().unwrap(); sender.send(MusicBotMessage::StateChange(state)).unwrap(); } @@ -362,7 +401,7 @@ impl AudioPlayer { } } _ => { - //debug!("{:?}", msg) + //debug!("Unhandled message on bus: {:?}", msg) } }; } diff --git a/src/bot/master.rs b/src/bot/master.rs index 2488064..755aaa1 100644 --- a/src/bot/master.rs +++ b/src/bot/master.rs @@ -1,12 +1,13 @@ use std::collections::HashMap; use std::future::Future; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, RwLock}; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Future as Future01; use log::info; use rand::{rngs::SmallRng, seq::SliceRandom, SeedableRng}; use serde::{Deserialize, Serialize}; +use tokio02::sync::mpsc::UnboundedSender; use tsclientlib::{ClientId, ConnectOptions, Identity, MessageTarget}; use crate::audio_player::AudioPlayerError; @@ -18,8 +19,9 @@ use crate::bot::{MusicBot, MusicBotArgs, MusicBotMessage}; pub struct MasterBot { config: Arc<MasterConfig>, - music_bots: Arc<Mutex<MusicBots>>, + music_bots: Arc<RwLock<MusicBots>>, teamspeak: Arc<TeamSpeakConnection>, + sender: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, } struct MusicBots { @@ -32,7 +34,7 @@ struct MusicBots { impl MasterBot { pub async fn new(args: MasterArgs) -> (Arc<Self>, impl Future) { let (tx, mut rx) = tokio02::sync::mpsc::unbounded_channel(); - let tx = Arc::new(Mutex::new(tx)); + let tx = Arc::new(RwLock::new(tx)); info!("Starting in TeamSpeak mode"); let mut con_config = ConnectOptions::new(args.address.clone()) @@ -65,7 +67,7 @@ impl MasterBot { let name_count = config.names.len(); let id_count = config.ids.len(); - let music_bots = Arc::new(Mutex::new(MusicBots { + let music_bots = Arc::new(RwLock::new(MusicBots { rng: SmallRng::from_entropy(), available_names: (0..name_count).collect(), available_ids: (0..id_count).collect(), @@ -76,6 +78,7 @@ impl MasterBot { config, music_bots, teamspeak: connection, + sender: tx.clone(), }); bot.teamspeak @@ -83,8 +86,12 @@ impl MasterBot { let cbot = bot.clone(); let msg_loop = async move { - loop { + 'outer: loop { while let Some(msg) = rx.recv().await { + if let MusicBotMessage::Quit(reason) = msg { + cbot.teamspeak.disconnect(&reason); + break 'outer; + } cbot.on_message(msg).await.unwrap(); } } @@ -115,7 +122,7 @@ impl MasterBot { ref mut available_names, ref mut available_ids, ref connected_bots, - } = &mut *self.music_bots.lock().expect("Mutex was not poisoned"); + } = &mut *self.music_bots.write().expect("RwLock was not poisoned"); for (_, bot) in connected_bots { if bot.my_channel() == channel { @@ -163,7 +170,7 @@ impl MasterBot { let cmusic_bots = self.music_bots.clone(); let disconnect_cb = Box::new(move |n, name_index, id_index| { - let mut music_bots = cmusic_bots.lock().expect("Mutex was not poisoned"); + 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); @@ -188,7 +195,7 @@ impl MasterBot { if let Some(bot_args) = self.build_bot_args_for(id) { let (bot, fut) = MusicBot::new(bot_args).await; tokio::spawn(fut.unit_error().boxed().compat().map(|_| ())); - let mut music_bots = self.music_bots.lock().expect("Mutex was not poisoned"); + let mut music_bots = self.music_bots.write().expect("RwLock was not poisoned"); music_bots .connected_bots .insert(bot.name().to_string(), bot); @@ -205,6 +212,62 @@ impl MasterBot { 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: 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(); + 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(), + }; + + result.push(bot_data); + } + + result + } + + pub fn bot_names(&self) -> Vec<String> { + let music_bots = self.music_bots.read().unwrap(); + + let len = music_bots.connected_bots.len(); + let mut result = Vec::with_capacity(len); + for (name, _) in &music_bots.connected_bots { + 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 { + bot.quit(reason.clone()) + } + let sender = self.sender.read().unwrap(); + sender.send(MusicBotMessage::Quit(reason)).unwrap(); + } } #[derive(Debug, Serialize, Deserialize)] @@ -217,6 +280,8 @@ pub struct MasterArgs { pub channel: Option<String>, #[serde(default = "default_verbose")] pub verbose: u8, + pub domain: String, + pub bind_address: String, pub names: Vec<String>, pub id: Identity, pub ids: Vec<Identity>, @@ -251,6 +316,8 @@ impl MasterArgs { ids: self.ids, local, address, + domain: self.domain, + bind_address: self.bind_address, id: self.id, channel, verbose, diff --git a/src/bot/music.rs b/src/bot/music.rs index 2539695..41976e5 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -1,10 +1,12 @@ use std::future::Future; use std::io::BufRead; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, RwLock}; use std::thread; +use std::time::Duration; use humantime; use log::{debug, info}; +use serde::Serialize; use structopt::StructOpt; use tokio02::sync::mpsc::UnboundedSender; use tsclientlib::{data, ChannelId, ClientId, ConnectOptions, Identity, Invoker, MessageTarget}; @@ -44,7 +46,7 @@ fn parse_seek(mut amount: &str) -> Result<Seek, ()> { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)] pub enum State { Playing, Paused, @@ -52,6 +54,18 @@ pub enum State { EndOfStream, } +impl std::fmt::Display for State { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + State::Playing => write!(fmt, "Playing"), + State::Paused => write!(fmt, "Paused"), + State::Stopped | State::EndOfStream => write!(fmt, "Stopped"), + }?; + + Ok(()) + } +} + #[derive(Debug)] pub enum MusicBotMessage { TextMessage(Message), @@ -71,8 +85,8 @@ pub struct MusicBot { name: String, player: Arc<AudioPlayer>, teamspeak: Option<Arc<TeamSpeakConnection>>, - playlist: Arc<Mutex<Playlist>>, - state: Arc<Mutex<State>>, + playlist: Arc<RwLock<Playlist>>, + state: Arc<RwLock<State>>, } pub struct MusicBotArgs { @@ -90,7 +104,7 @@ pub struct MusicBotArgs { impl MusicBot { pub async fn new(args: MusicBotArgs) -> (Arc<Self>, impl Future) { let (tx, mut rx) = tokio02::sync::mpsc::unbounded_channel(); - let tx = Arc::new(Mutex::new(tx)); + 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(); @@ -127,7 +141,7 @@ impl MusicBot { player.set_volume(0.5).unwrap(); let player = Arc::new(player); - let playlist = Arc::new(Mutex::new(Playlist::new())); + let playlist = Arc::new(RwLock::new(Playlist::new())); spawn_gstreamer_thread(player.clone(), tx.clone()); @@ -140,7 +154,7 @@ impl MusicBot { player, teamspeak: connection, playlist, - state: Arc::new(Mutex::new(State::Stopped)), + state: Arc::new(RwLock::new(State::Stopped)), }); let cbot = bot.clone(); @@ -173,24 +187,20 @@ impl MusicBot { } fn start_playing_audio(&self, metadata: AudioMetadata) { - if let Some(title) = metadata.title { - self.send_message(&format!("Playing {}", ts::underline(&title))); - self.set_description(&format!("Currently playing '{}'", title)); - } else { - self.send_message("Playing unknown title"); - self.set_description("Currently playing"); - } + self.send_message(&format!("Playing {}", ts::underline(&metadata.title))); + self.set_description(&format!("Currently playing '{}'", metadata.title)); self.player.reset().unwrap(); - self.player.set_source_url(metadata.url).unwrap(); + self.player.set_metadata(metadata).unwrap(); self.player.play().unwrap(); } - pub async fn add_audio(&self, url: String) { + pub async fn add_audio(&self, url: String, user: String) { match crate::youtube_dl::get_audio_download_url(url).await { - Ok(metadata) => { + Ok(mut metadata) => { + metadata.added_by = user; info!("Found audio url: {}", metadata.url); - let mut playlist = self.playlist.lock().expect("Mutex was not poisoned"); + let mut playlist = self.playlist.write().expect("RwLock was not poisoned"); playlist.push(metadata.clone()); if !self.player.is_started() { @@ -198,11 +208,10 @@ impl MusicBot { self.start_playing_audio(request); } } else { - if let Some(title) = metadata.title { - self.send_message(&format!("Added {} to playlist", ts::underline(&title))); - } else { - self.send_message("Added to playlist"); - } + self.send_message(&format!( + "Added {} to playlist", + ts::underline(&metadata.title) + )); } } Err(e) => { @@ -217,6 +226,26 @@ impl MusicBot { &self.name } + pub fn state(&self) -> State { + *self.state.read().expect("RwLock was not poisoned") + } + + pub 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 fn playlist_to_vec(&self) -> Vec<AudioMetadata> { + self.playlist.read().unwrap().to_vec() + } + pub fn my_channel(&self) -> ChannelId { self.teamspeak .as_ref() @@ -255,7 +284,7 @@ impl MusicBot { let tokens = msg[1..].split_whitespace().collect::<Vec<_>>(); match Command::from_iter_safe(&tokens) { - Ok(args) => self.on_command(args).await?, + Ok(args) => self.on_command(args, message.invoker).await?, Err(e) if e.kind == structopt::clap::ErrorKind::HelpDisplayed => { self.send_message(&format!("\n{}", e.message)); } @@ -266,10 +295,10 @@ impl MusicBot { Ok(()) } - async fn on_command(&self, command: Command) -> Result<(), AudioPlayerError> { + async fn on_command(&self, command: Command, invoker: Invoker) -> Result<(), AudioPlayerError> { match command { Command::Play => { - let playlist = self.playlist.lock().expect("Mutex was not poisoned"); + let playlist = self.playlist.read().expect("RwLock was not poisoned"); if !self.player.is_started() { if !playlist.is_empty() { @@ -283,7 +312,7 @@ impl MusicBot { // strip bbcode tags from url let url = url.replace("[URL]", "").replace("[/URL]", ""); - self.add_audio(url.to_string()).await; + self.add_audio(url.to_string(), invoker.name).await; } Command::Pause => { self.player.pause()?; @@ -303,7 +332,7 @@ impl MusicBot { } } Command::Next => { - let playlist = self.playlist.lock().expect("Mutex was not poisoned"); + let playlist = self.playlist.read().expect("RwLock was not poisoned"); if !playlist.is_empty() { info!("Skipping to next track"); self.player.stop_current()?; @@ -314,8 +343,8 @@ impl MusicBot { } Command::Clear => { self.playlist - .lock() - .expect("Mutex was not poisoned") + .write() + .expect("RwLock was not poisoned") .clear(); } Command::Volume { percent: volume } => { @@ -331,7 +360,7 @@ impl MusicBot { } fn on_state(&self, state: State) -> Result<(), AudioPlayerError> { - let mut current_state = self.state.lock().unwrap(); + let mut current_state = self.state.write().unwrap(); if *current_state != state { match state { State::Playing => { @@ -345,7 +374,11 @@ impl MusicBot { self.set_description(""); } State::EndOfStream => { - let next_track = self.playlist.lock().expect("Mutex was not poisoned").pop(); + let next_track = self + .playlist + .write() + .expect("RwLock was not poisoned") + .pop(); if let Some(request) = next_track { info!("Advancing playlist"); @@ -401,7 +434,7 @@ impl MusicBot { } } -fn spawn_stdin_reader(tx: Arc<Mutex<UnboundedSender<MusicBotMessage>>>) { +fn spawn_stdin_reader(tx: Arc<RwLock<UnboundedSender<MusicBotMessage>>>) { debug!("Spawning stdin reader thread"); thread::Builder::new() .name(String::from("stdin reader")) @@ -421,7 +454,7 @@ fn spawn_stdin_reader(tx: Arc<Mutex<UnboundedSender<MusicBotMessage>>>) { text: line, }); - let tx = tx.lock().unwrap(); + let tx = tx.read().unwrap(); tx.send(message).unwrap(); } }) @@ -430,7 +463,7 @@ fn spawn_stdin_reader(tx: Arc<Mutex<UnboundedSender<MusicBotMessage>>>) { fn spawn_gstreamer_thread( player: Arc<AudioPlayer>, - tx: Arc<Mutex<UnboundedSender<MusicBotMessage>>>, + tx: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, ) { thread::Builder::new() .name(String::from("gstreamer polling")) @@ -439,7 +472,7 @@ fn spawn_gstreamer_thread( break; } - tx.lock() + tx.read() .unwrap() .send(MusicBotMessage::StateChange(State::EndOfStream)) .unwrap(); diff --git a/src/main.rs b/src/main.rs index 922162f..2559a2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; +use std::thread; use futures::future::{FutureExt, TryFutureExt}; -use log::{debug, info}; +use log::{debug, error, info}; use structopt::clap::AppSettings; use structopt::StructOpt; use tsclientlib::Identity; @@ -13,6 +14,7 @@ mod bot; mod command; mod playlist; mod teamspeak; +mod web_server; mod youtube_dl; use bot::{MasterArgs, MasterBot, MusicBot, MusicBotArgs}; @@ -116,7 +118,22 @@ fn run() -> Result<(), Box<dyn std::error::Error>> { }; MusicBot::new(bot_args).await.1.await; } else { - MasterBot::new(bot_args).await.1.await; + 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); + } + }); + + fut.await; } } .unit_error() diff --git a/src/playlist.rs b/src/playlist.rs index 87c1c98..445f8a5 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -28,6 +28,16 @@ impl Playlist { res } + pub fn to_vec(&self) -> Vec<AudioMetadata> { + let (a, b) = self.data.as_slices(); + + let mut res = a.to_vec(); + res.extend_from_slice(b); + res.reverse(); + + res + } + pub fn is_empty(&self) -> bool { self.data.is_empty() } diff --git a/src/teamspeak/bbcode.rs b/src/teamspeak/bbcode.rs index 28be08a..91d576a 100644 --- a/src/teamspeak/bbcode.rs +++ b/src/teamspeak/bbcode.rs @@ -1,4 +1,4 @@ -use std::fmt::{Formatter, Display, Error}; +use std::fmt::{Display, Error, Formatter}; #[allow(dead_code)] pub enum BbCode<'a> { @@ -14,7 +14,9 @@ impl<'a> Display for BbCode<'a> { BbCode::Bold(text) => fmt.write_fmt(format_args!("[B]{}[/B]", text))?, BbCode::Italic(text) => fmt.write_fmt(format_args!("[I]{}[/I]", text))?, BbCode::Underline(text) => fmt.write_fmt(format_args!("[U]{}[/U]", text))?, - BbCode::Link(text, url) => fmt.write_fmt(format_args!("[URL={}]{}[/URL]", url, text))?, + BbCode::Link(text, url) => { + fmt.write_fmt(format_args!("[URL={}]{}[/URL]", url, text))? + } }; Ok(()) diff --git a/src/teamspeak/mod.rs b/src/teamspeak/mod.rs index 5ac0d44..7551e77 100644 --- a/src/teamspeak/mod.rs +++ b/src/teamspeak/mod.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; use futures::compat::Future01CompatExt; @@ -76,7 +76,7 @@ fn get_message<'a>(event: &Event) -> Option<MusicBotMessage> { impl TeamSpeakConnection { pub async fn new( - tx: Arc<Mutex<UnboundedSender<MusicBotMessage>>>, + tx: Arc<RwLock<UnboundedSender<MusicBotMessage>>>, options: ConnectOptions, ) -> Result<TeamSpeakConnection, tsclientlib::Error> { let conn = Connection::new(options).compat().await?; @@ -89,7 +89,7 @@ impl TeamSpeakConnection { if let ConEvents(_conn, events) = e { for event in *events { if let Some(msg) = get_message(event) { - let tx = tx.lock().expect("Mutex was not poisoned"); + let tx = tx.read().expect("RwLock was not poisoned"); // Ignore the result because the receiver might get dropped first. let _ = tx.send(msg); } diff --git a/src/web_server.rs b/src/web_server.rs new file mode 100644 index 0000000..01233f2 --- /dev/null +++ b/src/web_server.rs @@ -0,0 +1,122 @@ +use std::sync::Arc; +use std::time::Duration; + +use actix::{Addr, SyncArbiter}; +use actix_web::{ + get, http::header, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder, +}; +use askama::actix_web::TemplateIntoResponse; +use askama::Template; +use serde::{Deserialize, Serialize}; + +use crate::bot::MasterBot; +use crate::youtube_dl::AudioMetadata; + +mod api; +mod bot_executor; +mod default; +mod front_end_cookie; +mod tmtu; +pub use bot_executor::*; +use front_end_cookie::FrontEnd; + +pub struct WebServerArgs { + pub domain: String, + pub bind_address: String, + pub bot: Arc<MasterBot>, +} + +#[actix_rt::main] +pub async fn start(args: WebServerArgs) -> std::io::Result<()> { + let cbot = args.bot.clone(); + let bot_addr: Addr<BotExecutor> = SyncArbiter::start(4, move || BotExecutor(cbot.clone())); + + HttpServer::new(move || { + App::new() + .data(bot_addr.clone()) + .wrap(Logger::default()) + .service(index) + .service(get_bot) + .service(post_front_end) + .service( + web::scope("/api") + .service(api::get_bot_list) + .service(api::get_bot), + ) + .service(web::scope("/docs").service(get_api_docs)) + .service(actix_files::Files::new("/static", "web_server/static/")) + }) + .bind(args.bind_address)? + .run() + .await?; + + args.bot.quit(String::from("Stopping")); + + Ok(()) +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +struct FrontEndForm { + front_end: FrontEnd, +} + +#[post("/front-end")] +async fn post_front_end(form: web::Form<FrontEndForm>) -> impl Responder { + front_end_cookie::set_front_end(form.into_inner().front_end).await +} + +#[derive(Debug, Serialize)] +pub struct BotData { + pub name: String, + pub state: crate::bot::State, + pub volume: f64, + pub position: Option<Duration>, + pub currently_playing: Option<AudioMetadata>, + pub playlist: Vec<AudioMetadata>, +} + +#[get("/")] +async fn index(bot: web::Data<Addr<BotExecutor>>, front: FrontEnd) -> impl Responder { + match front { + FrontEnd::Default => default::index(bot).await, + FrontEnd::Tmtu => tmtu::index(bot).await, + } +} + +#[get("/bot/{name}")] +async fn get_bot( + bot: web::Data<Addr<BotExecutor>>, + name: web::Path<String>, + front: FrontEnd, +) -> impl Responder { + match front { + FrontEnd::Tmtu => tmtu::get_bot(bot, name.into_inner()).await, + FrontEnd::Default => Ok(HttpResponse::Found().header(header::LOCATION, "/").finish()), + } +} + +#[derive(Template)] +#[template(path = "docs/api.htm")] +struct ApiDocsTemplate; + +#[get("/api")] +async fn get_api_docs() -> impl Responder { + ApiDocsTemplate.into_response() +} + +mod filters { + use std::time::Duration; + + pub fn fmt_duration(duration: &Option<Duration>) -> Result<String, askama::Error> { + if let Some(duration) = duration { + let secs = duration.as_secs(); + let mins = secs / 60; + let submin_secs = secs % 60; + + Ok(format!("{:02}:{:02}", mins, submin_secs)) + } else { + Ok(String::from("--:--")) + } + } +} diff --git a/src/web_server/api.rs b/src/web_server/api.rs new file mode 100644 index 0000000..4deedad --- /dev/null +++ b/src/web_server/api.rs @@ -0,0 +1,48 @@ +use actix::Addr; +use actix_web::{get, web, HttpResponse, Responder, ResponseError}; +use derive_more::Display; +use serde::Serialize; + +use crate::web_server::{BotDataListRequest, BotDataRequest, BotExecutor}; + +#[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), + }; + + web::Json(bot_datas) +} + +#[get("/bots/{name}")] +pub async fn get_bot(bot: web::Data<Addr<BotExecutor>>, 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 { + Err(ApiErrorKind::NotFound) + } +} + +#[derive(Serialize)] +struct ApiError { + error: String, + description: String, +} + +#[derive(Debug, Display)] +enum ApiErrorKind { + #[display(fmt = "Not Found")] + NotFound, +} + +impl ResponseError for ApiErrorKind { + fn error_response(&self) -> HttpResponse { + match *self { + ApiErrorKind::NotFound => HttpResponse::NotFound().json(ApiError { + error: self.to_string(), + description: String::from("The requested resource was not found"), + }), + } + } +} diff --git a/src/web_server/bot_executor.rs b/src/web_server/bot_executor.rs new file mode 100644 index 0000000..fde3c08 --- /dev/null +++ b/src/web_server/bot_executor.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +use actix::{Actor, Handler, Message, SyncContext}; + +use crate::bot::MasterBot; +use crate::web_server::BotData; + +pub struct BotExecutor(pub Arc<MasterBot>); + +impl Actor for BotExecutor { + type Context = SyncContext<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 new file mode 100644 index 0000000..b3c8291 --- /dev/null +++ b/src/web_server/default.rs @@ -0,0 +1,24 @@ +use actix::Addr; +use actix_web::{web, Error, HttpResponse}; +use askama::actix_web::TemplateIntoResponse; +use askama::Template; + +use crate::web_server::{filters, BotData, BotDataListRequest, BotExecutor}; + +#[derive(Template)] +#[template(path = "index.htm")] +struct OverviewTemplate<'a> { + bots: &'a [BotData], +} + +pub async fn index(bot: web::Data<Addr<BotExecutor>>) -> Result<HttpResponse, Error> { + let bot_datas = match bot.send(BotDataListRequest).await.unwrap() { + Ok(data) => data, + Err(_) => Vec::with_capacity(0), + }; + + OverviewTemplate { + bots: &bot_datas[..], + } + .into_response() +} diff --git a/src/web_server/front_end_cookie.rs b/src/web_server/front_end_cookie.rs new file mode 100644 index 0000000..4812d0d --- /dev/null +++ b/src/web_server/front_end_cookie.rs @@ -0,0 +1,60 @@ +use futures::future::{ok, Ready}; + +use actix_web::{ + dev::Payload, + http::header::{COOKIE, LOCATION, SET_COOKIE}, + FromRequest, HttpRequest, HttpResponse, +}; +use serde::Deserialize; + +#[derive(PartialEq, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FrontEnd { + Default, + Tmtu, +} + +impl FrontEnd { + const COOKIE_NAME: &'static str = "front-end"; + + fn cookie(&self) -> String { + let name = match self { + FrontEnd::Default => "default", + FrontEnd::Tmtu => "tmtu", + }; + + format!("{}={}", Self::COOKIE_NAME, name) + } +} + +impl FromRequest for FrontEnd { + type Error = (); + type Future = Ready<Result<Self, ()>>; + type Config = (); + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + for header in req.headers().get_all(COOKIE) { + if let Ok(value) = header.to_str() { + for c in value.split(';').map(|s| s.trim()) { + let mut split = c.split('='); + if Some(Self::COOKIE_NAME) == split.next() { + match split.next() { + Some("default") => return ok(FrontEnd::Default), + Some("tmtu") => return ok(FrontEnd::Tmtu), + _ => (), + } + } + } + } + } + + ok(FrontEnd::Default) + } +} + +pub fn set_front_end(front: FrontEnd) -> HttpResponse { + HttpResponse::Found() + .header(SET_COOKIE, front.cookie()) + .header(LOCATION, "/") + .finish() +} diff --git a/src/web_server/tmtu.rs b/src/web_server/tmtu.rs new file mode 100644 index 0000000..0645ee4 --- /dev/null +++ b/src/web_server/tmtu.rs @@ -0,0 +1,41 @@ +use actix::Addr; +use actix_web::{http::header, web, Error, HttpResponse}; +use askama::actix_web::TemplateIntoResponse; +use askama::Template; + +use crate::web_server::{filters, BotData, BotDataRequest, BotExecutor, BotNameListRequest}; + +#[derive(Template)] +#[template(path = "tmtu/index.htm")] +struct TmtuTemplate { + bot_names: Vec<String>, + bot: Option<BotData>, +} + +pub async fn index(bot: web::Data<Addr<BotExecutor>>) -> Result<HttpResponse, Error> { + let bot_names = bot.send(BotNameListRequest).await.unwrap().unwrap(); + + TmtuTemplate { + bot_names, + bot: None, + } + .into_response() +} + +pub async fn get_bot( + bot: web::Data<Addr<BotExecutor>>, + name: String, +) -> Result<HttpResponse, Error> { + let bot_names = bot.send(BotNameListRequest).await.unwrap().unwrap(); + + if let Some(bot) = bot.send(BotDataRequest(name)).await.unwrap() { + TmtuTemplate { + bot_names, + bot: Some(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 c6012f0..89b1477 100644 --- a/src/youtube_dl.rs +++ b/src/youtube_dl.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use futures::compat::Future01CompatExt; use std::process::{Command, Stdio}; use tokio_process::CommandExt; @@ -9,7 +11,22 @@ use log::debug; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct AudioMetadata { pub url: String, - pub title: Option<String>, + pub webpage_url: String, + pub title: String, + pub thumbnail: Option<String>, + #[serde(default, deserialize_with = "duration_deserialize")] + pub duration: Option<Duration>, + #[serde(skip)] + pub added_by: String, +} + +fn duration_deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let dur: Option<f64> = Deserialize::deserialize(deserializer)?; + + Ok(dur.map(|v| Duration::from_secs_f64(v))) } pub async fn get_audio_download_url(uri: String) -> Result<AudioMetadata, String> { diff --git a/web_server/static/fonts/.gitkeep b/web_server/static/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/web_server/static/fonts/.gitkeep diff --git a/web_server/static/style.css b/web_server/static/style.css new file mode 100644 index 0000000..09a985c --- /dev/null +++ b/web_server/static/style.css @@ -0,0 +1,63 @@ +@font-face { + font-family: 'roboto-regular'; + src: url('fonts/Roboto-Regular.ttf') format('truetype'); +} + +@font-face { + font-family: 'roboto-light'; + src: url('fonts/Roboto-Light.ttf') format('truetype'); +} + +@font-face { + font-family: 'roboto-bold'; + src: url('fonts/Roboto-Bold.ttf') format('truetype'); +} + +body { + background-color: #151515; +} + +main { + margin: auto; + max-width: 800px; + padding: 1em; + background-color: #202020; + color: #eee; + font-family: 'roboto-regular', Arial; +} + +nav > a { + font-size: 1.4rem; +} + +a, a:visited { + color: #eee; +} + +a:hover { + color: #ccc; +} + +pre { + font-size: 0.9rem; + font-family: monospace; + background-color: #151515; + overflow: auto; + padding: 1em; +} + +.code-background { + background-color: #151515; +} + +.code-normal { + color: #c0c5ce; +} + +.code-string { + color:#a3be8c; +} + +.code-number { + color: #d08770; +} diff --git a/web_server/templates/base.htm b/web_server/templates/base.htm new file mode 100644 index 0000000..b8b2f49 --- /dev/null +++ b/web_server/templates/base.htm @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <link href="/static/style.css" rel="stylesheet"> + <title>{% block title %}{{ title }} - PokeBot{% endblock %}</title> + </head> + <body> + <main> + {% block content %}{% endblock %} + </main> + </body> +</html> diff --git a/web_server/templates/docs/api.htm b/web_server/templates/docs/api.htm new file mode 100644 index 0000000..a973272 --- /dev/null +++ b/web_server/templates/docs/api.htm @@ -0,0 +1,126 @@ +{% extends "base.htm" %} + +{% block title %}API Documentation{% endblock %} + +{% block content %} +<h1>API Documentation</h1> +<nav> + <a href="/">Bots</a> + <a href="/docs/api">API</a> +</nav> + +<h2>Bot list</h2> +<p>Show a list of all bots.</p> + +<p><b>URL</b>: <span class="code-background">/api/bots</span></p> +<p><b>Method</b>: <span class="code-background">GET</span></p> +<p><b>Auth required</b>: <span class="code-background">NO</span></p> + +<h3>Success Response</h3> + +<p><b>Code</b>: <span class="code-background">200 OK</span></p> + +<h3>Content example</h3> + +<!-- Generated with syntect and adjusted --> +<pre> +<span class="code-normal">[ +</span><span class="code-normal"> { +</span><span class="code-normal"> "</span><span class="code-string">name</span><span class="code-normal">": "</span><span class="code-string">MusicBot</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">state</span><span class="code-normal">": "</span><span class="code-string">Playing</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">volume</span><span class="code-normal">": </span><span class="code-number">0.5</span><span class="code-normal">, +</span><span class="code-normal"> "</span><span class="code-string">position</span><span class="code-normal">": { +</span><span class="code-normal"> "</span><span class="code-string">secs</span><span class="code-normal">": </span><span class="code-number">10</span><span class="code-normal">, +</span><span class="code-normal"> "</span><span class="code-string">nanos</span><span class="code-normal">": </span><span class="code-number">63573687 +</span><span class="code-normal"> }, +</span><span class="code-normal"> "</span><span class="code-string">currently_playing</span><span class="code-normal">": { +</span><span class="code-normal"> "</span><span class="code-string">url</span><span class="code-normal">": "</span><span class="code-string"><temp_url></span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">webpage_url</span><span class="code-normal">": "</span><span class="code-string">https://www.youtube.com/watch?v=dQw4w9WgXcQ</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">title</span><span class="code-normal">": "</span><span class="code-string">Rick Astley - Never Gonna Give You Up (Video)</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">thumbnail</span><span class="code-normal">": "</span><span class="code-string">https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">duration</span><span class="code-normal">": { +</span><span class="code-normal"> "</span><span class="code-string">secs</span><span class="code-normal">": </span><span class="code-number">212</span><span class="code-normal">, +</span><span class="code-normal"> "</span><span class="code-string">nanos</span><span class="code-normal">": </span><span class="code-number">0 +</span><span class="code-normal"> } +</span><span class="code-normal"> }, +</span><span class="code-normal"> "</span><span class="code-string">playlist</span><span class="code-normal">": [ +</span><span class="code-normal"> { +</span><span class="code-normal"> "</span><span class="code-string">url</span><span class="code-normal">": "</span><span class="code-string"><temp_url></span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">webpage_url</span><span class="code-normal">": "</span><span class="code-string">https://www.youtube.com/watch?v=dQw4w9WgXcQ</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">title</span><span class="code-normal">": "</span><span class="code-string">Rick Astley - Never Gonna Give You Up (Video)</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">thumbnail</span><span class="code-normal">": "</span><span class="code-string">https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">duration</span><span class="code-normal">": { +</span><span class="code-normal"> "</span><span class="code-string">secs</span><span class="code-normal">": </span><span class="code-number">212</span><span class="code-normal">, +</span><span class="code-normal"> "</span><span class="code-string">nanos</span><span class="code-normal">": </span><span class="code-number">0 +</span><span class="code-normal"> } +</span><span class="code-normal"> } +</span><span class="code-normal"> ] +</span><span class="code-normal"> } +</span><span class="code-normal">] +</span></pre> + + +<h2>Show Bot</h2> +<p>Show a specific bot.</p> + +<p><b>URL</b>: <span class="code-background">/api/bots/:botname</span></p> +<p><b>Method</b>: <span class="code-background">GET</span></p> +<p><b>Auth required</b>: <span class="code-background">NO</span></p> + +<h3>Success Response</h3> +<p><b>Code</b>: <span class="code-background">200 OK</span></p> + +<h3>Content example</h3> + +<!-- Generated with syntect and adjusted --> +<pre> +<span class="code-normal">{ +</span><span class="code-normal"> "</span><span class="code-string">name</span><span class="code-normal">": "</span><span class="code-string">MusicBot</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">state</span><span class="code-normal">": "</span><span class="code-string">Playing</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">volume</span><span class="code-normal">": </span><span class="code-number">0.5</span><span class="code-normal">, +</span><span class="code-normal"> "</span><span class="code-string">position</span><span class="code-normal">": { +</span><span class="code-normal"> "</span><span class="code-string">secs</span><span class="code-normal">": </span><span class="code-number">142</span><span class="code-normal">, +</span><span class="code-normal"> "</span><span class="code-string">nanos</span><span class="code-normal">": </span><span class="code-number">690911766 +</span><span class="code-normal"> }, +</span><span class="code-normal"> "</span><span class="code-string">currently_playing</span><span class="code-normal">": { +</span><span class="code-normal"> "</span><span class="code-string">url</span><span class="code-normal">": "</span><span class="code-string"><temp_url></span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">webpage_url</span><span class="code-normal">": "</span><span class="code-string">https://www.youtube.com/watch?v=dQw4w9WgXcQ</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">title</span><span class="code-normal">": "</span><span class="code-string">Rick Astley - Never Gonna Give You Up (Video)</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">thumbnail</span><span class="code-normal">": "</span><span class="code-string">https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">duration</span><span class="code-normal">": { +</span><span class="code-normal"> "</span><span class="code-string">secs</span><span class="code-normal">": </span><span class="code-number">212</span><span class="code-normal">, +</span><span class="code-normal"> "</span><span class="code-string">nanos</span><span class="code-normal">": </span><span class="code-number">0 +</span><span class="code-normal"> } +</span><span class="code-normal"> }, +</span><span class="code-normal"> "</span><span class="code-string">playlist</span><span class="code-normal">": [ +</span><span class="code-normal"> { +</span><span class="code-normal"> "</span><span class="code-string">url</span><span class="code-normal">": "</span><span class="code-string"><temp_url></span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">webpage_url</span><span class="code-normal">": "</span><span class="code-string">https://www.youtube.com/watch?v=dQw4w9WgXcQ</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">title</span><span class="code-normal">": "</span><span class="code-string">Rick Astley - Never Gonna Give You Up (Video)</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">thumbnail</span><span class="code-normal">": "</span><span class="code-string">https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">duration</span><span class="code-normal">": { +</span><span class="code-normal"> "</span><span class="code-string">secs</span><span class="code-normal">": </span><span class="code-number">212</span><span class="code-normal">, +</span><span class="code-normal"> "</span><span class="code-string">nanos</span><span class="code-normal">": </span><span class="code-number">0 +</span><span class="code-normal"> } +</span><span class="code-normal"> } +</span><span class="code-normal"> ] +</span><span class="code-normal">} +</span></pre> + +<h3>Error Response</h3> + +<p><b>Condition</b>: If ':botname' is not connected to TeamSpeak.</p> + +<p><b>Code</b>: <span class="code-background">404 NOT FOUND</span></p> + +<b>Content</b>: + +<!-- Generated with syntect and adjusted --> +<pre> +<span class="code-normal">{ +</span><span class="code-normal"> "</span><span class="code-string">error</span><span class="code-normal">": "</span><span class="code-string">Not Found</span><span class="code-normal">", +</span><span class="code-normal"> "</span><span class="code-string">description</span><span class="code-normal">": "</span><span class="code-string">The requested resource was not found</span><span class="code-normal">" +</span><span class="code-normal">} +</span></pre> + +{% endblock %} diff --git a/web_server/templates/index.htm b/web_server/templates/index.htm new file mode 100644 index 0000000..eed31f3 --- /dev/null +++ b/web_server/templates/index.htm @@ -0,0 +1,36 @@ +{% extends "base.htm" %} + +{% block title %}Overview{% endblock %} + +{% block content %} +<h1>Bots</h1> +<form action="/front-end" method="POST"> + <input type="hidden" placeholder="Enter front end" name="front-end" value="tmtu"> + <button type="submit">tmtu-mode</button> +</form> +<nav> + <a href="/">Bots</a> + <a href="/docs/api">API</a> +</nav> + +<ul> + {% for bot in bots %} + <h2>{{ bot.name }}</h1> + <div>State: {{ bot.state }}</div> + <div>Volume: {{ bot.volume * 100.0 }}%</div> + {% match bot.currently_playing %} + {% when Some with (current) %} + <span>Currently playing:</span> + {% let item = current %} + {% include "song.htm" %} + {% when None %} + {% endmatch %} + + {% for item in bot.playlist %} + <li> + {% include "song.htm" %} + </li> + {% endfor %} + {% endfor %} +</ul> +{% endblock %} diff --git a/web_server/templates/song.htm b/web_server/templates/song.htm new file mode 100644 index 0000000..072567a --- /dev/null +++ b/web_server/templates/song.htm @@ -0,0 +1,7 @@ +<a href="{{ item.webpage_url }}">{{ item.title }}</a> +<span>({{ item.duration|fmt_duration }})</span> +{% match item.thumbnail %} + {% when Some with (thumbnail) %} + <img src="{{ thumbnail }}" height="128"> + {% when None %} +{% endmatch %} diff --git a/web_server/templates/tmtu/index.htm b/web_server/templates/tmtu/index.htm new file mode 100644 index 0000000..785e653 --- /dev/null +++ b/web_server/templates/tmtu/index.htm @@ -0,0 +1,145 @@ +<!DOCTYPE html> +<html> + <head> + <title>tmtu mode</title> + <style type="text/css"> + body { + margin: 16px; + } + td { + padding-right: 16px; + padding-top: 1px; + padding-bottom: : 1px; + } + td, th { + vertical-align:top; + } + .tableheader td { + color: gray; + border-bottom: 1px solid gray; + } + .stat { + color: gray; + text-align: right; + white-space: nowrap; + padding-left: 8px + } + .tracktable { + border-left: 1px solid gray; + } + .tracktable tr:hover { + background-color: #E0E0E0; + } + .bottable tr:hover { + background-color: #E0E0E0; + } + #test:hover { + background: 2px solid red; + } + a { + color: teal; + } + a:hover { + color: red; + } + a[visited] { + color: navy + } + .addedby { + color: darkorange; + } + .botname { + } + .selected { + font-weight: 700; + } + .playing { + background: PaleGreen; + } + </style> + </head> + <body> + <table> + <tr> + <td colspan="2"> + <h1>PokeBot</h1> + <p>A web interface for inspecting currently playing audio in PokeBot. Select an instance of the bot to view it's playlist and history.</p> + <nav style="display: inline-block;"> + <ol> + {% let bot_name %} + {% match bot %} + {% when Some with (bot) %} + {% let bot_name = bot.name.clone() %} + {% when None %} + {% let bot_name = "".to_owned() %} + {% endmatch %} + {% for name in bot_names %} + {% if name.clone() == bot_name %} + <li><a href="/bot/{{ name }}" class="botname selected">{{ name }}</a></li> + {% else %} + <li><a href="/bot/{{ name }}" class="botname">{{ name }}</a></li> + {% endif %} + {% endfor %} + </ol> + </nav> + </td> + </tr> + {% match bot %} + {% when Some with (bot) %} + <tr> + <td colspan="2"> + <h2>Status</h2> + <div class="{{ bot.state|lower }}" style="padding: 5px;"> + {% match bot.currently_playing %} + {% when Some with (current) %} + <p>Currently playing: <a href="{{ current.webpage_url }}">{{ current.title }}</a></p> + <p><strong>{{ bot.position|fmt_duration }} / {{ current.duration|fmt_duration }}</strong> + {% match current.duration %} + {% when Some with (duration) %} + {% let position %} + {% match bot.position %} + {% when Some with (pos) %} + {% let position = pos.as_secs_f64() %} + {% when None %} + {% let position = 0.0 %} + {% endmatch %} + {% let progress = position / duration.as_secs_f64() %} + {% let percent = progress * 100.0 %} + <progress value="{{ percent }}" max="100" title="test"></progress></p> + {% when None %} + <progress value="0" max="100" title="test"></progress></p> + {% endmatch %} + {% when None %} + {% endmatch %} + </div> + </td> + </tr> + <tr> + <td> + <h2>Playlist</h2> + <table class="tracktable" cellspacing="0" cellpadding="0"> + <tr class="tableheader"> + <td class="stat">#</td> + <td>track</td> + <td>length</td> + <td>added by</td> + </tr> + {% for item in bot.playlist %} + <tr> + <td class="stat">{{ loop.index }}</td> + <td><a href="{{ item.webpage_url }}">{{ item.title }}</a></td> + <td> + {% let duration = item.duration %} + {{ duration|fmt_duration }} + </td> + <td>{{ item.added_by }}</td> + </tr> + {% endfor %} + </table> + </td> + </tr> + {% when None %} + {% endmatch %} + </table> + </body> +</html> |
