diff options
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | common/src/lib.rs | 5 | ||||
-rw-r--r-- | server/Cargo.toml | 3 | ||||
-rw-r--r-- | server/src/config.rs | 2 | ||||
-rw-r--r-- | server/src/database.rs | 2 | ||||
-rw-r--r-- | server/src/library.rs | 6 | ||||
-rw-r--r-- | server/src/main.rs | 13 | ||||
-rw-r--r-- | server/src/routes/mod.rs | 5 | ||||
-rw-r--r-- | server/src/routes/ui/account/mod.rs | 19 | ||||
-rw-r--r-- | server/src/routes/ui/error.rs | 5 | ||||
-rw-r--r-- | server/src/routes/ui/home.rs | 3 | ||||
-rw-r--r-- | server/src/routes/ui/layout.rs | 6 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 65 | ||||
-rw-r--r-- | server/src/routes/ui/style/layout.css | 67 | ||||
-rw-r--r-- | tools/Cargo.toml | 4 | ||||
-rw-r--r-- | tools/src/bin/create_item.rs | 34 | ||||
-rw-r--r-- | tools/src/bin/import.rs | 33 |
17 files changed, 193 insertions, 80 deletions
@@ -829,6 +829,7 @@ dependencies = [ "jellyremuxer", "log", "markup", + "once_cell", "rocket", "serde", "serde_json", diff --git a/common/src/lib.rs b/common/src/lib.rs index d9e443b..a4380c6 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,7 +1,7 @@ pub mod r#impl; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, path::PathBuf}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct DirectoryInfo { @@ -12,7 +12,8 @@ pub struct DirectoryInfo { pub struct ItemInfo { pub title: String, pub duration: f64, // in seconds - pub path: String, + pub path: PathBuf, + pub banner: Option<PathBuf>, pub tracks: BTreeMap<u64, SourceTrack>, } diff --git a/server/Cargo.toml b/server/Cargo.toml index b8a57f2..482baa8 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -13,12 +13,13 @@ serde_json = "1.0.91" log = "0.4.17" env_logger = "0.10.0" anyhow = "1.0.68" +once_cell = "1.17.0" +chashmap = "2.2.2" rocket = "0.5.0-rc.2" tokio = { version = "1.24.1", features = ["io-util"] } tokio-util = { version = "0.7.4", features = ["io", "io-util"] } markup = "0.13.1" -chashmap = "2.2.2" sled = "0.34.7" typed-sled = "0.2.3" diff --git a/server/src/config.rs b/server/src/config.rs index 1edf5e5..10b5b5f 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -2,7 +2,7 @@ use std::{fs::File, path::PathBuf}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Default)] pub struct GlobalConfig { pub brand: String, pub database_path: PathBuf, diff --git a/server/src/database.rs b/server/src/database.rs index 8574189..39b306c 100644 --- a/server/src/database.rs +++ b/server/src/database.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use anyhow::Context; use log::info; +use serde::{Deserialize, Serialize}; use typed_sled::Tree; pub struct Database { @@ -9,6 +10,7 @@ pub struct Database { pub users: Tree<String, User>, } +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { pub name: String, pub display_name: String, diff --git a/server/src/library.rs b/server/src/library.rs index 8c2e809..a48e746 100644 --- a/server/src/library.rs +++ b/server/src/library.rs @@ -2,7 +2,6 @@ use anyhow::{anyhow, bail, Context, Ok}; use jellycommon::{DirectoryInfo, ItemInfo}; use log::info; use std::{ - ffi::OsStr, fs::File, path::{Path, PathBuf}, sync::Arc, @@ -102,10 +101,7 @@ impl Node { .read_dir()? .filter_map(|e| { let e = e.unwrap(); - // TODO - if ((e.path().extension() != Some(OsStr::new("mkv")) - && e.path().extension() != Some(OsStr::new("webm"))) - || e.metadata().unwrap().is_dir()) + if (e.path().extension() == None || e.metadata().unwrap().is_dir()) && !e.path().ends_with("directory.json") { Some(e.path()) diff --git a/server/src/main.rs b/server/src/main.rs index 39d5513..29d51ca 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -4,6 +4,7 @@ use config::{load_global_config, GlobalConfig}; use database::Database; use jellyremuxer::RemuxerContext; use library::Library; +use once_cell::sync::Lazy; use rocket::launch; use routes::build_rocket; use std::sync::Arc; @@ -19,14 +20,18 @@ pub struct AppState { pub remuxer: Arc<RemuxerContext>, } +pub static CONF: Lazy<GlobalConfig> = Lazy::new(|| load_global_config()); + #[launch] fn rocket() -> _ { - env_logger::init_from_env("LOG"); - let conf = load_global_config(); + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .parse_env("LOG") + .init(); let state = AppState { remuxer: RemuxerContext::new(), - library: Library::open(&conf.library_path).unwrap(), - database: Database::open(&conf.database_path).unwrap(), + library: Library::open(&CONF.library_path).unwrap(), + database: Database::open(&CONF.database_path).unwrap(), }; build_rocket(state) } diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index f74ed65..51d32d1 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -4,7 +4,7 @@ use stream::r_stream; use ui::account::{r_account_login, r_account_register, r_account_register_post}; use ui::error::r_not_found; use ui::home::r_home; -use ui::node::r_library_node; +use ui::node::{r_item_assets, r_library_node}; use ui::player::r_player; use ui::style::{r_assets_font, r_assets_style}; @@ -26,7 +26,8 @@ pub fn build_rocket(state: AppState) -> Rocket<Build> { r_player, r_account_login, r_account_register, - r_account_register_post + r_account_register_post, + r_item_assets, ], ) } diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs index 86e0f18..7e329a1 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/routes/ui/account/mod.rs @@ -1,6 +1,8 @@ use super::HtmlTemplate; +use crate::database::User; +use crate::{AppState, CONF}; use rocket::form::Form; -use rocket::{get, post, FromForm}; +use rocket::{get, post, FromForm, State}; #[derive(FromForm)] pub struct RegisterForm { @@ -17,7 +19,7 @@ pub fn r_account_register() -> HtmlTemplate<markup::DynRender<'static>> { HtmlTemplate( "Register".to_string(), markup::new! { - h1 { "Register for Jellything" } + h1 { "Register for " @CONF.brand } form[method="POST", action=""] { label[for="inp-invitation"] { "Invite Code: " } input[type="text", id="inp-invitation", name="invitation"]; br; @@ -46,8 +48,21 @@ pub fn r_account_login() -> HtmlTemplate<markup::DynRender<'static>> { #[post("/account/register", data = "<form>")] pub fn r_account_register_post( + state: &State<AppState>, form: Form<RegisterForm>, ) -> HtmlTemplate<markup::DynRender<'static>> { + state + .database + .users + .insert( + &form.username, + &User { + display_name: form.username.clone(), + name: form.username.clone(), + password: form.password.clone().into(), // TODO hash it + }, + ) + .unwrap(); HtmlTemplate( "Registration successful".to_string(), markup::new! { diff --git a/server/src/routes/ui/error.rs b/server/src/routes/ui/error.rs index 0b24b62..1a50796 100644 --- a/server/src/routes/ui/error.rs +++ b/server/src/routes/ui/error.rs @@ -59,3 +59,8 @@ impl From<std::fmt::Error> for MyError { MyError(anyhow::anyhow!("{err}")) } } +impl From<std::io::Error> for MyError { + fn from(err: std::io::Error) -> Self { + MyError(anyhow::anyhow!("{err}")) + } +} diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs index df95665..04a4c7d 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/routes/ui/home.rs @@ -1,4 +1,5 @@ use crate::routes::ui::node::NodePage; +use crate::CONF; use crate::{routes::ui::HtmlTemplate, AppState}; use rocket::{get, State}; @@ -7,7 +8,7 @@ pub async fn r_home(state: &State<AppState>) -> HtmlTemplate<markup::DynRender> HtmlTemplate( "Home".to_string(), markup::new! { - p { "Welcome to Jellything" } + p { "Welcome to " @CONF.brand } @NodePage { node: state.library.root.clone() } }, ) diff --git a/server/src/routes/ui/layout.rs b/server/src/routes/ui/layout.rs index f333fa1..51905db 100644 --- a/server/src/routes/ui/layout.rs +++ b/server/src/routes/ui/layout.rs @@ -1,16 +1,18 @@ use markup::Render; +use crate::CONF; + markup::define! { Layout<Main: Render>(title: String, main: Main) { @markup::doctype() html { head { - title { @title " - Jellything" } + title { @title " - " @CONF.brand } link[rel="stylesheet", href="/assets/style.css"]; } body { nav { - h1 { a[href="/"] { "Jellything" } } + h1 { a[href="/"] { @CONF.brand } } a[href="/library"] { "My Library" } } #main { @main } diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 64f5e84..9162982 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -5,9 +5,11 @@ use crate::{ routes::ui::HtmlTemplate, AppState, }; -use anyhow::Context; +use anyhow::{anyhow, Context}; +use log::info; use rocket::{get, uri, State}; use std::{ops::Deref, path::PathBuf, sync::Arc}; +use tokio::fs::File; #[get("/library/<path..>")] pub async fn r_library_node( @@ -35,27 +37,66 @@ markup::define! { } } DirectoryCard(dir: Arc<Directory>) { - span { a[href=&uri!(r_library_node(&dir.lib_path)).to_string()] { @dir.data.name } } + div.card.dir { 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() { + div.page.dir { + h1 { @dir.data.name } + ul.directorylisting { + @for el in &dir.children { + li { @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.info.title } } + div.card.item { + a[href=&uri!(r_library_node(&item.lib_path)).to_string()] { + img[src=uri!(r_item_assets(&item.lib_path)).to_string()]; + } + a[href=&uri!(r_library_node(&item.lib_path)).to_string()] { + p.title { @item.info.title } + } + } } ItemPage(item: Arc<Item>) { - h1 { @item.info.title } - a[href=&player_uri(&item.lib_path)] { "Watch now" } - p { "Lorem ipsum dolor sit amed...." } + div.page.item { + div.backdrop { + // TODO different image here + img[src=uri!(r_item_assets(&item.lib_path)).to_string()]; + } + div.banner { + img[src=uri!(r_item_assets(&item.lib_path)).to_string()]; + } + div.title { + h1 { @item.info.title } + a[href=&player_uri(&item.lib_path)] { "Watch now" } + } + div.details { + h3 { "Lorem Ipsum!" } + p { "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." } + } + } } } + +#[get("/item_assets/<path..>")] +pub async fn r_item_assets(path: PathBuf, state: &State<AppState>) -> Result<File, MyError> { + let node = state + .library + .nested_path(&path) + .context("retrieving library node")? + .get_item()? + .clone(); + let path = node.fs_path.parent().unwrap().join( + node.info + .banner + .clone() + .ok_or(anyhow!("no banner available"))?, + ); + info!("loading asset from {path:?}"); + Ok(File::open(path).await?) +} diff --git a/server/src/routes/ui/style/layout.css b/server/src/routes/ui/style/layout.css index 078dd4e..6a63ccc 100644 --- a/server/src/routes/ui/style/layout.css +++ b/server/src/routes/ui/style/layout.css @@ -3,6 +3,10 @@ src: url(/assets/cantarell.woff2) format("woff2"); } +:root { + --card-size: 12em; +} + * { color: rgb(218, 218, 218); font-family: "Cantarell", sans-serif; @@ -10,8 +14,16 @@ } body { - background-color: #0f0f0f; + background-color: #070707; width: 100vw; + margin: 0px; + padding: 0px; +} + +img { + width: 100%; + height: 100%; + object-fit: cover; } nav { @@ -21,7 +33,7 @@ nav { padding: 1em; width: calc(100vw - 2em); height: 2em; - background-color: #27272744; + background-color: #1c1c1c9a; } nav h1 { @@ -32,10 +44,6 @@ nav h1 { } #main { - margin-top: 5em; - padding: 1em; - padding-left: 3em; - padding-right: 3em; } .error { @@ -52,3 +60,50 @@ input { option { font-family: "Cantarell", sans-serif; } + +.page.dir { + margin-top: 5em; + padding: 1em; + padding-left: 3em; + padding-right: 3em; +} +.directorylisting { + list-style: none; + display: flex; + flex-wrap: wrap; +} +.directorylisting li { + display: block; +} + +.card.item { + width: var(--card-size); + height: calc(var(--card-size) * 1.41); + padding: 1em; +} +.card.dir { + width: calc(var(--card-size) * 2); + height: calc(var(--card-size) * 1.41); +} +.card a { + width: 100%; + height: 100%; +} + +.card.item .title { + margin-top: 0.1em; + text-align: center; + text-overflow: ellipsis; +} + +.page.item .backdrop { + width: 100%; + margin-bottom: calc(-100% * 1.41 + 18em); +} +.page.item .backdrop img { + mask-mode: alpha; + mask-image: linear-gradient(rgba(0, 0, 0, 1), transparent 30%); +} +.page.item .banner { + width: max(8em, 20%); +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index cba6c82..9e9b965 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -16,9 +16,5 @@ anyhow = "1.0.68" serde_json = "1.0.91" [[bin]] -path = "src/bin/create_item.rs" -name = "jellything-create-item" - -[[bin]] path = "src/bin/import.rs" name = "jellything-import" diff --git a/tools/src/bin/create_item.rs b/tools/src/bin/create_item.rs deleted file mode 100644 index eb5b3a9..0000000 --- a/tools/src/bin/create_item.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::{fs::File, io::Write, path::PathBuf}; - -use clap::Parser; -use jellycommon::ItemInfo; - -#[derive(Parser)] -struct Args { - #[clap(short = 'I', long)] - item: PathBuf, - #[clap(short = 'd', long)] - dry: bool, - #[clap(short, long)] - title: String, -} - -fn main() -> anyhow::Result<()> { - let args = Args::parse(); - - let iteminfo = ItemInfo { - title: args.title, - path: String::new(), - duration: 0.0, - tracks: Default::default(), - }; - - let k = serde_json::to_string_pretty(&iteminfo)?; - if args.dry { - println!("{k}") - } else { - let mut f = File::create(args.item)?; - f.write_all(k.as_bytes())?; - } - Ok(()) -} diff --git a/tools/src/bin/import.rs b/tools/src/bin/import.rs index 98f5fb7..4af6ac7 100644 --- a/tools/src/bin/import.rs +++ b/tools/src/bin/import.rs @@ -2,26 +2,51 @@ use clap::Parser; use jellycommon::ItemInfo; use jellymatroska::read::EbmlReader; use jellyremuxer::import::import_read; +use log::warn; use std::{fs::File, io::Write, path::PathBuf}; #[derive(Parser)] struct Args { #[clap(short = 'I', long)] item: PathBuf, + + #[clap(short, long)] + banner: Option<PathBuf>, + + #[clap(short, long)] + title: Option<String>, + #[clap(short = 'd', long)] dry: bool, + #[clap(short = 'i', long)] input: PathBuf, } fn main() -> anyhow::Result<()> { - env_logger::init_from_env("LOG"); + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .parse_env("LOG") + .init(); let args = Args::parse(); - let mut iteminfo: ItemInfo = serde_json::from_reader(File::open(&args.item)?)?; - // let iteminfo_orig = iteminfo.clone(); + let mut iteminfo: ItemInfo = match File::open(&args.item) { + Ok(f) => serde_json::from_reader(f)?, + Err(e) => { + warn!("could not load item info: {e}"); + warn!("using the default instead"); + ItemInfo { + duration: 0.0, + path: args.input.clone(), + banner: args.banner, + title: args + .title + .unwrap_or(args.item.to_str().unwrap().to_string()), + tracks: Default::default(), + } + } + }; - iteminfo.path = args.input.to_str().unwrap().to_string(); let input = File::open(args.input.clone()).unwrap(); let mut input = EbmlReader::new(input); |