aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJokler <jokler@protonmail.com>2020-02-22 22:46:06 +0100
committerJokler <jokler@protonmail.com>2020-02-22 23:20:10 +0100
commit84804836f5c1e782c77f1bbf676177151558e008 (patch)
tree55f0ee9664018f6ed0cc41d2cfcf13ca3e0ffe60
parent5eea11a03c11551091b2c72f48590aec7f5410f0 (diff)
downloadpokebot-84804836f5c1e782c77f1bbf676177151558e008.tar.gz
pokebot-84804836f5c1e782c77f1bbf676177151558e008.zip
Add tmtu mode as a front-end
-rw-r--r--src/audio_player.rs6
-rw-r--r--src/bot/master.rs18
-rw-r--r--src/bot/music.rs16
-rw-r--r--src/web_server.rs104
-rw-r--r--src/web_server/front_end_cookie.rs60
-rw-r--r--templates/index.htm5
-rw-r--r--templates/song.htm11
-rw-r--r--templates/tmtu/index.htm145
8 files changed, 350 insertions, 15 deletions
diff --git a/src/audio_player.rs b/src/audio_player.rs
index 4df213f..4bcab56 100644
--- a/src/audio_player.rs
+++ b/src/audio_player.rs
@@ -223,6 +223,12 @@ impl AudioPlayer {
*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()
}
diff --git a/src/bot/master.rs b/src/bot/master.rs
index 67867ef..9b33744 100644
--- a/src/bot/master.rs
+++ b/src/bot/master.rs
@@ -222,6 +222,7 @@ impl MasterBot {
name: name,
state: bot.state(),
volume: bot.volume(),
+ position: bot.position(),
currently_playing: bot.currently_playing(),
playlist: bot.playlist_to_vec(),
})
@@ -237,6 +238,7 @@ impl MasterBot {
name: name.clone(),
state: bot.state(),
volume: bot.volume(),
+ position: bot.position(),
currently_playing: bot.currently_playing(),
playlist: bot.playlist_to_vec(),
};
@@ -247,6 +249,18 @@ impl MasterBot {
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 {
@@ -267,6 +281,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>,
@@ -301,6 +317,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 0def280..41976e5 100644
--- a/src/bot/music.rs
+++ b/src/bot/music.rs
@@ -2,6 +2,7 @@ use std::future::Future;
use std::io::BufRead;
use std::sync::{Arc, RwLock};
use std::thread;
+use std::time::Duration;
use humantime;
use log::{debug, info};
@@ -193,9 +194,10 @@ impl MusicBot {
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.write().expect("RwLock was not poisoned");
@@ -232,6 +234,10 @@ impl MusicBot {
self.player.volume()
}
+ pub fn position(&self) -> Option<Duration> {
+ self.player.position()
+ }
+
pub fn currently_playing(&self) -> Option<AudioMetadata> {
self.player.currently_playing()
}
@@ -278,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));
}
@@ -289,7 +295,7 @@ 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.read().expect("RwLock was not poisoned");
@@ -306,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()?;
diff --git a/src/web_server.rs b/src/web_server.rs
index 02c57e7..80c914c 100644
--- a/src/web_server.rs
+++ b/src/web_server.rs
@@ -1,17 +1,22 @@
use std::sync::Arc;
+use std::time::Duration;
use actix::{Actor, Addr, Handler, Message, SyncArbiter, SyncContext};
use actix_web::{
- get, middleware::Logger, web, App, HttpResponse, HttpServer, Responder, ResponseError,
+ get, http::header, middleware::Logger, post, web, App, Error, HttpResponse, HttpServer,
+ Responder, ResponseError,
};
use askama::actix_web::TemplateIntoResponse;
use askama::Template;
use derive_more::Display;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
use crate::bot::MasterBot;
use crate::youtube_dl::AudioMetadata;
+mod front_end_cookie;
+use front_end_cookie::FrontEnd;
+
pub struct WebServerArgs {
pub domain: String,
pub bind_address: String,
@@ -28,6 +33,8 @@ pub async fn start(args: WebServerArgs) -> std::io::Result<()> {
.data(bot_addr.clone())
.wrap(Logger::default())
.service(index)
+ .service(tmtu_bot)
+ .service(post_front_end)
.service(web::scope("/api").service(get_bot_list).service(get_bot))
.service(actix_files::Files::new("/static", "static/"))
})
@@ -46,6 +53,34 @@ impl Actor for BotExecutor {
type Context = SyncContext<Self>;
}
+#[derive(Deserialize)]
+#[serde(rename_all = "kebab-case")]
+struct FrontEndForm {
+ front_end: FrontEnd,
+}
+
+#[post("/front-end")]
+async fn post_front_end(form: web::Form<FrontEndForm>) -> Result<HttpResponse, Error> {
+ front_end_cookie::set_front_end(form.into_inner().front_end).await
+}
+
+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())
+ }
+}
+
struct BotDataListRequest;
impl Message for BotDataListRequest {
@@ -86,17 +121,32 @@ struct OverviewTemplate<'a> {
bots: &'a [BotData],
}
+#[derive(Template)]
+#[template(path = "tmtu/index.htm")]
+struct TmtuTemplate {
+ bot_names: Vec<String>,
+ bot: Option<BotData>,
+}
+
#[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>>) -> impl Responder {
+async fn index(bot: web::Data<Addr<BotExecutor>>, front: FrontEnd) -> Result<HttpResponse, Error> {
+ match front {
+ FrontEnd::Lazy => lazy_index(bot).await,
+ FrontEnd::Tmtu => tmtu_index(bot).await,
+ }
+}
+
+async fn lazy_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),
@@ -108,6 +158,38 @@ async fn index(bot: web::Data<Addr<BotExecutor>>) -> impl Responder {
.into_response()
}
+async fn tmtu_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()
+}
+
+#[get("/tmtu/{name}")]
+async fn tmtu_bot(
+ bot: web::Data<Addr<BotExecutor>>,
+ name: web::Path<String>,
+ front: FrontEnd,
+) -> Result<HttpResponse, Error> {
+ if front != FrontEnd::Tmtu {
+ return Ok(HttpResponse::Found().header(header::LOCATION, "/").finish());
+ }
+
+ let bot_names = bot.send(BotNameListRequest).await.unwrap().unwrap();
+ if let Some(bot) = bot.send(BotDataRequest(name.into_inner())).await.unwrap() {
+ TmtuTemplate {
+ bot_names,
+ bot: Some(bot),
+ }
+ .into_response()
+ } else {
+ Ok(HttpResponse::Found().header(header::LOCATION, "/").finish())
+ }
+}
+
#[get("/bots")]
async fn get_bot_list(bot: web::Data<Addr<BotExecutor>>) -> impl Responder {
let bot_datas = match bot.send(BotDataListRequest).await.unwrap() {
@@ -149,3 +231,19 @@ async fn get_bot(bot: web::Data<Addr<BotExecutor>>, name: web::Path<String>) ->
Err(ApiErrorKind::NotFound)
}
}
+
+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/front_end_cookie.rs b/src/web_server/front_end_cookie.rs
new file mode 100644
index 0000000..0207933
--- /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 {
+ Lazy,
+ Tmtu,
+}
+
+impl FrontEnd {
+ const COOKIE_NAME: &'static str = "front-end";
+
+ fn cookie(&self) -> String {
+ let name = match self {
+ FrontEnd::Lazy => "lazy",
+ 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("lazy") => return ok(FrontEnd::Lazy),
+ Some("tmtu") => return ok(FrontEnd::Tmtu),
+ _ => (),
+ }
+ }
+ }
+ }
+ }
+
+ ok(FrontEnd::Lazy)
+ }
+}
+
+pub fn set_front_end(front: FrontEnd) -> HttpResponse {
+ HttpResponse::Found()
+ .header(SET_COOKIE, front.cookie())
+ .header(LOCATION, "/")
+ .finish()
+}
diff --git a/templates/index.htm b/templates/index.htm
index 2584603..3183b52 100644
--- a/templates/index.htm
+++ b/templates/index.htm
@@ -4,6 +4,11 @@
{% 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>
+
<ul>
{% for bot in bots %}
<h2>{{ bot.name }}</h1>
diff --git a/templates/song.htm b/templates/song.htm
index 93f4fec..072567a 100644
--- a/templates/song.htm
+++ b/templates/song.htm
@@ -1,10 +1,7 @@
<a href="{{ item.webpage_url }}">{{ item.title }}</a>
-{% match item.duration %}
- {% when Some with (duration) %}
- {% let secs = duration.as_secs() %}
- {% let mins = secs / 60 %}
- {% let submin_secs = secs % 60 %}
- <span>({{ "{:02}"|format(mins) }}:{{ "{:02}"|format(submin_secs) }})</span>
+<span>({{ item.duration|fmt_duration }})</span>
+{% match item.thumbnail %}
+ {% when Some with (thumbnail) %}
+ <img src="{{ thumbnail }}" height="128">
{% when None %}
- <span>(--:--)</span>
{% endmatch %}
diff --git a/templates/tmtu/index.htm b/templates/tmtu/index.htm
new file mode 100644
index 0000000..e7d1922
--- /dev/null
+++ b/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="/tmtu/{{ name }}" class="botname selected">{{ name }}</a></li>
+ {% else %}
+ <li><a href="/tmtu/{{ 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>