diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-02-26 03:02:24 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-02-26 03:02:24 +0100 |
| commit | eb6648770e7de66ccafe44d114ecbb2c1eaf444d (patch) | |
| tree | 2bce9f579b3ea7313f84df94b27fad813c66e9e1 | |
| parent | 7f7deec27e69ed110c52caddaa3a0c04430e71d9 (diff) | |
| download | jellything-eb6648770e7de66ccafe44d114ecbb2c1eaf444d.tar jellything-eb6648770e7de66ccafe44d114ecbb2c1eaf444d.tar.bz2 jellything-eb6648770e7de66ccafe44d114ecbb2c1eaf444d.tar.zst | |
implement application-side continuation tokens
| -rw-r--r-- | common/src/api.rs | 1 | ||||
| -rw-r--r-- | common/src/routes.rs | 5 | ||||
| -rw-r--r-- | database/src/kv/tests.rs | 10 | ||||
| -rw-r--r-- | database/src/lib.rs | 1 | ||||
| -rw-r--r-- | database/src/query_syntax.rs | 6 | ||||
| -rw-r--r-- | import/src/helpers.rs | 4 | ||||
| -rw-r--r-- | import/src/lib.rs | 8 | ||||
| -rw-r--r-- | locale/en.ini | 1 | ||||
| -rw-r--r-- | server/src/auth.rs | 4 | ||||
| -rw-r--r-- | server/src/compat/youtube.rs | 4 | ||||
| -rw-r--r-- | server/src/logic/stream.rs | 4 | ||||
| -rw-r--r-- | server/src/main.rs | 4 | ||||
| -rw-r--r-- | server/src/routes.rs | 2 | ||||
| -rw-r--r-- | server/src/ui/account/mod.rs | 4 | ||||
| -rw-r--r-- | server/src/ui/account/settings.rs | 4 | ||||
| -rw-r--r-- | server/src/ui/admin/users.rs | 6 | ||||
| -rw-r--r-- | server/src/ui/items.rs | 69 | ||||
| -rw-r--r-- | server/src/ui/mod.rs | 1 | ||||
| -rw-r--r-- | server/src/ui/node.rs | 4 | ||||
| -rw-r--r-- | server/src/ui/player.rs | 4 | ||||
| -rw-r--r-- | ui/src/components/node_list.rs | 5 |
21 files changed, 94 insertions, 57 deletions
diff --git a/common/src/api.rs b/common/src/api.rs index f791cd8..89cfe16 100644 --- a/common/src/api.rs +++ b/common/src/api.rs @@ -49,6 +49,7 @@ fields! { NODELIST_TITLE: &str = b"titl"; NODELIST_DISPLAYSTYLE: Tag = b"dsty"; NODELIST_ITEM: Object = b"item"; // multi + NODELIST_CONTINUATION: &str = b"cont"; MESSAGE_KIND: &str = b"kind"; MESSAGE_TEXT: &str = b"text"; diff --git a/common/src/routes.rs b/common/src/routes.rs index b76fc98..96f36e2 100644 --- a/common/src/routes.rs +++ b/common/src/routes.rs @@ -42,9 +42,8 @@ pub fn u_node_slug_progress(node: &str, time: f64) -> String { pub fn u_items() -> String { "/items".to_string() } -pub fn u_items_filter(page: usize) -> String { - // TODO - format!("/items?page={page}") +pub fn u_items_cont(cont: &str) -> String { + format!("/items?cont={cont}") } pub fn u_admin_users() -> String { "/admin/users".to_string() diff --git a/database/src/kv/tests.rs b/database/src/kv/tests.rs index 85ee5d7..e3e6b3b 100644 --- a/database/src/kv/tests.rs +++ b/database/src/kv/tests.rs @@ -1,5 +1,5 @@ use crate::{ - Database, Filter, Query, Sort, Value, + Database, Filter, Query, Value, test_shared::{AGE, NAME, new_alice, new_bob, new_charlie}, }; use anyhow::Result; @@ -68,7 +68,7 @@ pub fn query_match_int() -> Result<()> { db.transaction(&mut |txn| { result = txn.query_single(Query { filter: Filter::Match(Path(vec![AGE.0]), Value::U32(35)), - sort: Sort::None, + ..Default::default() })?; Ok(()) })?; @@ -95,7 +95,7 @@ pub fn query_match_str() -> Result<()> { db.transaction(&mut |txn| { result = txn.query_single(Query { filter: Filter::Match(Path(vec![NAME.0]), "Alice".into()), - sort: Sort::None, + ..Default::default() })?; Ok(()) })?; @@ -114,7 +114,7 @@ pub fn query_match_str_after() -> Result<()> { db.transaction(&mut |txn| { result = txn.query_single(Query { filter: Filter::Match(Path(vec![NAME.0]), "Alice".into()), - sort: Sort::None, + ..Default::default() })?; Ok(()) })?; @@ -132,7 +132,7 @@ pub fn query_match_str_after() -> Result<()> { db.transaction(&mut |txn| { result = txn.query_single(Query { filter: Filter::Match(Path(vec![NAME.0]), "Alice".into()), - sort: Sort::None, + ..Default::default() })?; Ok(()) })?; diff --git a/database/src/lib.rs b/database/src/lib.rs index 1f491de..13e4a0d 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -35,6 +35,7 @@ pub trait Transaction { #[derive(Debug, Default, Clone, PartialEq)] pub struct Query<'a> { + pub continuation: Option<Vec<u8>>, pub filter: Filter<'a>, pub sort: Sort, } diff --git a/database/src/query_syntax.rs b/database/src/query_syntax.rs index f3d1166..9efc3d5 100644 --- a/database/src/query_syntax.rs +++ b/database/src/query_syntax.rs @@ -91,16 +91,17 @@ impl FromStr for Query<'static> { Ok(Self { filter: Filter::from_str(filter)?, sort: Sort::from_str(sort)?, + ..Default::default() }) } else if let Some(sort) = s.strip_prefix("SORT ") { Ok(Self { - filter: Filter::True, sort: Sort::from_str(sort)?, + ..Default::default() }) } else if let Some(filter) = s.strip_prefix("FILTER") { Ok(Self { filter: Filter::from_str(filter)?, - sort: Sort::None, + ..Default::default() }) } else { bail!("invalid query") @@ -192,6 +193,7 @@ fn test_parse() { multi: MultiBehaviour::First, offset: None, }), + ..Default::default() } ) } diff --git a/import/src/helpers.rs b/import/src/helpers.rs index 5245a50..e42ea8e 100644 --- a/import/src/helpers.rs +++ b/import/src/helpers.rs @@ -9,12 +9,12 @@ use jellycommon::{ NO_SLUG, jellyobject::{ObjectBuffer, Path}, }; -use jellydb::{Filter, Query, RowNum, Sort, Transaction}; +use jellydb::{Filter, Query, RowNum, Transaction}; pub fn get_or_insert_slug(txn: &mut dyn Transaction, slug: &str) -> Result<RowNum> { match txn.query_single(Query { filter: Filter::Match(Path(vec![NO_SLUG.0]), slug.into()), - sort: Sort::None, + ..Default::default() })? { Some(r) => Ok(r), None => txn.insert(ObjectBuffer::new(&mut [(NO_SLUG.0, &slug)])), diff --git a/import/src/lib.rs b/import/src/lib.rs index 232d9e0..684ef07 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -21,7 +21,7 @@ use jellycommon::{ jellyobject::{self, ObjectBuffer, Path as TagPath, Tag}, *, }; -use jellydb::{Database, Filter, Query, RowNum, Sort}; +use jellydb::{Database, Filter, Query, RowNum}; use jellyremuxer::{ demuxers::create_demuxer_autodetect, matroska::{self, AttachedFile, Segment}, @@ -84,7 +84,7 @@ pub struct ImportConfig { fn node_slug_query<'a>(slug: &'a str) -> Query<'a> { Query { filter: Filter::Match(jellyobject::Path(vec![NO_SLUG.0]), slug.into()), - sort: Sort::None, + ..Default::default() } } @@ -455,7 +455,7 @@ fn compare_mtime(dba: &ImportConfig, path: &Path) -> Result<bool> { TagPath(vec![IM_PATH.0]), path.to_string_lossy().to_string().into(), ), - sort: Sort::None, + ..Default::default() })? { None => was_changed = true, Some(row) => { @@ -478,7 +478,7 @@ fn update_mtime(dba: &ImportConfig, path: &Path) -> Result<()> { TagPath(vec![IM_PATH.0]), path.to_string_lossy().to_string().into(), ), - sort: Sort::None, + ..Default::default() })? { Some(row) => row, None => txn.insert(ObjectBuffer::new(&mut [( diff --git a/locale/en.ini b/locale/en.ini index 048886f..c0edecb 100644 --- a/locale/en.ini +++ b/locale/en.ini @@ -81,6 +81,7 @@ tag.iden.isrc=ISRC tag.iden.mbar=MusicBrainz Artist tag.iden.mbrc=MusicBrainz Recording tag.iden.mbrg=MusicBrainz Release Group +tag.iden.mbrl=MusicBrainz Release tag.iden.omdb=OMDB tag.iden.tmmv=TMDB (Movie) tag.iden.tmpe=TMDB (Person) diff --git a/server/src/auth.rs b/server/src/auth.rs index 26da82b..d973dfc 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -11,7 +11,7 @@ use jellycommon::{ jellyobject::{ObjectBuffer, Path}, *, }; -use jellydb::{Filter, Query, Sort}; +use jellydb::{Filter, Query}; pub fn token_to_user(state: &State, token: &str) -> Result<ObjectBuffer> { let user_row = token::validate(&state.session_key, token)?; @@ -38,7 +38,7 @@ pub fn login( state.database.transaction(&mut |txn| { user_row = txn.query_single(Query { filter: Filter::Match(Path(vec![USER_LOGIN.0]), username.into()), - sort: Sort::None, + ..Default::default() })?; if let Some(ur) = user_row { user = txn.get(ur)?; diff --git a/server/src/compat/youtube.rs b/server/src/compat/youtube.rs index a5f540b..4ba3dc8 100644 --- a/server/src/compat/youtube.rs +++ b/server/src/compat/youtube.rs @@ -8,7 +8,7 @@ use anyhow::anyhow; use jellycommon::{ IDENT_YOUTUBE_VIDEO, NO_IDENTIFIERS, NO_SLUG, jellyobject::Path, routes::u_node_id, }; -use jellydb::{Filter, Query, Sort}; +use jellydb::{Filter, Query}; use rocket::{get, response::Redirect}; #[get("/watch?<v>")] @@ -23,7 +23,7 @@ pub fn r_youtube_watch(ri: RequestInfo<'_>, v: &str) -> MyResult<Redirect> { Path(vec![NO_IDENTIFIERS.0, IDENT_YOUTUBE_VIDEO.0]), v.into(), ), - sort: Sort::None, + ..Default::default() })? { res = txn.get(row)?; } diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs index 6f0fdc4..e332811 100644 --- a/server/src/logic/stream.rs +++ b/server/src/logic/stream.rs @@ -9,7 +9,7 @@ use jellycommon::{ NO_SLUG, NO_TITLE, NO_TRACK, TR_SOURCE, TRSOURCE_LOCAL_PATH, jellyobject::Path, stream::StreamSpec, }; -use jellydb::{Filter, Query, Sort}; +use jellydb::{Filter, Query}; use jellystream::SMediaInfo; use log::{info, warn}; use rocket::{ @@ -58,7 +58,7 @@ pub async fn r_stream( ri.state.database.transaction(&mut |txn| { if let Some(row) = txn.query_single(Query { filter: Filter::Match(Path(vec![NO_SLUG.0]), slug.into()), - sort: Sort::None, + ..Default::default() })? { node = txn.get(row)?; } diff --git a/server/src/main.rs b/server/src/main.rs index cbad704..f3eabcf 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -17,7 +17,7 @@ use jellycommon::{ USER_ADMIN, USER_LOGIN, USER_PASSWORD, jellyobject::{ObjectBuffer, Path}, }; -use jellydb::{Database, Filter, Query, Sort}; +use jellydb::{Database, Filter, Query}; use log::{error, info}; use routes::build_rocket; use serde::Deserialize; @@ -102,7 +102,7 @@ fn create_admin_user(state: &State) -> Result<()> { state.database.transaction(&mut |txn| { let admin_row = txn.query_single(Query { filter: Filter::Match(Path(vec![USER_LOGIN.0]), "admin".into()), - sort: Sort::None, + ..Default::default() })?; if admin_row.is_none() { info!("Creating new admin user"); diff --git a/server/src/routes.rs b/server/src/routes.rs index f17952a..fc1d5e6 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -25,6 +25,7 @@ use crate::{ assets::{r_image, r_image_fallback_person}, error::{r_api_catch, r_catch}, home::r_home, + items::r_items, node::r_node, player::r_player, r_favicon, r_index, @@ -101,6 +102,7 @@ pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> { r_image_fallback_person, r_image, r_index, + r_items, r_node, r_player, r_playersync, diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index 837d49a..ab5093d 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -20,7 +20,7 @@ use jellycommon::{ routes::{u_account_login, u_home}, *, }; -use jellydb::{Filter, Query, Sort}; +use jellydb::{Filter, Query}; use rocket::{ Either, FromForm, form::{Contextual, Form}, @@ -74,7 +74,7 @@ pub fn r_account_login_post( ri.state.database.transaction(&mut |txn| { let user_row = txn.query_single(Query { filter: Filter::Match(Path(vec![USER_LOGIN.0]), form.username.clone().into()), - sort: Sort::None, + ..Default::default() })?; if let Some(ur) = user_row { let mut user = txn.get(ur)?.unwrap(); diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs index bb4b323..c1068f6 100644 --- a/server/src/ui/account/settings.rs +++ b/server/src/ui/account/settings.rs @@ -13,7 +13,7 @@ use jellycommon::{ routes::u_account_settings, *, }; -use jellydb::{Filter, Query, Sort}; +use jellydb::{Filter, Query}; use jellyui::tr; use rocket::{ FromForm, @@ -112,7 +112,7 @@ fn update_user(ri: &RequestInfo, update: impl Fn(Object) -> ObjectBuffer) -> MyR let user_row = txn .query_single(Query { filter: Filter::Match(Path(vec![USER_LOGIN.0]), login.into()), - sort: Sort::None, + ..Default::default() })? .ok_or(anyhow!("user vanished"))?; diff --git a/server/src/ui/admin/users.rs b/server/src/ui/admin/users.rs index 654a6b9..172facc 100644 --- a/server/src/ui/admin/users.rs +++ b/server/src/ui/admin/users.rs @@ -12,7 +12,7 @@ use jellycommon::{ routes::u_admin_users, *, }; -use jellydb::{Filter, Query, Sort}; +use jellydb::{Filter, Query}; use jellyui::tr; use rand::random; use rocket::{ @@ -88,8 +88,8 @@ pub fn r_admin_user(ri: RequestInfo<'_>, name: &str) -> MyResult<UiResponse> { let mut page = OBB::new(); ri.state.database.transaction(&mut |txn| { if let Some(row) = txn.query_single(Query { - sort: Sort::None, filter: Filter::Match(Path(vec![USER_LOGIN.0]), name.into()), + ..Default::default() })? { let user = txn.get(row)?.unwrap(); page = OBB::new(); @@ -106,8 +106,8 @@ pub fn r_admin_user_remove(ri: RequestInfo<'_>, name: &str) -> MyResult<Flash<Re ri.require_admin()?; ri.state.database.transaction(&mut |txn| { if let Some(row) = txn.query_single(Query { - sort: Sort::None, filter: Filter::Match(Path(vec![USER_LOGIN.0]), name.into()), + ..Default::default() })? { txn.remove(row)?; } diff --git a/server/src/ui/items.rs b/server/src/ui/items.rs index cc8c18f..6383830 100644 --- a/server/src/ui/items.rs +++ b/server/src/ui/items.rs @@ -3,27 +3,52 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use super::error::MyError; -use rocket::{Either, get, response::content::RawHtml, serde::json::Json}; -#[get("/items?<page>&<filter..>")] -pub fn r_items( - ri: RequestInfo, - page: Option<usize>, - filter: ANodeFilterSort, -) -> Result<Either<RawHtml<String>, Json<ApiItemsResponse>>, MyError> { - let r = all_items(&ri.session, page, filter.clone().into())?; - Ok(if matches!(ri.accept, Accept::Json) { - Either::Right(Json(r)) - } else { - Either::Left(RawHtml(render_page( - &ItemsPage { - lang: &ri.lang, - r, - filter: &filter.clone().into(), - page: page.unwrap_or(0), - }, - ri.render_info(), - ))) - }) +use crate::{request_info::RequestInfo, ui::error::MyResult, ui_responder::UiResponse}; +use anyhow::anyhow; +use base64::{Engine, prelude::BASE64_URL_SAFE}; +use jellycommon::{ + jellyobject::{OBB, Path}, + *, +}; +use jellydb::{Filter, Query}; +use rocket::get; + +#[get("/items?<cont>")] +pub fn r_items(ri: RequestInfo, cont: Option<&str>) -> MyResult<UiResponse> { + let cont = cont + .map(|s| BASE64_URL_SAFE.decode(s)) + .transpose() + .map_err(|_| anyhow!("invalid contination token"))?; + + let mut page = OBB::new(); + ri.state.database.transaction(&mut |txn| { + let rows = txn + .query(Query { + filter: Filter::Has(Path(vec![NO_SLUG.0])), + continuation: cont.clone(), + ..Default::default() + })? + .take(64) + .collect::<Result<Vec<_>, _>>()?; + + let mut list = OBB::new() + .with(NODELIST_DISPLAYSTYLE, NLSTYLE_GRID) + .with(NODELIST_TITLE, "items"); + + let mut iterstate = Vec::new(); + for (r, is) in rows { + let node = txn.get(r)?.unwrap(); + let nku = OBB::new().with(NKU_NODE, node.as_object()).finish(); + list.push(NODELIST_ITEM, nku.as_object()); + iterstate = is; + } + list.push(NODELIST_CONTINUATION, &BASE64_URL_SAFE.encode(iterstate)); + + page = OBB::new(); + page.push(VIEW_NODE_LIST, list.finish().as_object()); + + Ok(()) + })?; + Ok(ri.respond_ui(page)) } diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs index 27535fa..6cb3cd2 100644 --- a/server/src/ui/mod.rs +++ b/server/src/ui/mod.rs @@ -18,6 +18,7 @@ pub mod home; pub mod node; pub mod player; pub mod style; +pub mod items; #[get("/")] pub async fn r_index(ri: RequestInfo<'_>) -> MyResult<Redirect> { diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs index 509e9ae..55a1d09 100644 --- a/server/src/ui/node.rs +++ b/server/src/ui/node.rs @@ -23,7 +23,7 @@ pub fn r_node(ri: RequestInfo<'_>, slug: &str) -> MyResult<UiResponse> { ri.state.database.transaction(&mut |txn| { if let Some(row) = txn.query_single(Query { filter: Filter::Match(Path(vec![NO_SLUG.0]), slug.into()), - sort: Sort::None, + ..Default::default() })? { let n = txn.get(row)?.unwrap(); let nku = Object::EMPTY.insert(NKU_NODE, n.as_object()); @@ -78,6 +78,7 @@ fn c_children( Filter::Match(Path(vec![NO_VISIBILITY.0]), VISI_VISIBLE.into()), Filter::Match(Path(vec![NO_PARENT.0]), row.into()), ]), + ..Default::default() })? .collect::<Result<Vec<_>>>()?; @@ -163,6 +164,7 @@ fn c_credited(page: &mut ObjectBufferBuilder, txn: &mut dyn Transaction, row: u6 Filter::Match(Path(vec![NO_VISIBILITY.0]), VISI_VISIBLE.into()), Filter::Match(Path(vec![NO_CREDIT.0, CR_NODE.0]), row.into()), ]), + ..Default::default() })? .collect::<Result<Vec<_>>>()?; diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs index 4c592e4..f0d6dea 100644 --- a/server/src/ui/player.rs +++ b/server/src/ui/player.rs @@ -9,7 +9,7 @@ use jellycommon::{ jellyobject::{OBB, Object, Path}, *, }; -use jellydb::{Filter, Query, Sort}; +use jellydb::{Filter, Query}; use rocket::get; // fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: &str) -> String { @@ -34,7 +34,7 @@ pub fn r_player(ri: RequestInfo<'_>, t: Option<f64>, slug: &str) -> MyResult<UiR ri.state.database.transaction(&mut |txn| { if let Some(row) = txn.query_single(Query { filter: Filter::Match(Path(vec![NO_SLUG.0]), slug.into()), - sort: Sort::None, + ..Default::default() })? { let n = txn.get(row)?.unwrap(); let nku = Object::EMPTY.insert(NKU_NODE, n.as_object()); diff --git a/ui/src/components/node_list.rs b/ui/src/components/node_list.rs index 679a11d..df405ea 100644 --- a/ui/src/components/node_list.rs +++ b/ui/src/components/node_list.rs @@ -8,7 +8,7 @@ use crate::{ RenderInfo, components::node_card::{NodeCard, NodeCardHightlight, NodeCardWide}, }; -use jellycommon::{jellyobject::Object, *}; +use jellycommon::{jellyobject::Object, routes::u_items_cont, *}; use jellyui_locale::tr; markup::define! { @@ -40,5 +40,8 @@ markup::define! { } _ => {} } + @if let Some(cont) = nl.get(NODELIST_CONTINUATION) { + a[href=u_items_cont(cont)] { button { "Show more" } } + } } } |