aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/routes')
-rw-r--r--server/src/routes/mod.rs26
-rw-r--r--server/src/routes/stream.rs46
-rw-r--r--server/src/routes/ui/error.rs59
-rw-r--r--server/src/routes/ui/home.rs14
-rw-r--r--server/src/routes/ui/layout.rs20
-rw-r--r--server/src/routes/ui/mod.rs32
-rw-r--r--server/src/routes/ui/node.rs61
-rw-r--r--server/src/routes/ui/style/cantarell.woff2bin0 -> 93888 bytes
-rw-r--r--server/src/routes/ui/style/layout.css45
-rw-r--r--server/src/routes/ui/style/mod.rs51
10 files changed, 354 insertions, 0 deletions
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
new file mode 100644
index 0000000..4491e81
--- /dev/null
+++ b/server/src/routes/mod.rs
@@ -0,0 +1,26 @@
+use crate::AppState;
+use rocket::{catchers, routes, Build, Rocket};
+use stream::r_stream;
+use ui::error::r_not_found;
+use ui::home::r_home;
+use ui::node::r_library_node;
+use ui::style::{r_assets_font, r_assets_style};
+
+pub mod stream;
+pub mod ui;
+
+pub fn build_rocket(state: AppState) -> Rocket<Build> {
+ rocket::build()
+ .manage(state)
+ .register("/", catchers![r_not_found])
+ .mount(
+ "/",
+ routes![
+ r_home,
+ r_library_node,
+ r_assets_style,
+ r_assets_font,
+ r_stream
+ ],
+ )
+}
diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs
new file mode 100644
index 0000000..6ff6982
--- /dev/null
+++ b/server/src/routes/stream.rs
@@ -0,0 +1,46 @@
+use super::ui::error::MyError;
+use crate::AppState;
+use anyhow::anyhow;
+use anyhow::Context;
+use log::debug;
+use log::warn;
+use rocket::{get, http::ContentType, response::stream::ReaderStream, State};
+use std::path::PathBuf;
+use tokio::io::{duplex, DuplexStream};
+use tokio_util::io::SyncIoBridge;
+
+#[get("/stream/<path..>?<selection>")]
+pub fn r_stream(
+ path: PathBuf,
+ selection: String,
+ state: &State<AppState>,
+) -> Result<(ContentType, ReaderStream![DuplexStream]), MyError> {
+ let (a, b) = duplex(1024);
+ let path = path.to_str().unwrap().to_string();
+ let item = state
+ .library
+ .nested(&path)
+ .context("retrieving library node")?
+ .get_item()?;
+ let remuxer = state.remuxer.clone();
+ let selection = selection
+ .split(",")
+ .map(|e| e.parse().map_err(|_| anyhow!("invalid number")))
+ .into_iter()
+ .collect::<Result<Vec<_>, _>>()?;
+
+ let b = SyncIoBridge::new(b);
+ tokio::task::spawn_blocking(move || {
+ if let Err(e) = remuxer.generate_into(
+ b,
+ 0,
+ item.fs_path.parent().unwrap().to_path_buf(),
+ item.data.clone(),
+ selection,
+ ) {
+ warn!("stream stopped: {e}")
+ }
+ });
+ debug!("starting stream");
+ Ok((ContentType::WEBM, ReaderStream::one(a)))
+}
diff --git a/server/src/routes/ui/error.rs b/server/src/routes/ui/error.rs
new file mode 100644
index 0000000..2a1f8e6
--- /dev/null
+++ b/server/src/routes/ui/error.rs
@@ -0,0 +1,59 @@
+use super::{layout::Layout, HtmlTemplate};
+use markup::Render;
+use rocket::http::Status;
+use rocket::{
+ catch,
+ http::ContentType,
+ response::{self, Responder},
+ Request, Response,
+};
+use std::{fmt::Display, io::Cursor};
+
+#[catch(default)]
+pub fn r_not_found<'a>(status: Status, _request: &Request) -> HtmlTemplate<markup::DynRender<'a>> {
+ HtmlTemplate(
+ "Not found".to_string(),
+ markup::new! {
+ h2 { "Error" }
+ p { @format!("{status:?}") }
+ },
+ )
+}
+
+#[derive(Debug)]
+pub struct MyError(anyhow::Error);
+
+impl<'r> Responder<'r, 'static> for MyError {
+ fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
+ let mut out = String::new();
+ Layout {
+ title: "Error".to_string(),
+ main: markup::new! {
+ h2 { "An error occured. Nobody is sorry"}
+ pre.error { @format!("{:?}", self.0) }
+ },
+ }
+ .render(&mut out)
+ .unwrap();
+ Response::build()
+ .header(ContentType::HTML)
+ .streamed_body(Cursor::new(out))
+ .ok()
+ }
+}
+
+impl Display for MyError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+impl From<anyhow::Error> for MyError {
+ fn from(err: anyhow::Error) -> MyError {
+ MyError(err)
+ }
+}
+impl From<std::fmt::Error> for MyError {
+ fn from(err: std::fmt::Error) -> MyError {
+ MyError(anyhow::anyhow!("{err}"))
+ }
+}
diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs
new file mode 100644
index 0000000..df95665
--- /dev/null
+++ b/server/src/routes/ui/home.rs
@@ -0,0 +1,14 @@
+use crate::routes::ui::node::NodePage;
+use crate::{routes::ui::HtmlTemplate, AppState};
+use rocket::{get, State};
+
+#[get("/")]
+pub async fn r_home(state: &State<AppState>) -> HtmlTemplate<markup::DynRender> {
+ HtmlTemplate(
+ "Home".to_string(),
+ markup::new! {
+ p { "Welcome to Jellything" }
+ @NodePage { node: state.library.root.clone() }
+ },
+ )
+}
diff --git a/server/src/routes/ui/layout.rs b/server/src/routes/ui/layout.rs
new file mode 100644
index 0000000..1085f84
--- /dev/null
+++ b/server/src/routes/ui/layout.rs
@@ -0,0 +1,20 @@
+use markup::Render;
+
+markup::define! {
+ Layout<Main: Render>(title: String, main: Main) {
+ @markup::doctype()
+ html {
+ head {
+ title { @title " - Jellything" }
+ link[rel="stylesheet", href="/assets/style.css"];
+ }
+ body {
+ nav {
+ h1 { "Jellything" }
+
+ }
+ #main { @main }
+ }
+ }
+ }
+}
diff --git a/server/src/routes/ui/mod.rs b/server/src/routes/ui/mod.rs
new file mode 100644
index 0000000..aa0259d
--- /dev/null
+++ b/server/src/routes/ui/mod.rs
@@ -0,0 +1,32 @@
+use self::layout::Layout;
+use markup::Render;
+use rocket::{
+ http::ContentType,
+ response::{self, Responder},
+ Request, Response,
+};
+use std::io::Cursor;
+
+pub mod error;
+pub mod home;
+pub mod layout;
+pub mod node;
+pub mod style;
+
+pub struct HtmlTemplate<T>(pub String, pub T);
+
+impl<'r, T: Render> Responder<'r, 'static> for HtmlTemplate<T> {
+ fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
+ let mut out = String::new();
+ Layout {
+ title: self.0,
+ main: self.1,
+ }
+ .render(&mut out)
+ .unwrap();
+ Response::build()
+ .header(ContentType::HTML)
+ .streamed_body(Cursor::new(out))
+ .ok()
+ }
+}
diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs
new file mode 100644
index 0000000..b6d0d30
--- /dev/null
+++ b/server/src/routes/ui/node.rs
@@ -0,0 +1,61 @@
+use super::super::stream::rocket_uri_macro_r_stream;
+use super::error::MyError;
+use crate::{
+ library::{Directory, Item, Node},
+ routes::ui::HtmlTemplate,
+ AppState,
+};
+use anyhow::Context;
+use rocket::{get, uri, State};
+use std::{ops::Deref, path::PathBuf, sync::Arc};
+
+#[get("/library/<path..>")]
+pub async fn r_library_node(
+ path: PathBuf,
+ state: &State<AppState>,
+) -> Result<HtmlTemplate<markup::DynRender>, MyError> {
+ let path = path.to_str().unwrap().to_string();
+ let node = state
+ .library
+ .nested(&path)
+ .context("retrieving library node")?
+ .clone();
+ Ok(HtmlTemplate(
+ format!("{}", node.title()),
+ markup::new! {
+ @NodePage { node: node.clone() }
+ },
+ ))
+}
+
+markup::define! {
+ NodePage(node: Arc<Node>) {
+ @match node.deref() {
+ Node::Directory(dir) => { @DirectoryPage { dir: dir.clone() } }
+ Node::Item(item) => { @ItemPage { item: item.clone() } }
+ }
+ }
+ DirectoryCard(dir: Arc<Directory>) {
+ span { a[href=&uri!(r_library_node(&dir.lib_path)).to_string()] { @dir.data.name } }
+ }
+ DirectoryPage(dir: Arc<Directory>) {
+ h1 { @dir.data.name }
+ ul.directorylisting {
+ @for el in &dir.children {
+ li {
+ span.title { @match el.deref().to_owned() {
+ Node::Directory(dir) => { @DirectoryCard { dir } }
+ Node::Item(item) => { @ItemCard { item } }
+ }}
+ }
+ }
+ }
+ }
+ ItemCard(item: Arc<Item>) {
+ span { a[href=&uri!(r_library_node(&item.lib_path)).to_string()] { @item.data.title } }
+ }
+ ItemPage(item: Arc<Item>) {
+ h1 { @item.data.title }
+ video[src=&uri!(r_stream(&item.lib_path, "1,2")).to_string(), controls] {}
+ }
+}
diff --git a/server/src/routes/ui/style/cantarell.woff2 b/server/src/routes/ui/style/cantarell.woff2
new file mode 100644
index 0000000..76fd894
--- /dev/null
+++ b/server/src/routes/ui/style/cantarell.woff2
Binary files differ
diff --git a/server/src/routes/ui/style/layout.css b/server/src/routes/ui/style/layout.css
new file mode 100644
index 0000000..0612a7e
--- /dev/null
+++ b/server/src/routes/ui/style/layout.css
@@ -0,0 +1,45 @@
+@font-face {
+ font-family: 'Cantarell';
+ src: url(/assets/cantarell.woff2) format('woff2');
+}
+
+* {
+ color: white;
+ font-family: "Cantarell", sans-serif;
+ font-weight: 300;
+ margin: 0px;
+ padding: 0px;
+}
+
+body {
+ background-color: #1a1a1a;
+ width: 100vw;
+}
+
+nav {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ padding: 1em;
+ width: calc(100vw - 2em);
+ height: 2em;
+ background-color: #41414144;
+}
+
+nav h1 {
+ margin: 0px;
+ font-size: 1.5em;
+}
+
+#main {
+ margin-top: 5em;
+ padding: 1em;
+ padding-left: 3em;
+ padding-right: 3em;
+}
+
+.error {
+ padding: 1em;
+ color: rgb(255, 117, 117);
+ font-family: monospace;
+} \ No newline at end of file
diff --git a/server/src/routes/ui/style/mod.rs b/server/src/routes/ui/style/mod.rs
new file mode 100644
index 0000000..f3d751b
--- /dev/null
+++ b/server/src/routes/ui/style/mod.rs
@@ -0,0 +1,51 @@
+use rocket::{get, http::ContentType};
+use std::{
+ fs::{read_to_string, File},
+ io::Read,
+ path::PathBuf,
+ str::FromStr,
+};
+
+fn css_bundle() -> String {
+ if cfg!(debug_assertions) {
+ read_to_string(
+ PathBuf::from_str(file!())
+ .unwrap()
+ .parent()
+ .unwrap()
+ .join("layout.css"),
+ )
+ .unwrap()
+ } else {
+ include_str!("layout.css").to_string()
+ }
+}
+fn font_bundle() -> Vec<u8> {
+ if cfg!(debug_assertions) {
+ let mut woff = Vec::new();
+
+ File::open(
+ PathBuf::from_str(file!())
+ .unwrap()
+ .parent()
+ .unwrap()
+ .join("cantarell.woff2"),
+ )
+ .unwrap()
+ .read_to_end(&mut woff)
+ .unwrap();
+ woff
+ } else {
+ include_bytes!("cantarell.woff2").to_vec()
+ }
+}
+
+#[get("/assets/style.css")]
+pub fn r_assets_style() -> (ContentType, String) {
+ (ContentType::CSS, css_bundle())
+}
+
+#[get("/assets/cantarell.woff2")]
+pub fn r_assets_font() -> (ContentType, Vec<u8>) {
+ (ContentType::WOFF2, font_bundle())
+}