diff options
| author | Jokler <jokler@protonmail.com> | 2020-02-02 19:50:33 +0100 |
|---|---|---|
| committer | Jokler <jokler@protonmail.com> | 2020-02-22 23:20:10 +0100 |
| commit | 2831c2b60cb61a14c7efee4ab5c0389eb3ad5469 (patch) | |
| tree | 835f1abad6e234f6d74d4be999f690709954be89 | |
| parent | ca4c0158f417b87f04313053a3f656f2de4e803b (diff) | |
| download | pokebot-2831c2b60cb61a14c7efee4ab5c0389eb3ad5469.tar.gz pokebot-2831c2b60cb61a14c7efee4ab5c0389eb3ad5469.zip | |
Add a very basic template using available info
| -rw-r--r-- | Cargo.lock | 123 | ||||
| -rw-r--r-- | Cargo.toml | 8 | ||||
| -rw-r--r-- | src/audio_player.rs | 29 | ||||
| -rw-r--r-- | src/bot/master.rs | 22 | ||||
| -rw-r--r-- | src/bot/music.rs | 32 | ||||
| -rw-r--r-- | src/playlist.rs | 10 | ||||
| -rw-r--r-- | src/web_server.rs | 63 | ||||
| -rw-r--r-- | src/youtube_dl.rs | 28 | ||||
| -rw-r--r-- | static/.gitkeep | 0 | ||||
| -rw-r--r-- | templates/base.htm | 14 | ||||
| -rw-r--r-- | templates/index.htm | 27 | ||||
| -rw-r--r-- | templates/song.htm | 10 |
12 files changed, 334 insertions, 32 deletions
@@ -60,6 +60,26 @@ dependencies = [ ] [[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" @@ -392,6 +412,54 @@ 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" @@ -1572,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" @@ -1973,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" @@ -2272,8 +2356,10 @@ name = "pokebot" version = "0.1.1" dependencies = [ "actix", + "actix-files", "actix-rt", "actix-web", + "askama", "byte-slice-cast", "futures 0.1.29", "futures-preview", @@ -3077,7 +3163,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", @@ -3645,7 +3731,7 @@ dependencies = [ "bytes 0.4.12", "derive_more 0.14.1", "failure", - "nom", + "nom 4.2.3", "num-derive", "num-traits", "rental", @@ -3797,6 +3883,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" @@ -3926,7 +4043,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" dependencies = [ - "nom", + "nom 4.2.3", ] [[package]] @@ -34,7 +34,9 @@ gstreamer-audio = "0.15.0" byte-slice-cast = "0.3.5" serde_json = "1.0.44" serde = "1.0.104" -rand = { version = "0.7.3", features = ["small_rng"] } -actix-web = "2.0.0" -actix-rt = "1.0.0" 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"] } diff --git a/src/audio_player.rs b/src/audio_player.rs index cdb04d7..4df213f 100644 --- a/src/audio_player.rs +++ b/src/audio_player.rs @@ -13,6 +13,8 @@ use log::{debug, error, info, warn}; 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<RwLock<UnboundedSender<MusicBotMessage>>>, + currently_playing: RwLock<Option<AudioMetadata>>, } fn make_element(factoryname: &str, display_name: &str) -> Result<gst::Element, AudioPlayerError> { @@ -111,8 +115,10 @@ impl AudioPlayer { bus, http_src, + volume_f64: RwLock::new(0.0), volume, sender, + currently_playing: RwLock::new(None), }) } @@ -173,7 +179,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 +196,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 +219,20 @@ impl AudioPlayer { } } + pub fn volume(&self) -> f64 { + *self.volume_f64.read().unwrap() + } + + 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(()) diff --git a/src/bot/master.rs b/src/bot/master.rs index bc38cca..10a7572 100644 --- a/src/bot/master.rs +++ b/src/bot/master.rs @@ -213,14 +213,24 @@ impl MasterBot { Ok(()) } - pub fn names(&self) -> Vec<String> { + pub fn bot_datas(&self) -> Vec<crate::web_server::BotData> { let music_bots = self.music_bots.read().unwrap(); - music_bots - .connected_bots - .iter() - .map(|(_, b)| b.name().to_owned()) - .collect() + 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(), + currently_playing: bot.currently_playing(), + playlist: bot.playlist_to_vec(), + }; + + result.push(bot_data); + } + + result } pub fn quit(&self, reason: String) { diff --git a/src/bot/music.rs b/src/bot/music.rs index 920f1cb..d53e4a8 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -44,7 +44,7 @@ fn parse_seek(mut amount: &str) -> Result<Seek, ()> { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum State { Playing, Paused, @@ -52,6 +52,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), @@ -176,7 +188,7 @@ impl MusicBot { 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(); } @@ -211,6 +223,22 @@ 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 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() 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/web_server.rs b/src/web_server.rs index 1edbc50..94f043a 100644 --- a/src/web_server.rs +++ b/src/web_server.rs @@ -1,21 +1,12 @@ use std::sync::Arc; use actix::{Actor, Addr, Handler, Message, SyncArbiter, SyncContext}; -use actix_web::{get, middleware::Logger, web, App, HttpResponse, HttpServer, Responder}; +use actix_web::{get, middleware::Logger, web, App, HttpServer, Responder}; +use askama::actix_web::TemplateIntoResponse; +use askama::Template; use crate::bot::MasterBot; - -struct GetNames; - -impl Message for GetNames { - type Result = Result<Vec<String>, ()>; -} - -#[get("/")] -async fn index(bot: web::Data<Addr<BotExecutor>>) -> impl Responder { - let names = bot.send(GetNames).await.unwrap().unwrap(); - HttpResponse::Ok().body(&format!("Music bots connected: {}", names.join(", "))) -} +use crate::youtube_dl::AudioMetadata; pub struct WebServerArgs { pub domain: String, @@ -33,6 +24,7 @@ pub async fn start(args: WebServerArgs) -> std::io::Result<()> { .data(bot_addr.clone()) .wrap(Logger::default()) .service(index) + .service(actix_files::Files::new("/static", "static/")) }) .bind(args.bind_address)? .run() @@ -49,12 +41,49 @@ impl Actor for BotExecutor { type Context = SyncContext<Self>; } -impl Handler<GetNames> for BotExecutor { - type Result = Result<Vec<String>, ()>; +impl Handler<PlaylistRequest> for BotExecutor { + type Result = Result<Vec<BotData>, ()>; - fn handle(&mut self, _: GetNames, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, _: PlaylistRequest, _: &mut Self::Context) -> Self::Result { let bot = &self.0; - Ok(bot.names()) + Ok(bot.bot_datas()) + } +} + +struct PlaylistRequest; + +impl Message for PlaylistRequest { + type Result = Result<Vec<BotData>, ()>; +} + +#[derive(Template)] +#[template(path = "index.htm")] +struct PlaylistTemplate<'a> { + bots: &'a [BotData], +} + +#[derive(Debug)] +pub struct BotData { + pub name: String, + pub state: crate::bot::State, + pub volume: f64, + pub currently_playing: Option<AudioMetadata>, + pub playlist: Vec<AudioMetadata>, +} + +#[get("/")] +async fn index(bot: web::Data<Addr<BotExecutor>>) -> impl Responder { + let bot_datas = match bot.send(PlaylistRequest).await.unwrap() { + Ok(data) => data, + Err(_) => { + //error!("Playlist error: {}", e); + Vec::with_capacity(0) + } + }; + + PlaylistTemplate { + bots: &bot_datas[..], } + .into_response() } diff --git a/src/youtube_dl.rs b/src/youtube_dl.rs index b62d4b3..99e50e7 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,33 @@ use log::debug; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct AudioMetadata { pub url: String, + pub webpage_url: String, pub title: String, + pub thumbnail: Option<String>, + #[serde(default, deserialize_with = "duration_deserialize")] + #[serde(serialize_with = "duration_serialize")] + pub duration: Option<Duration>, + #[serde(skip)] + pub added_by: String, +} + +fn duration_serialize<S>(d: &Option<Duration>, s: S) -> Result<S::Ok, S::Error> +where + S: serde::Serializer, +{ + match d { + Some(d) => s.serialize_some(&d.as_secs_f64()), + None => s.serialize_none(), + } +} + +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/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/static/.gitkeep diff --git a/templates/base.htm b/templates/base.htm new file mode 100644 index 0000000..7810f21 --- /dev/null +++ b/templates/base.htm @@ -0,0 +1,14 @@ +<!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"> + <title>{% block title %}{{ title }} - PokeBot{% endblock %}</title> + </head> + <body> + <main> + {% block content %}{% endblock %} + </main> + </body> +</html> diff --git a/templates/index.htm b/templates/index.htm new file mode 100644 index 0000000..2584603 --- /dev/null +++ b/templates/index.htm @@ -0,0 +1,27 @@ +{% extends "base.htm" %} + +{% block title %}Overview{% endblock %} + +{% block content %} +<h1>Bots</h1> +<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/templates/song.htm b/templates/song.htm new file mode 100644 index 0000000..93f4fec --- /dev/null +++ b/templates/song.htm @@ -0,0 +1,10 @@ +<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> + {% when None %} + <span>(--:--)</span> +{% endmatch %} |
