aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFelix Kaaman <tmtu@tmtu.ee>2020-02-22 23:27:01 +0100
committerGitHub <noreply@github.com>2020-02-22 23:27:01 +0100
commit763b8c6579f3ae571f7287c72b9fb4f8b6e89349 (patch)
treebc55a0e79107a93bc3e605a0cae32926dc4c52fc
parent2792ba9c8a7120a91b3bd2c6075e737690e73405 (diff)
parent326cfa543c6263818aad7dec4a869bc8139ec14c (diff)
downloadpokebot-763b8c6579f3ae571f7287c72b9fb4f8b6e89349.tar.gz
pokebot-763b8c6579f3ae571f7287c72b9fb4f8b6e89349.zip
Merge pull request #33 from Mavulp/webserver
Webserver
-rw-r--r--Cargo.lock746
-rw-r--r--Cargo.toml6
-rw-r--r--askama.toml3
-rw-r--r--config.toml.example4
-rw-r--r--log4rs.yml4
-rw-r--r--src/audio_player.rs57
-rw-r--r--src/bot/master.rs83
-rw-r--r--src/bot/music.rs105
-rw-r--r--src/main.rs21
-rw-r--r--src/playlist.rs10
-rw-r--r--src/teamspeak/bbcode.rs6
-rw-r--r--src/teamspeak/mod.rs6
-rw-r--r--src/web_server.rs122
-rw-r--r--src/web_server/api.rs48
-rw-r--r--src/web_server/bot_executor.rs63
-rw-r--r--src/web_server/default.rs24
-rw-r--r--src/web_server/front_end_cookie.rs60
-rw-r--r--src/web_server/tmtu.rs41
-rw-r--r--src/youtube_dl.rs19
-rw-r--r--web_server/static/fonts/.gitkeep0
-rw-r--r--web_server/static/style.css63
-rw-r--r--web_server/templates/base.htm15
-rw-r--r--web_server/templates/docs/api.htm126
-rw-r--r--web_server/templates/index.htm36
-rw-r--r--web_server/templates/song.htm7
-rw-r--r--web_server/templates/tmtu/index.htm145
26 files changed, 1734 insertions, 86 deletions
diff --git a/Cargo.lock b/Cargo.lock
index fe0ea26..9117117 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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]]
diff --git a/Cargo.toml b/Cargo.toml
index 7f7aeb5..d271d7f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"]
diff --git a/log4rs.yml b/log4rs.yml
index 77d8eef..b0d0ab8 100644
--- a/log4rs.yml
+++ b/log4rs.yml
@@ -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"> &quot;</span><span class="code-string">name</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">MusicBot</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">state</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">Playing</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">volume</span><span class="code-normal">&quot;: </span><span class="code-number">0.5</span><span class="code-normal">,
+</span><span class="code-normal"> &quot;</span><span class="code-string">position</span><span class="code-normal">&quot;: {
+</span><span class="code-normal"> &quot;</span><span class="code-string">secs</span><span class="code-normal">&quot;: </span><span class="code-number">10</span><span class="code-normal">,
+</span><span class="code-normal"> &quot;</span><span class="code-string">nanos</span><span class="code-normal">&quot;: </span><span class="code-number">63573687
+</span><span class="code-normal"> },
+</span><span class="code-normal"> &quot;</span><span class="code-string">currently_playing</span><span class="code-normal">&quot;: {
+</span><span class="code-normal"> &quot;</span><span class="code-string">url</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">&lt;temp_url&gt;</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">webpage_url</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">https://www.youtube.com/watch?v=dQw4w9WgXcQ</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">title</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">Rick Astley - Never Gonna Give You Up (Video)</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">thumbnail</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">duration</span><span class="code-normal">&quot;: {
+</span><span class="code-normal"> &quot;</span><span class="code-string">secs</span><span class="code-normal">&quot;: </span><span class="code-number">212</span><span class="code-normal">,
+</span><span class="code-normal"> &quot;</span><span class="code-string">nanos</span><span class="code-normal">&quot;: </span><span class="code-number">0
+</span><span class="code-normal"> }
+</span><span class="code-normal"> },
+</span><span class="code-normal"> &quot;</span><span class="code-string">playlist</span><span class="code-normal">&quot;: [
+</span><span class="code-normal"> {
+</span><span class="code-normal"> &quot;</span><span class="code-string">url</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">&lt;temp_url&gt;</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">webpage_url</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">https://www.youtube.com/watch?v=dQw4w9WgXcQ</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">title</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">Rick Astley - Never Gonna Give You Up (Video)</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">thumbnail</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">duration</span><span class="code-normal">&quot;: {
+</span><span class="code-normal"> &quot;</span><span class="code-string">secs</span><span class="code-normal">&quot;: </span><span class="code-number">212</span><span class="code-normal">,
+</span><span class="code-normal"> &quot;</span><span class="code-string">nanos</span><span class="code-normal">&quot;: </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"> &quot;</span><span class="code-string">name</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">MusicBot</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">state</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">Playing</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">volume</span><span class="code-normal">&quot;: </span><span class="code-number">0.5</span><span class="code-normal">,
+</span><span class="code-normal"> &quot;</span><span class="code-string">position</span><span class="code-normal">&quot;: {
+</span><span class="code-normal"> &quot;</span><span class="code-string">secs</span><span class="code-normal">&quot;: </span><span class="code-number">142</span><span class="code-normal">,
+</span><span class="code-normal"> &quot;</span><span class="code-string">nanos</span><span class="code-normal">&quot;: </span><span class="code-number">690911766
+</span><span class="code-normal"> },
+</span><span class="code-normal"> &quot;</span><span class="code-string">currently_playing</span><span class="code-normal">&quot;: {
+</span><span class="code-normal"> &quot;</span><span class="code-string">url</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">&lt;temp_url&gt;</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">webpage_url</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">https://www.youtube.com/watch?v=dQw4w9WgXcQ</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">title</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">Rick Astley - Never Gonna Give You Up (Video)</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">thumbnail</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">duration</span><span class="code-normal">&quot;: {
+</span><span class="code-normal"> &quot;</span><span class="code-string">secs</span><span class="code-normal">&quot;: </span><span class="code-number">212</span><span class="code-normal">,
+</span><span class="code-normal"> &quot;</span><span class="code-string">nanos</span><span class="code-normal">&quot;: </span><span class="code-number">0
+</span><span class="code-normal"> }
+</span><span class="code-normal"> },
+</span><span class="code-normal"> &quot;</span><span class="code-string">playlist</span><span class="code-normal">&quot;: [
+</span><span class="code-normal"> {
+</span><span class="code-normal"> &quot;</span><span class="code-string">url</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">&lt;temp_url&gt;</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">webpage_url</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">https://www.youtube.com/watch?v=dQw4w9WgXcQ</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">title</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">Rick Astley - Never Gonna Give You Up (Video)</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">thumbnail</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">duration</span><span class="code-normal">&quot;: {
+</span><span class="code-normal"> &quot;</span><span class="code-string">secs</span><span class="code-normal">&quot;: </span><span class="code-number">212</span><span class="code-normal">,
+</span><span class="code-normal"> &quot;</span><span class="code-string">nanos</span><span class="code-normal">&quot;: </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"> &quot;</span><span class="code-string">error</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">Not Found</span><span class="code-normal">&quot;,
+</span><span class="code-normal"> &quot;</span><span class="code-string">description</span><span class="code-normal">&quot;: &quot;</span><span class="code-string">The requested resource was not found</span><span class="code-normal">&quot;
+</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>