diff options
-rw-r--r-- | common/Cargo.toml | 2 | ||||
-rw-r--r-- | common/src/api.rs | 36 | ||||
-rw-r--r-- | common/src/lib.rs | 1 | ||||
-rw-r--r-- | import/src/lib.rs | 1 | ||||
-rw-r--r-- | import/src/matroska.rs | 6 | ||||
-rw-r--r-- | import/src/trakt.rs | 2 | ||||
-rw-r--r-- | server/Cargo.toml | 2 | ||||
-rw-r--r-- | server/src/routes/api.rs (renamed from server/src/routes/api/mod.rs) | 3 | ||||
-rw-r--r-- | server/src/routes/mod.rs | 9 | ||||
-rw-r--r-- | server/src/routes/playersync.rs | 2 | ||||
-rw-r--r-- | server/src/routes/ui/browser.rs | 57 | ||||
-rw-r--r-- | server/src/routes/ui/home.rs | 89 | ||||
-rw-r--r-- | server/src/routes/ui/mod.rs | 31 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 67 | ||||
-rw-r--r-- | server/src/routes/ui/search.rs | 63 |
15 files changed, 237 insertions, 134 deletions
diff --git a/common/Cargo.toml b/common/Cargo.toml index 066e79c..9038bc4 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -serde = { version = "1.0.217", features = ["derive"] } +serde = { version = "1.0.217", features = ["derive", "rc"] } bincode = { version = "2.0.0-rc.3", features = ["derive"] } rocket = { workspace = true, optional = true } chrono = { version = "0.4.39", features = ["serde"] } diff --git a/common/src/api.rs b/common/src/api.rs new file mode 100644 index 0000000..d0b1db7 --- /dev/null +++ b/common/src/api.rs @@ -0,0 +1,36 @@ +/* + 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) 2025 metamuffin <metamuffin.org> +*/ + +use crate::{user::NodeUserData, Node}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Serialize, Deserialize)] +pub struct ApiNodeResponse { + pub parents: Vec<(Arc<Node>, NodeUserData)>, + pub children: Vec<(Arc<Node>, NodeUserData)>, + pub node: Arc<Node>, + pub userdata: NodeUserData, +} + +#[derive(Serialize, Deserialize)] +pub struct ApiSearchResponse { + pub count: usize, + pub results: Vec<(Arc<Node>, NodeUserData)>, +} + +#[derive(Serialize, Deserialize)] +pub struct ApiItemsResponse { + pub count: usize, + pub pages: usize, + pub items: Vec<(Arc<Node>, NodeUserData)>, +} + +#[derive(Serialize, Deserialize)] +pub struct ApiHomeResponse { + pub toplevel: Vec<(Arc<Node>, NodeUserData)>, + pub categories: Vec<(String, Vec<(Arc<Node>, NodeUserData)>)>, +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 4b67054..74476fe 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,6 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ +pub mod api; pub mod config; pub mod helpers; pub mod r#impl; diff --git a/import/src/lib.rs b/import/src/lib.rs index 0990ba1..7b19a61 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -410,6 +410,7 @@ fn import_media_file( NodeKind::Video }; node.title = Some(infojson.title); + node.subtitle = infojson.uploader; if let Some(desc) = infojson.description { node.description = Some(desc) } diff --git a/import/src/matroska.rs b/import/src/matroska.rs index 4694833..4ab1148 100644 --- a/import/src/matroska.rs +++ b/import/src/matroska.rs @@ -16,7 +16,7 @@ use jellybase::{ cache::{cache_file, cache_memory}, common::Asset, }; -use log::info; +use log::{info, warn}; use std::{ fs::File, io::{BufReader, ErrorKind, Read, Write}, @@ -41,7 +41,7 @@ pub(crate) fn matroska_metadata(path: &Path) -> Result<Arc<Option<MatroskaMetada return Ok(None); } - info!("reading media file {path:?}"); + info!("reading {path:?}"); let mut file = BufReader::new(File::open(path)?); let mut file = file.by_ref().take(u64::MAX); @@ -103,7 +103,7 @@ pub(crate) fn matroska_metadata(path: &Path) -> Result<Arc<Option<MatroskaMetada break; } id => { - eprintln!("unknown top-level element {id:x}"); + warn!("unknown top-level element {id:x}"); seg.consume()?; } } diff --git a/import/src/trakt.rs b/import/src/trakt.rs index 1daee77..52a5cb0 100644 --- a/import/src/trakt.rs +++ b/import/src/trakt.rs @@ -1,9 +1,9 @@ -use anyhow::Context; /* 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) 2025 metamuffin <metamuffin.org> */ +use anyhow::Context; use bincode::{Decode, Encode}; use jellybase::{ cache::async_cache_memory, diff --git a/server/Cargo.toml b/server/Cargo.toml index eb0b036..a709a98 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ jellystream = { path = "../stream" } jellytranscoder = { path = "../transcoder" } jellyimport = { path = "../import" } -serde = { version = "1.0.217", features = ["derive"] } +serde = { version = "1.0.217", features = ["derive", "rc"] } bincode = { version = "2.0.0-rc.3", features = ["serde", "derive"] } serde_json = "1.0.138" diff --git a/server/src/routes/api/mod.rs b/server/src/routes/api.rs index 065c136..f761a8f 100644 --- a/server/src/routes/api/mod.rs +++ b/server/src/routes/api.rs @@ -49,8 +49,7 @@ pub fn r_api_account_login( } #[get("/api/asset_token_raw/<token>")] -pub fn r_api_asset_token_raw(admin: AdminSession, token: &str) -> MyResult<Json<AssetInner>> { - drop(admin); +pub fn r_api_asset_token_raw(_admin: AdminSession, token: &str) -> MyResult<Json<AssetInner>> { Ok(Json(AssetInner::deser(token)?)) } diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 371f088..e48b7cf 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use self::playersync::{r_streamsync, PlayersyncChannels}; +use self::playersync::{r_playersync, PlayersyncChannels}; use crate::{database::Database, routes::ui::error::MyResult}; use api::{r_api_account_login, r_api_asset_token_raw, r_api_root, r_api_version}; use base64::Engine; @@ -58,9 +58,10 @@ use ui::{ assets::{r_asset, r_item_backdrop, r_item_poster, r_node_thumbnail, r_person_asset}, browser::r_all_items_filter, error::{r_api_catch, r_catch}, - home::{r_home, r_home_unpriv}, + home::r_home, node::r_library_node_filter, player::r_player, + r_index, search::r_search, style::{r_assets_font, r_assets_js, r_assets_js_map, r_assets_style}, }; @@ -154,8 +155,8 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> r_assets_js, r_assets_style, r_favicon, - r_home_unpriv, r_home, + r_index, r_item_backdrop, r_item_poster, r_library_node_filter, @@ -168,7 +169,7 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> r_player, r_search, r_stream, - r_streamsync, + r_playersync, // API r_api_account_login, r_api_asset_token_raw, diff --git a/server/src/routes/playersync.rs b/server/src/routes/playersync.rs index 1ef9d73..9eb6175 100644 --- a/server/src/routes/playersync.rs +++ b/server/src/routes/playersync.rs @@ -23,7 +23,7 @@ pub enum Packet { } #[get("/playersync/<channel>")] -pub fn r_streamsync( +pub fn r_playersync( ws: WebSocket, state: &State<PlayersyncChannels>, channel: &str, diff --git a/server/src/routes/ui/browser.rs b/server/src/routes/ui/browser.rs index 7affbac..13f30c8 100644 --- a/server/src/routes/ui/browser.rs +++ b/server/src/routes/ui/browser.rs @@ -6,13 +6,13 @@ use super::{ account::session::Session, error::MyError, - layout::DynLayoutPage, + layout::{DynLayoutPage, LayoutPage}, node::NodeCard, sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm, SortOrder, SortProperty}, }; -use crate::{database::Database, uri}; -use jellycommon::Visibility; -use rocket::{get, State}; +use crate::{database::Database, routes::api::AcceptJson, uri}; +use jellycommon::{api::ApiItemsResponse, Visibility}; +use rocket::{get, serde::json::Json, Either, State}; /// This function is a stub and only useful for use in the uri! macro. #[get("/items")] @@ -22,9 +22,10 @@ pub fn r_all_items() {} pub fn r_all_items_filter( sess: Session, db: &State<Database>, + aj: AcceptJson, page: Option<usize>, filter: NodeFilterSort, -) -> Result<DynLayoutPage<'_>, MyError> { +) -> Result<Either<DynLayoutPage<'_>, Json<ApiItemsResponse>>, MyError> { let mut items = db.list_nodes_with_udata(sess.user.name.as_str())?; items.retain(|(n, _)| matches!(n.visibility, Visibility::Visible)); @@ -42,26 +43,34 @@ pub fn r_all_items_filter( let to = (offset + page_size).min(items.len()); let max_page = items.len().div_ceil(page_size); - Ok(super::layout::LayoutPage { - title: "All Items".to_owned(), - content: markup::new! { - .page.dir { - h1 { "All Items" } - @NodeFilterSortForm { f: &filter } - ul.children { @for (node, udata) in &items[from..to] { - li {@NodeCard { node, udata }} - }} - p.pagecontrols { - span.current { "Page " @{page + 1} " of " @max_page " " } - @if page > 0 { - a.prev[href=uri!(r_all_items_filter(Some(page - 1), filter.clone()))] { "Previous page" } " " - } - @if page + 1 < max_page { - a.next[href=uri!(r_all_items_filter(Some(page + 1), filter.clone()))] { "Next page" } + Ok(if *aj { + Either::Right(Json(ApiItemsResponse { + count: items.len(), + pages: max_page, + items: items[from..to].to_vec(), + })) + } else { + Either::Left(LayoutPage { + title: "All Items".to_owned(), + content: markup::new! { + .page.dir { + h1 { "All Items" } + @NodeFilterSortForm { f: &filter } + ul.children { @for (node, udata) in &items[from..to] { + li {@NodeCard { node, udata }} + }} + p.pagecontrols { + span.current { "Page " @{page + 1} " of " @max_page " " } + @if page > 0 { + a.prev[href=uri!(r_all_items_filter(Some(page - 1), filter.clone()))] { "Previous page" } " " + } + @if page + 1 < max_page { + a.next[href=uri!(r_all_items_filter(Some(page + 1), filter.clone()))] { "Next page" } + } } } - } - }, - ..Default::default() + }, + ..Default::default() + }) }) } diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs index ee98bd2..3e002ee 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/routes/ui/home.rs @@ -10,17 +10,23 @@ use super::{ }; use crate::{ database::Database, - routes::ui::{error::MyResult, layout::DynLayoutPage}, + routes::{ + api::AcceptJson, + ui::{error::MyResult, layout::DynLayoutPage}, + }, }; use anyhow::Context; use chrono::{Datelike, Utc}; use jellybase::CONF; -use jellycommon::{user::WatchedState, NodeID, NodeKind, Rating, Visibility}; -use rocket::{get, State}; -use tokio::fs::read_to_string; +use jellycommon::{api::ApiHomeResponse, user::WatchedState, NodeID, NodeKind, Rating, Visibility}; +use rocket::{get, serde::json::Json, Either, State}; -#[get("/")] -pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { +#[get("/home")] +pub fn r_home( + sess: Session, + db: &State<Database>, + aj: AcceptJson, +) -> MyResult<Either<DynLayoutPage, Json<ApiHomeResponse>>> { let mut items = db.list_nodes_with_udata(&sess.user.name)?; let mut toplevel = db @@ -31,10 +37,10 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { .collect::<anyhow::Result<Vec<_>>>()?; toplevel.sort_by_key(|(n, _)| n.index.unwrap_or(usize::MAX)); - let mut categories = Vec::<(&'static str, Vec<_>)>::new(); + let mut categories = Vec::<(String, Vec<_>)>::new(); categories.push(( - "Continue Watching", + "Continue Watching".to_string(), items .iter() .filter(|(_, u)| matches!(u.watched, WatchedState::Progress(_))) @@ -42,7 +48,7 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { .collect(), )); categories.push(( - "Your Watchlist", + "Your Watchlist".to_string(), items .iter() .filter(|(_, u)| matches!(u.watched, WatchedState::Pending)) @@ -55,7 +61,7 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { items.sort_by_key(|(n, _)| n.release_date.map(|d| -d).unwrap_or(i64::MAX)); categories.push(( - "Latest in Videos", + "Latest in Videos".to_string(), items .iter() .filter(|(n, _)| matches!(n.kind, NodeKind::Video)) @@ -64,7 +70,7 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { .collect(), )); categories.push(( - "Latest in Music", + "Latest in Music".to_string(), items .iter() .filter(|(n, _)| matches!(n.kind, NodeKind::Music)) @@ -73,7 +79,7 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { .collect(), )); categories.push(( - "Latest in Short form", + "Latest in Short form".to_string(), items .iter() .filter(|(n, _)| matches!(n.kind, NodeKind::ShortFormVideo)) @@ -90,7 +96,7 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { }); categories.push(( - "Top Rated", + "Top Rated".to_string(), items .iter() .take(16) @@ -107,7 +113,7 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { }); categories.push(( - "Today's Picks", + "Today's Picks".to_string(), (0..16) .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone())) .collect(), @@ -117,7 +123,7 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { let mut items = items.clone(); items.retain(|(_, u)| matches!(u.watched, WatchedState::Watched)); categories.push(( - "Watch again", + "Watch again".to_string(), (0..16) .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone())) .collect(), @@ -126,41 +132,36 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { items.retain(|(n, _)| matches!(n.kind, NodeKind::Music)); categories.push(( - "Discover Music", + "Discover Music".to_string(), (0..16) .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone())) .collect(), )); - Ok(LayoutPage { - title: "Home".to_string(), - content: markup::new! { - h2 { "Explore " @CONF.brand } - ul.children.hlist {@for (node, udata) in &toplevel { - li { @NodeCard { node, udata } } - }} - @for (name, nodes) in &categories { - @if !nodes.is_empty() { - h2 { @name } - ul.children.hlist {@for (node, udata) in nodes { - li { @NodeCard { node, udata } } - }} + Ok(if *aj { + Either::Right(Json(ApiHomeResponse { + toplevel, + categories, + })) + } else { + Either::Left(LayoutPage { + title: "Home".to_string(), + content: markup::new! { + h2 { "Explore " @CONF.brand } + ul.children.hlist {@for (node, udata) in &toplevel { + li { @NodeCard { node, udata } } + }} + @for (name, nodes) in &categories { + @if !nodes.is_empty() { + h2 { @name } + ul.children.hlist {@for (node, udata) in nodes { + li { @NodeCard { node, udata } } + }} + } } - } - }, - ..Default::default() - }) -} - -#[get("/", rank = 2)] -pub async fn r_home_unpriv() -> MyResult<DynLayoutPage<'static>> { - let front = read_to_string(CONF.asset_path.join("front.htm")).await?; - Ok(LayoutPage { - title: "Home".to_string(), - content: markup::new! { - @markup::raw(&front) - }, - ..Default::default() + }, + ..Default::default() + }) }) } diff --git a/server/src/routes/ui/mod.rs b/server/src/routes/ui/mod.rs index 812e2d1..bffdfdd 100644 --- a/server/src/routes/ui/mod.rs +++ b/server/src/routes/ui/mod.rs @@ -3,13 +3,19 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ +use account::session::Session; +use error::MyResult; +use home::rocket_uri_macro_r_home; +use jellybase::CONF; +use layout::{DynLayoutPage, LayoutPage}; use log::debug; use markup::Render; use rocket::{ futures::FutureExt, + get, http::{ContentType, Header, Status}, - response::{self, Responder}, - Request, Response, + response::{self, Redirect, Responder}, + Either, Request, Response, }; use std::{ collections::hash_map::DefaultHasher, @@ -20,7 +26,10 @@ use std::{ path::Path, pin::Pin, }; -use tokio::{fs::File, io::AsyncRead}; +use tokio::{ + fs::{read_to_string, File}, + io::AsyncRead, +}; pub mod account; pub mod admin; @@ -35,6 +44,22 @@ pub mod search; pub mod sort; pub mod style; +#[get("/")] +pub async fn r_index(sess: Option<Session>) -> MyResult<Either<Redirect, DynLayoutPage<'static>>> { + if sess.is_some() { + Ok(Either::Left(Redirect::temporary(rocket::uri!(r_home())))) + } else { + let front = read_to_string(CONF.asset_path.join("front.htm")).await?; + Ok(Either::Right(LayoutPage { + title: "Home".to_string(), + content: markup::new! { + @markup::raw(&front) + }, + ..Default::default() + })) + } +} + pub struct HtmlTemplate<'a>(pub markup::DynRender<'a>); impl<'r> Responder<'r, 'static> for HtmlTemplate<'_> { diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 75893a8..365182c 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -31,6 +31,7 @@ use crate::{ use anyhow::{anyhow, Result}; use chrono::DateTime; use jellycommon::{ + api::ApiNodeResponse, user::{NodeUserData, WatchedState}, Chapter, MediaInfo, Node, NodeID, NodeKind, PeopleGroup, Rating, SourceTrackKind, Visibility, }; @@ -43,34 +44,39 @@ pub fn r_library_node(id: String) { drop(id) } -#[get("/n/<slug>?<filter..>")] +#[get("/n/<slug>?<parents>&<children>&<filter..>")] pub async fn r_library_node_filter<'a>( session: Session, slug: &'a str, db: &'a State<Database>, aj: AcceptJson, filter: NodeFilterSort, -) -> MyResult<Either<DynLayoutPage<'a>, Json<Node>>> { + parents: bool, + children: bool, +) -> MyResult<Either<DynLayoutPage<'a>, Json<ApiNodeResponse>>> { let id = NodeID::from_slug(slug); let (node, udata) = db.get_node_with_userdata(id, &session)?; - if *aj { - return Ok(Either::Right(Json((*node).clone()))); - } + let mut children = if !*aj || children { + db.get_node_children(id)? + .into_iter() + .map(|c| db.get_node_with_userdata(c, &session)) + .collect::<anyhow::Result<Vec<_>>>()? + } else { + Vec::new() + }; - let mut children = db - .get_node_children(id)? - .into_iter() - .map(|c| db.get_node_with_userdata(c, &session)) - .collect::<anyhow::Result<Vec<_>>>()?; - children.retain(|(n, _)| n.visibility >= Visibility::Reduced); + let mut parents = if !*aj || parents { + node.parents + .iter() + .map(|pid| db.get_node_with_userdata(*pid, &session)) + .collect::<anyhow::Result<Vec<_>>>()? + } else { + Vec::new() + }; - let mut parents = node - .parents - .iter() - .flat_map(|pid| db.get_node(*pid).transpose()) - .collect::<Result<Vec<_>, _>>()?; - parents.retain(|n| n.visibility >= Visibility::Reduced); + children.retain(|(n, _)| n.visibility >= Visibility::Reduced); + parents.retain(|(n, _)| n.visibility >= Visibility::Reduced); filter_and_sort_nodes( &filter, @@ -82,13 +88,22 @@ pub async fn r_library_node_filter<'a>( &mut children, ); - Ok(Either::Left(LayoutPage { - title: node.title.clone().unwrap_or_default(), - content: markup::new! { - @NodePage { node: &node, id: slug, udata: &udata, children: &children, parents: &parents, filter: &filter } - }, - ..Default::default() - })) + Ok(if *aj { + Either::Right(Json(ApiNodeResponse { + children, + parents, + node, + userdata: udata, + })) + } else { + Either::Left(LayoutPage { + title: node.title.clone().unwrap_or_default(), + content: markup::new! { + @NodePage { node: &node, id: slug, udata: &udata, children: &children, parents: &parents, filter: &filter } + }, + ..Default::default() + }) + }) } markup::define! { @@ -137,7 +152,7 @@ markup::define! { } } } - NodePage<'a>(id: &'a str, node: &'a Node, udata: &'a NodeUserData, children: &'a [(Arc<Node>, NodeUserData)], parents: &'a [Arc<Node>], filter: &'a NodeFilterSort) { + NodePage<'a>(id: &'a str, node: &'a Node, udata: &'a NodeUserData, children: &'a [(Arc<Node>, NodeUserData)], parents: &'a [(Arc<Node>, NodeUserData)], filter: &'a NodeFilterSort) { @if !matches!(node.kind, NodeKind::Collection) { img.backdrop[src=uri!(r_item_backdrop(id, Some(2048))), loading="lazy"]; } @@ -148,7 +163,7 @@ markup::define! { } .title { h1 { @node.title } - ul.parents { @for node in *parents { li { + ul.parents { @for (node, _) in *parents { li { a.component[href=uri!(r_library_node(&node.slug))] { @node.title } }}} @if node.media.is_some() { a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" }} diff --git a/server/src/routes/ui/search.rs b/server/src/routes/ui/search.rs index d020e2e..c5944ec 100644 --- a/server/src/routes/ui/search.rs +++ b/server/src/routes/ui/search.rs @@ -1,53 +1,68 @@ +/* + 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) 2025 metamuffin <metamuffin.org> +*/ use super::{ account::session::Session, error::MyResult, layout::{DynLayoutPage, LayoutPage}, node::{DatabaseNodeUserDataExt, NodeCard}, }; +use crate::routes::api::AcceptJson; +use anyhow::anyhow; use jellybase::database::Database; -use jellycommon::Visibility; -use rocket::{get, State}; +use jellycommon::{api::ApiSearchResponse, Visibility}; +use rocket::{get, serde::json::Json, Either, State}; use std::time::Instant; #[get("/search?<query>&<page>")] pub async fn r_search<'a>( session: Session, db: &State<Database>, + aj: AcceptJson, query: Option<&str>, page: Option<usize>, -) -> MyResult<DynLayoutPage<'a>> { - let timing = Instant::now(); +) -> MyResult<Either<DynLayoutPage<'a>, Json<ApiSearchResponse>>> { let results = if let Some(query) = query { + let timing = Instant::now(); let (count, ids) = db.search(query, 32, page.unwrap_or_default() * 32)?; let mut nodes = ids .into_iter() .map(|id| db.get_node_with_userdata(id, &session)) .collect::<Result<Vec<_>, anyhow::Error>>()?; nodes.retain(|(n, _)| n.visibility >= Visibility::Reduced); - Some((count, nodes)) + let search_dur = timing.elapsed(); + Some((count, nodes, search_dur)) } else { None }; - let search_dur = timing.elapsed(); let query = query.unwrap_or_default().to_string(); - Ok(LayoutPage { - title: "Search".to_string(), - class: Some("search"), - content: markup::new! { - h1 { "Search" } - form[action="", method="GET"] { - input[type="text", name="query", placeholder="Search Term", value=&query]; - input[type="submit", value="Search"]; - } - @if let Some((count, results)) = &results { - h2 { "Results" } - p.stats { @format!("Found {count} nodes in {search_dur:?}.") } - ul.children {@for (node, udata) in results.iter() { - li { @NodeCard { node, udata } } - }} - // TODO pagination - } - }, + Ok(if *aj { + let Some((count, results, _)) = results else { + Err(anyhow!("no query"))? + }; + Either::Right(Json(ApiSearchResponse { count, results })) + } else { + Either::Left(LayoutPage { + title: "Search".to_string(), + class: Some("search"), + content: markup::new! { + h1 { "Search" } + form[action="", method="GET"] { + input[type="text", name="query", placeholder="Search Term", value=&query]; + input[type="submit", value="Search"]; + } + @if let Some((count, results, search_dur)) = &results { + h2 { "Results" } + p.stats { @format!("Found {count} nodes in {search_dur:?}.") } + ul.children {@for (node, udata) in results.iter() { + li { @NodeCard { node, udata } } + }} + // TODO pagination + } + }, + }) }) } |