aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-02-06 16:52:46 +0100
committermetamuffin <metamuffin@disroot.org>2025-02-06 16:52:46 +0100
commit0ce64a50b763d2b19f5ca254233370418f4b7658 (patch)
treefe1587d38c77180b0c7fa4911d416605e5d6c6e3
parent87ebdede17007b626b1275c66dde1e5aefd6cddc (diff)
downloadjellything-0ce64a50b763d2b19f5ca254233370418f4b7658.tar
jellything-0ce64a50b763d2b19f5ca254233370418f4b7658.tar.bz2
jellything-0ce64a50b763d2b19f5ca254233370418f4b7658.tar.zst
add json capability to most useful endpoints
-rw-r--r--common/Cargo.toml2
-rw-r--r--common/src/api.rs36
-rw-r--r--common/src/lib.rs1
-rw-r--r--import/src/lib.rs1
-rw-r--r--import/src/matroska.rs6
-rw-r--r--import/src/trakt.rs2
-rw-r--r--server/Cargo.toml2
-rw-r--r--server/src/routes/api.rs (renamed from server/src/routes/api/mod.rs)3
-rw-r--r--server/src/routes/mod.rs9
-rw-r--r--server/src/routes/playersync.rs2
-rw-r--r--server/src/routes/ui/browser.rs57
-rw-r--r--server/src/routes/ui/home.rs89
-rw-r--r--server/src/routes/ui/mod.rs31
-rw-r--r--server/src/routes/ui/node.rs67
-rw-r--r--server/src/routes/ui/search.rs63
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
+ }
+ },
+ })
})
}