diff options
Diffstat (limited to 'server/src/routes/ui')
-rw-r--r-- | server/src/routes/ui/assets.rs | 44 | ||||
-rw-r--r-- | server/src/routes/ui/browser.rs | 31 | ||||
-rw-r--r-- | server/src/routes/ui/home.rs | 8 | ||||
-rw-r--r-- | server/src/routes/ui/mod.rs | 1 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 172 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 49 |
6 files changed, 161 insertions, 144 deletions
diff --git a/server/src/routes/ui/assets.rs b/server/src/routes/ui/assets.rs new file mode 100644 index 0000000..2c0b85a --- /dev/null +++ b/server/src/routes/ui/assets.rs @@ -0,0 +1,44 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2023 metamuffin <metamuffin.org> +*/ +use crate::{ + database::Database, + routes::ui::{account::session::Session, error::MyError, CacheControlFile}, + CONF, +}; +use anyhow::anyhow; +use log::info; +use rocket::{get, http::ContentType, FromFormField, State, UriDisplayQuery}; +use std::{path::PathBuf, str::FromStr}; +use tokio::fs::File; + +#[derive(FromFormField, UriDisplayQuery)] +pub enum AssetRole { + Poster, + Backdrop, +} + +#[get("/item_assets/<id>?<role>")] +pub async fn r_item_assets( + _sess: Session, + id: String, + role: AssetRole, + db: &State<Database>, +) -> Result<(ContentType, CacheControlFile), MyError> { + let node = db.node.get(&id)?.ok_or(anyhow!("node does not exist"))?; + let path = CONF.asset_path.join( + match role { + AssetRole::Backdrop => node.private.backdrop, + AssetRole::Poster => node.private.poster, + } + .unwrap_or_else(|| PathBuf::from_str("fallback.jpeg").unwrap()), + ); + info!("loading asset from {path:?}"); + let ext = path.extension().unwrap().to_str().unwrap(); + Ok(( + ContentType::from_extension(ext).unwrap(), + CacheControlFile::new(File::open(path).await?).await, + )) +} diff --git a/server/src/routes/ui/browser.rs b/server/src/routes/ui/browser.rs index 30eb3f2..767e411 100644 --- a/server/src/routes/ui/browser.rs +++ b/server/src/routes/ui/browser.rs @@ -3,35 +3,24 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ -use super::{account::session::Session, error::MyError, layout::DynLayoutPage, node::PosterCard}; -use crate::library::{Library, Node}; +use super::{account::session::Session, error::MyError, layout::DynLayoutPage}; +use crate::database::Database; use rocket::{get, State}; -use std::collections::VecDeque; #[get("/items")] -pub fn r_all_items(_sess: Session, library: &State<Library>) -> Result<DynLayoutPage<'_>, MyError> { - let mut dirs = VecDeque::from_iter(Some(library.root.get_directory().unwrap())); - let mut items = Vec::new(); - while let Some(d) = dirs.pop_front() { - for e in &d.children { - match e.as_ref() { - Node::Directory(d) => dirs.push_back(d.clone()), - Node::Item(i) => items.push(i.clone()), - } - } - } +pub fn r_all_items(_sess: Session, db: &State<Database>) -> Result<DynLayoutPage<'_>, MyError> { Ok(super::layout::LayoutPage { title: "All Items".to_owned(), content: markup::new! { .page.dir { h1 { "All Items" } - ul.directorylisting { @for item in &items { - li {@PosterCard { - wide: false, dir: false, - path: item.lib_path.clone(), - title: &item.info.title - }} - }} + // ul.directorylisting { @for item in &items { + // li {@PosterCard { + // wide: false, dir: false, + // path: item.lib_path.clone(), + // title: &item.info.title + // }} + // }} } }, ..Default::default() diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs index cdde478..0b85e89 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/routes/ui/home.rs @@ -5,20 +5,20 @@ */ use super::{account::session::Session, layout::LayoutPage}; use crate::{ - library::Library, - routes::ui::{error::MyResult, layout::DynLayoutPage, node::NodePage}, + database::Database, + routes::ui::{error::MyResult, layout::DynLayoutPage}, CONF, }; use rocket::{get, State}; use tokio::fs::read_to_string; #[get("/")] -pub fn r_home(_sess: Session, library: &State<Library>) -> DynLayoutPage { +pub fn r_home(_sess: Session, _db: &State<Database>) -> DynLayoutPage { LayoutPage { title: "Home".to_string(), content: markup::new! { p { "Welcome to " @CONF.brand } - @NodePage { node: &library.root } + // @NodePage { node: &db } }, ..Default::default() } diff --git a/server/src/routes/ui/mod.rs b/server/src/routes/ui/mod.rs index f566f6d..5fad2b7 100644 --- a/server/src/routes/ui/mod.rs +++ b/server/src/routes/ui/mod.rs @@ -29,6 +29,7 @@ pub mod layout; pub mod node; pub mod player; pub mod style; +pub mod assets; pub struct HtmlTemplate<'a>(pub markup::DynRender<'a>); diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 4d599dc..0ae0d9e 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -1,163 +1,141 @@ /* -This file is part of jellything (https://codeberg.org/metamuffin/jellything) -which is licensed under the GNU Affero General Public License (version 3); see /COPYING. -Copyright (C) 2023 metamuffin <metamuffin.org> + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2023 metamuffin <metamuffin.org> */ -use super::error::MyError; -use super::player::player_uri; -use super::CacheControlFile; -use crate::uri; +use super::{assets::rocket_uri_macro_r_item_assets, error::MyError, player::player_uri}; use crate::{ - library::{Directory, Item, Library, Node}, + database::Database, routes::ui::{ account::session::Session, + assets::AssetRole, layout::{DynLayoutPage, LayoutPage}, }, + uri, }; -use anyhow::Context; -use jellycommon::DirectoryKind; -use log::info; -use rocket::{get, http::ContentType, State}; -use rocket::{FromFormField, UriDisplayQuery}; -use std::{path::PathBuf, sync::Arc}; -use tokio::fs::File; +use anyhow::{anyhow, Context}; +use jellycommon::{Node, NodeKind}; +use rocket::{get, State}; -#[get("/library/<path..>")] +#[get("/library/<id>")] pub async fn r_library_node( _sess: Session, - path: PathBuf, - library: &State<Library>, + id: String, + db: &State<Database>, ) -> Result<DynLayoutPage<'_>, MyError> { - let node = library - .nested_path(&path) - .context("retrieving library node")?; + let node = db + .node + .get(&id) + .context("retrieving library node")? + .ok_or(anyhow!("node does not exist"))?; + + let children = node + .public + .children + .iter() + .map(|c| { + Ok(( + c.to_owned(), + db.node.get(c)?.ok_or(anyhow!("child does not exist"))?, + )) + }) + .collect::<anyhow::Result<Vec<_>>>()? + .into_iter() + .collect(); + Ok(LayoutPage { - title: node.common().title.to_string(), - show_back: node.get_item().is_ok(), + title: node.public.title.to_string(), + show_back: matches!(node.public.kind, NodeKind::Movie | NodeKind::Episode), content: markup::new! { - @NodePage { node: &node } + @NodePage { node: &node, id: &id, children: &children } }, ..Default::default() }) } markup::define! { - NodePage<'a>(node: &'a Arc<Node>) { - @match node.as_ref() { - Node::Directory(dir) => { @match dir.info.kind { - DirectoryKind::Series => { @SeriesPage { dir } } - _ => { @DirectoryPage { dir } } - } } - Node::Item(item) => { @ItemPage { item } } + NodePage<'a>(id: &'a str, node: &'a Node, children: &'a Vec<(String,Node)>) { + @match node.public.kind { + NodeKind::Collection | NodeKind::Show | NodeKind::Season => { @DirectoryPage { node, id, children } } + NodeKind::Series => { @SeriesPage { node, children, id } } + NodeKind::Movie | NodeKind::Episode => { @ItemPage { node, id } } } } - NodeCard<'a>(node: &'a Arc<Node>) { - @match node.as_ref() { - Node::Directory(dir) => {@PosterCard { - wide: !matches!(dir.info.kind, DirectoryKind::Series | DirectoryKind::Season), - dir: true, - path: dir.lib_path.clone(), - title: &dir.info.title - }} - Node::Item(item) => {@PosterCard { - wide: false, dir: false, - path: item.lib_path.clone(), - title: &item.info.title - }} + NodeCard<'a>(id: &'a str, node: &'a Node) { + @PosterCard { + wide: matches!(node.public.kind, NodeKind::Collection), + dir: !matches!(node.public.kind, NodeKind::Movie | NodeKind::Episode), + id, + title: &node.public.title } } - DirectoryPage<'a>(dir: &'a Arc<Directory>) { + DirectoryPage<'a>(id: &'a str, node: &'a Node, children: &'a Vec<(String,Node)>) { div.page.dir { - h1 { @dir.info.title } - @if let Some(parent) = dir.lib_path.parent() { - a.dirup[href=uri!(r_library_node(&parent))] { "Go up" } - } + h1 { @node.public.title } + // @if let Some(parent) = node.lib_path.parent() { + // a.dirup[href=uri!(r_library_node(&parent))] { "Go up" } + // } ul.directorylisting { - @for node in &dir.children { - li { @NodeCard { node } } + @for (id, node) in children.iter() { + li { @NodeCard { id, node } } } } } } - PosterCard<'a>(path: PathBuf, title: &'a str, wide: bool, dir: bool) { + PosterCard<'a>(id: &'a str, title: &'a str, wide: bool, dir: bool) { div[class=if *wide {"card wide poster"} else {"card poster"}] { div.banner { - a[href=uri!(r_library_node(path))] { - img[src=uri!(r_item_assets(path, AssetRole::Poster))]; + a[href=uri!(r_library_node(id))] { + img[src=uri!(r_item_assets(id, AssetRole::Poster))]; } @if *dir { - div.hoverdir { a[href=&uri!(r_library_node(path))] { "Open" } } + div.hoverdir { a[href=&uri!(r_library_node(id))] { "Open" } } } else { - div.hoveritem { a[href=&player_uri(path)] { "▶" } } + div.hoveritem { a[href=&player_uri(id)] { "▶" } } } } p.title { - a[href=uri!(r_library_node(path))] { + a[href=uri!(r_library_node(id))] { @title } } } } - ItemPage<'a>(item: &'a Arc<Item>) { + ItemPage<'a>(id: &'a str, node: &'a Node) { // TODO different image here - img.backdrop[src=uri!(r_item_assets(&item.lib_path, AssetRole::Backdrop))]; + img.backdrop[src=uri!(r_item_assets(id, AssetRole::Backdrop))]; div.page.item { div.banner { - img[src=uri!(r_item_assets(&item.lib_path, AssetRole::Poster))]; + img[src=uri!(r_item_assets(id, AssetRole::Poster))]; } div.title { - h1 { @item.info.title } + h1 { @node.public.title } // TODO release date, duration, ratings - a.play[href=&player_uri(&item.lib_path)] { "Watch now" } + a.play[href=&player_uri(id)] { "Watch now" } } div.details { - h3 { @item.info.tagline } - p { @item.info.description } + h3 { @node.public.tagline } + p { @node.public.description } } } } - SeriesPage<'a>(dir: &'a Arc<Directory>) { + SeriesPage<'a>(id: &'a str, node: &'a Node, children: &'a Vec<(String,Node)>) { // TODO different image here - img.backdrop[src=uri!(r_item_assets(&dir.lib_path, AssetRole::Backdrop))]; + img.backdrop[src=uri!(r_item_assets(id, AssetRole::Backdrop))]; div.page.item { div.banner { - img[src=uri!(r_item_assets(&dir.lib_path, AssetRole::Poster))]; + img[src=uri!(r_item_assets(id, AssetRole::Poster))]; } div.title { - h1 { @dir.info.title } + h1 { @node.public.title } } div.details { - h3 { @dir.info.tagline } - p { @dir.info.description } + h3 { @node.public.tagline } + p { @node.public.description } } - ol { @for ep in &dir.children { - li { a[href=uri!(r_library_node(ep.lib_path()))] { @ep.common().title } } + ol { @for (id, c) in children.iter() { + li { a[href=uri!(r_library_node(id))] { @c.public.title } } } } } } } - -#[derive(FromFormField, UriDisplayQuery)] -pub enum AssetRole { - Poster, - Backdrop, -} - -#[get("/item_assets/<path..>?<role>")] -pub async fn r_item_assets( - _sess: Session, - path: PathBuf, - role: AssetRole, - library: &State<Library>, -) -> Result<(ContentType, CacheControlFile), MyError> { - let node = library - .nested_path(&path) - .context("retrieving library node")?; - let path = node.get_asset(library, role); - info!("loading asset from {path:?}"); - let ext = path.extension().unwrap().to_str().unwrap(); - Ok(( - ContentType::from_extension(ext).unwrap(), - CacheControlFile::new(File::open(path).await?).await, - )) -} diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index d5cb685..0e87749 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -5,27 +5,24 @@ */ use super::{account::session::Session, layout::LayoutPage}; use crate::{ - library::{Item, Library}, + database::Database, routes::{ stream::stream_uri, ui::{ + assets::{rocket_uri_macro_r_item_assets, AssetRole}, error::MyResult, layout::DynLayoutPage, - node::{rocket_uri_macro_r_item_assets, AssetRole}, }, }, uri, }; -use jellycommon::SourceTrackKind; +use anyhow::anyhow; +use jellycommon::{Node, SourceTrackKind}; use markup::DynRender; use rocket::{get, FromForm, State}; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -pub fn player_uri(path: &Path) -> String { - format!("/player/{}", path.to_str().unwrap()) +pub fn player_uri(id: &str) -> String { + format!("/player/{}", id) } #[derive(FromForm, Default, Clone, Debug)] @@ -36,14 +33,14 @@ pub struct PlayerConfig { pub webm: bool, } -#[get("/player/<path..>?<conf..>", rank = 4)] +#[get("/player/<id>?<conf..>", rank = 4)] pub fn r_player( _sess: Session, - library: &State<Library>, - path: PathBuf, + db: &State<Database>, + id: String, conf: PlayerConfig, ) -> MyResult<DynLayoutPage<'_>> { - let item = library.nested_path(&path)?.get_item()?; + let item = db.node.get(&id)?.ok_or(anyhow!("node does not exist"))?; let tracks = None .into_iter() .chain(conf.v.into_iter()) @@ -51,27 +48,35 @@ pub fn r_player( .chain(conf.s.into_iter()) .collect::<Vec<_>>(); + let conf = player_conf(item.clone(), !tracks.is_empty())?; Ok(LayoutPage { - title: item.info.title.to_owned(), + title: item.public.title.to_owned(), class: Some("player"), content: markup::new! { @if tracks.is_empty() { - img.backdrop[src=uri!(r_item_assets(&item.lib_path, AssetRole::Backdrop)).to_string()]; + img.backdrop[src=uri!(r_item_assets(&id, AssetRole::Backdrop)).to_string()]; } else { - video[src=stream_uri(&item.lib_path, &tracks, true), controls]{} + video[src=stream_uri(&id, &tracks, true), controls]{} } - @player_conf(item.clone(), !tracks.is_empty()) + @conf }, show_back: true, ..Default::default() }) } -pub fn player_conf<'a>(item: Arc<Item>, playing: bool) -> DynRender<'a> { +pub fn player_conf<'a>(item: Node, playing: bool) -> anyhow::Result<DynRender<'a>> { let mut audio_tracks = vec![]; let mut video_tracks = vec![]; let mut sub_tracks = vec![]; - for (tid, track) in item.info.tracks.clone() { + let tracks = item + .public + .media + .clone() + .ok_or(anyhow!("node does not have media"))? + .tracks + .clone(); + for (tid, track) in tracks.into_iter().enumerate() { match &track.kind { SourceTrackKind::Audio { .. } => audio_tracks.push((tid, track)), SourceTrackKind::Video { .. } => video_tracks.push((tid, track)), @@ -79,9 +84,9 @@ pub fn player_conf<'a>(item: Arc<Item>, playing: bool) -> DynRender<'a> { } } - markup::new! { + Ok(markup::new! { form.playerconf[method = "GET", action = ""] { - h2 { "Select tracks for " @item.info.title } + h2 { "Select tracks for " @item.public.title } fieldset.video { legend { "Video" } @@ -115,5 +120,5 @@ pub fn player_conf<'a>(item: Arc<Item>, playing: bool) -> DynRender<'a> { input[type="submit", value=if playing { "Change tracks" } else { "Start playback" }]; } - } + }) } |