diff options
| author | Jokler <jokler@protonmail.com> | 2020-02-22 22:46:06 +0100 |
|---|---|---|
| committer | Jokler <jokler@protonmail.com> | 2020-02-22 23:20:10 +0100 |
| commit | 84804836f5c1e782c77f1bbf676177151558e008 (patch) | |
| tree | 55f0ee9664018f6ed0cc41d2cfcf13ca3e0ffe60 | |
| parent | 5eea11a03c11551091b2c72f48590aec7f5410f0 (diff) | |
| download | pokebot-84804836f5c1e782c77f1bbf676177151558e008.tar.gz pokebot-84804836f5c1e782c77f1bbf676177151558e008.zip | |
Add tmtu mode as a front-end
| -rw-r--r-- | src/audio_player.rs | 6 | ||||
| -rw-r--r-- | src/bot/master.rs | 18 | ||||
| -rw-r--r-- | src/bot/music.rs | 16 | ||||
| -rw-r--r-- | src/web_server.rs | 104 | ||||
| -rw-r--r-- | src/web_server/front_end_cookie.rs | 60 | ||||
| -rw-r--r-- | templates/index.htm | 5 | ||||
| -rw-r--r-- | templates/song.htm | 11 | ||||
| -rw-r--r-- | templates/tmtu/index.htm | 145 |
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> |
