From db511d3fe50f05329615f718515fab1b80d9e06a Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 29 Jan 2025 18:03:06 +0100 Subject: no direct redb access --- server/src/routes/ui/account/mod.rs | 54 +++----- server/src/routes/ui/account/session/guard.rs | 17 +-- server/src/routes/ui/account/settings.rs | 66 ++++------ server/src/routes/ui/admin/mod.rs | 170 +++++++++++--------------- server/src/routes/ui/admin/user.rs | 67 ++++------ server/src/routes/ui/assets.rs | 71 +++++------ server/src/routes/ui/browser.rs | 30 +---- server/src/routes/ui/error.rs | 42 ------- server/src/routes/ui/home.rs | 60 +++------ server/src/routes/ui/node.rs | 62 ++++------ server/src/routes/ui/player.rs | 17 ++- server/src/routes/ui/search.rs | 58 ++------- server/src/routes/ui/sort.rs | 32 +++-- 13 files changed, 254 insertions(+), 492 deletions(-) (limited to 'server/src/routes/ui') diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs index d73cf4c..6139a08 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/routes/ui/account/mod.rs @@ -8,7 +8,7 @@ pub mod settings; use super::{error::MyError, layout::LayoutPage}; use crate::{ - database::DataAcid, + database::Database, routes::ui::{ account::session::Session, error::MyResult, home::rocket_uri_macro_r_home, layout::DynLayoutPage, @@ -18,10 +18,7 @@ use crate::{ use anyhow::anyhow; use argon2::{password_hash::Salt, Argon2, PasswordHasher}; use chrono::Duration; -use jellybase::{ - database::{Ser, TableExt, T_INVITE, T_USER}, - CONF, -}; +use jellybase::CONF; use jellycommon::user::{User, UserPermission}; use rocket::{ form::{Contextual, Form}, @@ -124,7 +121,7 @@ pub fn r_account_logout() -> DynLayoutPage<'static> { #[post("/account/register", data = "
")] pub fn r_account_register_post<'a>( - database: &'a State, + database: &'a State, _sess: Option, form: Form>, ) -> MyResult> { @@ -134,31 +131,16 @@ pub fn r_account_register_post<'a>( None => return Err(format_form_error(form)), }; - let txn = database.begin_write()?; - let mut invites = txn.open_table(T_INVITE)?; - let mut users = txn.open_table(T_USER)?; - - if invites.remove(&*form.invitation)?.is_none() { - Err(anyhow!("invitation invalid"))?; - } - let prev_user = users - .insert( - &*form.username, - Ser(User { - display_name: form.username.clone(), - name: form.username.clone(), - password: hash_password(&form.username, &form.password), - ..Default::default() - }), - )? - .map(|x| x.value().0); - if prev_user.is_some() { - Err(anyhow!("username taken"))?; - } - - drop(users); - drop(invites); - txn.commit()?; + database.register_user( + &form.invitation, + &form.username, + User { + display_name: form.username.clone(), + name: form.username.clone(), + password: hash_password(&form.username, &form.password), + ..Default::default() + }, + )?; Ok(LayoutPage { title: "Registration successful".to_string(), @@ -175,7 +157,7 @@ pub fn r_account_register_post<'a>( #[post("/account/login", data = "")] pub fn r_account_login_post( - database: &State, + database: &State, jar: &CookieJar, form: Form>, ) -> MyResult { @@ -202,17 +184,17 @@ pub fn r_account_logout_post(jar: &CookieJar) -> MyResult { } pub fn login_logic( - database: &DataAcid, + database: &Database, username: &str, password: &str, expire: Option, drop_permissions: Option>, ) -> MyResult { - // hashing the password regardless if the accounts exists to prevent timing attacks + // hashing the password regardless if the accounts exists to better resist timing attacks let password = hash_password(username, password); - let mut user = T_USER - .get(database, username)? + let mut user = database + .get_user(username)? .ok_or(anyhow!("invalid password"))?; if user.password != password { diff --git a/server/src/routes/ui/account/session/guard.rs b/server/src/routes/ui/account/session/guard.rs index 6a9bdaf..57540cf 100644 --- a/server/src/routes/ui/account/session/guard.rs +++ b/server/src/routes/ui/account/session/guard.rs @@ -4,9 +4,8 @@ Copyright (C) 2025 metamuffin */ use super::{AdminSession, Session}; -use crate::{database::DataAcid, routes::ui::error::MyError}; +use crate::{database::Database, routes::ui::error::MyError}; use anyhow::anyhow; -use jellybase::database::T_USER; use log::warn; use rocket::{ async_trait, @@ -36,19 +35,9 @@ impl Session { username = "admin".to_string(); } - let db = req.guard::<&State>().await.unwrap(); + let db = req.guard::<&State>().await.unwrap(); - let user = { - let txn = db.inner.begin_read()?; - let table = txn.open_table(T_USER)?; - let user = table - .get(&*username)? - .ok_or(anyhow!("user not found"))? - .value() - .0; - drop(table); - user - }; + let user = db.get_user(&username)?.ok_or(anyhow!("user not found"))?; Ok(Session { user }) } diff --git a/server/src/routes/ui/account/settings.rs b/server/src/routes/ui/account/settings.rs index 24e90de..06754b1 100644 --- a/server/src/routes/ui/account/settings.rs +++ b/server/src/routes/ui/account/settings.rs @@ -5,7 +5,7 @@ */ use super::{format_form_error, hash_password}; use crate::{ - database::DataAcid, + database::Database, routes::ui::{ account::{rocket_uri_macro_r_account_login, session::Session}, error::MyResult, @@ -13,11 +13,7 @@ use crate::{ }, uri, }; -use anyhow::anyhow; -use jellybase::{ - database::{redb::ReadableTable, Ser, T_USER}, - permission::PermissionSetExt, -}; +use jellybase::permission::PermissionSetExt; use jellycommon::user::{PlayerKind, Theme, UserPermission}; use markup::{Render, RenderAttributeValue}; use rocket::{ @@ -117,7 +113,7 @@ pub fn r_account_settings(session: Session) -> DynLayoutPage<'static> { #[post("/account/settings", data = "")] pub fn r_account_settings_post( session: Session, - database: &State, + database: &State, form: Form>, ) -> MyResult> { session @@ -132,39 +128,29 @@ pub fn r_account_settings_post( let mut out = String::new(); - let txn = database.begin_write()?; - let mut users = txn.open_table(T_USER)?; - - let mut user = users - .get(&*session.user.name)? - .ok_or(anyhow!("user missing"))? - .value() - .0; - - if let Some(password) = &form.password { - user.password = hash_password(&session.user.name, password); - out += "Password updated\n"; - } - if let Some(display_name) = &form.display_name { - user.display_name = display_name.clone(); - out += "Display name updated\n"; - } - if let Some(theme) = form.theme { - user.theme = theme; - out += "Theme updated\n"; - } - if let Some(player_preference) = form.player_preference { - user.player_preference = player_preference; - out += "Player preference changed.\n"; - } - if let Some(native_secret) = &form.native_secret { - user.native_secret = native_secret.to_owned(); - out += "Native secret updated.\n"; - } - - users.insert(&*session.user.name, Ser(user))?; - drop(users); - txn.commit()?; + database.update_user(&session.user.name, |user| { + if let Some(password) = &form.password { + user.password = hash_password(&session.user.name, password); + out += "Password updated\n"; + } + if let Some(display_name) = &form.display_name { + user.display_name = display_name.clone(); + out += "Display name updated\n"; + } + if let Some(theme) = form.theme { + user.theme = theme; + out += "Theme updated\n"; + } + if let Some(player_preference) = form.player_preference { + user.player_preference = player_preference; + out += "Player preference changed.\n"; + } + if let Some(native_secret) = &form.native_secret { + user.native_secret = native_secret.to_owned(); + out += "Native secret updated.\n"; + } + Ok(()) + })?; Ok(settings_page( session, // using the old session here, results in outdated theme being displayed diff --git a/server/src/routes/ui/admin/mod.rs b/server/src/routes/ui/admin/mod.rs index 1fba6c0..160999b 100644 --- a/server/src/routes/ui/admin/mod.rs +++ b/server/src/routes/ui/admin/mod.rs @@ -6,12 +6,9 @@ pub mod log; pub mod user; -use super::{ - account::session::AdminSession, - assets::{resolve_asset, AVIF_QUALITY, AVIF_SPEED}, -}; +use super::account::session::AdminSession; use crate::{ - database::DataAcid, + database::Database, routes::ui::{ admin::log::rocket_uri_macro_r_admin_log, error::MyResult, @@ -20,16 +17,7 @@ use crate::{ uri, }; use anyhow::{anyhow, Context}; -use humansize::{format_size, DECIMAL}; -use jellybase::{ - assetfed::AssetInner, - database::{ - redb::{ReadableTable, ReadableTableMetadata}, - tantivy::query::Bm25StatisticsProvider, - TableExt, T_INVITE, T_NODE, T_USER_NODE, - }, - CONF, -}; +use jellybase::CONF; use markup::DynRender; use rand::Rng; use rocket::{form::Form, get, post, FromForm, State}; @@ -40,28 +28,16 @@ use user::rocket_uri_macro_r_admin_users; #[get("/admin/dashboard")] pub async fn r_admin_dashboard( _session: AdminSession, - database: &State, + database: &State, ) -> MyResult> { admin_dashboard(database, None).await } pub async fn admin_dashboard<'a>( - database: &DataAcid, + database: &Database, flash: Option>, ) -> MyResult> { - let invites = { - let txn = database.begin_read()?; - let table = txn.open_table(T_INVITE)?; - let i = table - .iter()? - .map(|a| { - let (x, _) = a.unwrap(); - x.value().to_owned() - }) - .collect::>(); - drop(table); - i - }; + let invites = database.list_invites()?; let flash = flash.map(|f| f.map_err(|e| format!("{e:?}"))); // let last_import_err = IMPORT_ERRORS.read().await.to_owned(); @@ -129,11 +105,10 @@ pub async fn admin_dashboard<'a>( #[post("/admin/generate_invite")] pub async fn r_admin_invite( _session: AdminSession, - database: &State, + database: &State, ) -> MyResult> { let i = format!("{}", rand::rng().random::()); - T_INVITE.insert(database, &*i, ())?; - + database.create_invite(&i)?; admin_dashboard(database, Some(Ok(format!("Invite: {}", i)))).await } @@ -145,14 +120,13 @@ pub struct DeleteInvite { #[post("/admin/remove_invite", data = "")] pub async fn r_admin_remove_invite( session: AdminSession, - database: &State, + database: &State, form: Form, ) -> MyResult> { drop(session); - T_INVITE - .remove(database, form.invite.as_str())? - .ok_or(anyhow!("invite did not exist"))?; - + if !database.delete_invite(&form.invite)? { + Err(anyhow!("invite does not exist"))?; + }; admin_dashboard(database, Some(Ok("Invite invalidated".into()))).await } @@ -178,7 +152,7 @@ pub async fn r_admin_remove_invite( #[post("/admin/delete_cache")] pub async fn r_admin_delete_cache( session: AdminSession, - database: &State, + database: &State, ) -> MyResult> { drop(session); let t = Instant::now(); @@ -202,7 +176,7 @@ fn is_transcoding() -> bool { #[post("/admin/transcode_posters")] pub async fn r_admin_transcode_posters( session: AdminSession, - database: &State, + database: &State, ) -> MyResult> { drop(session); let _permit = SEM_TRANSCODING @@ -211,24 +185,25 @@ pub async fn r_admin_transcode_posters( let t = Instant::now(); - { - let txn = database.begin_read()?; - let nodes = txn.open_table(T_NODE)?; - for node in nodes.iter()? { - let (_, node) = node?; - if let Some(poster) = &node.value().0.poster { - let asset = AssetInner::deser(&poster.0)?; - if asset.is_federated() { - continue; - } - let source = resolve_asset(asset).await.context("resolving asset")?; - jellytranscoder::image::transcode(source, AVIF_QUALITY, AVIF_SPEED, 1024) - .await - .context("transcoding asset")?; - } - } - } - drop(_permit); + // TODO + // { + // let txn = database.begin_read()?; + // let nodes = txn.open_table(T_NODE)?; + // for node in nodes.iter()? { + // let (_, node) = node?; + // if let Some(poster) = &node.value().0.poster { + // let asset = AssetInner::deser(&poster.0)?; + // if asset.is_federated() { + // continue; + // } + // let source = resolve_asset(asset).await.context("resolving asset")?; + // jellytranscoder::image::transcode(source, AVIF_QUALITY, AVIF_SPEED, 1024) + // .await + // .context("transcoding asset")?; + // } + // } + // } + // drop(_permit); admin_dashboard( database, @@ -240,47 +215,48 @@ pub async fn r_admin_transcode_posters( .await } -fn db_stats(db: &DataAcid) -> anyhow::Result { - let txn = db.inner.begin_read()?; - let stats = [ - ("node", txn.open_table(T_NODE)?.stats()?), - ("user", txn.open_table(T_USER_NODE)?.stats()?), - ("user-node", txn.open_table(T_USER_NODE)?.stats()?), - ("invite", txn.open_table(T_INVITE)?.stats()?), - ]; +fn db_stats(_db: &Database) -> anyhow::Result { + // TODO + // let txn = db.inner.begin_read()?; + // let stats = [ + // ("node", txn.open_table(T_NODE)?.stats()?), + // ("user", txn.open_table(T_USER_NODE)?.stats()?), + // ("user-node", txn.open_table(T_USER_NODE)?.stats()?), + // ("invite", txn.open_table(T_INVITE)?.stats()?), + // ]; - let cache_stats = db.node_index.reader.searcher().doc_store_cache_stats(); - let ft_total_docs = db.node_index.reader.searcher().total_num_docs()?; + // let cache_stats = db.node_index.reader.searcher().doc_store_cache_stats(); + // let ft_total_docs = db.node_index.reader.searcher().total_num_docs()?; Ok(markup::new! { - h3 { "Key-Value-Store Statistics" } - table.border { - tbody { - tr { - th { "table name" } - th { "tree height" } - th { "stored bytes" } - th { "metadata bytes" } - th { "fragmented bytes" } - th { "branch pages" } - th { "leaf pages" } - } - @for (name, stats) in &stats { tr { - td { @name } - td { @stats.tree_height() } - td { @format_size(stats.stored_bytes(), DECIMAL) } - td { @format_size(stats.metadata_bytes(), DECIMAL) } - td { @format_size(stats.fragmented_bytes(), DECIMAL) } - td { @stats.branch_pages() } - td { @stats.leaf_pages() } - }} - } - } - h3 { "Search Engine Statistics" } - ul { - li { "Total documents: " @ft_total_docs } - li { "Cache misses: " @cache_stats.cache_misses } - li { "Cache hits: " @cache_stats.cache_hits } - } + // h3 { "Key-Value-Store Statistics" } + // table.border { + // tbody { + // tr { + // th { "table name" } + // th { "tree height" } + // th { "stored bytes" } + // th { "metadata bytes" } + // th { "fragmented bytes" } + // th { "branch pages" } + // th { "leaf pages" } + // } + // @for (name, stats) in &stats { tr { + // td { @name } + // td { @stats.tree_height() } + // td { @format_size(stats.stored_bytes(), DECIMAL) } + // td { @format_size(stats.metadata_bytes(), DECIMAL) } + // td { @format_size(stats.fragmented_bytes(), DECIMAL) } + // td { @stats.branch_pages() } + // td { @stats.leaf_pages() } + // }} + // } + // } + // h3 { "Search Engine Statistics" } + // ul { + // li { "Total documents: " @ft_total_docs } + // li { "Cache misses: " @cache_stats.cache_misses } + // li { "Cache hits: " @cache_stats.cache_hits } + // } }) } diff --git a/server/src/routes/ui/admin/user.rs b/server/src/routes/ui/admin/user.rs index 524f849..7ba6d4e 100644 --- a/server/src/routes/ui/admin/user.rs +++ b/server/src/routes/ui/admin/user.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin */ use crate::{ - database::DataAcid, + database::Database, routes::ui::{ account::session::AdminSession, error::MyResult, @@ -13,36 +13,23 @@ use crate::{ uri, }; use anyhow::{anyhow, Context}; -use jellybase::database::{redb::ReadableTable, Ser, TableExt, T_USER}; use jellycommon::user::{PermissionSet, UserPermission}; use rocket::{form::Form, get, post, FromForm, FromFormField, State}; #[get("/admin/users")] pub fn r_admin_users( _session: AdminSession, - database: &State, + database: &State, ) -> MyResult> { user_management(database, None) } fn user_management<'a>( - database: &DataAcid, + database: &Database, flash: Option>, ) -> MyResult> { // TODO this doesnt scale, pagination! - let users = { - let txn = database.begin_read()?; - let table = txn.open_table(T_USER)?; - let i = table - .iter()? - .map(|a| { - let (x, y) = a.unwrap(); - (x.value().to_owned(), y.value().0) - }) - .collect::>(); - drop(table); - i - }; + let users = database.list_users()?; let flash = flash.map(|f| f.map_err(|e| format!("{e:?}"))); Ok(LayoutPage { @@ -51,7 +38,7 @@ fn user_management<'a>( h1 { "User Management" } @FlashDisplay { flash: flash.clone() } h2 { "All Users" } - ul { @for (_, u) in &users { + ul { @for u in &users { li { a[href=uri!(r_admin_user(&u.name))] { @format!("{:?}", u.display_name) " (" @u.name ")" } } @@ -64,19 +51,19 @@ fn user_management<'a>( #[get("/admin/user/")] pub fn r_admin_user<'a>( _session: AdminSession, - database: &State, + database: &State, name: &'a str, ) -> MyResult> { manage_single_user(database, None, name.to_string()) } fn manage_single_user<'a>( - database: &DataAcid, + database: &Database, flash: Option>, name: String, ) -> MyResult> { - let user = T_USER - .get(database, &*name)? + let user = database + .get_user(&name)? .ok_or(anyhow!("user does not exist"))?; let flash = flash.map(|f| f.map_err(|e| format!("{e:?}"))); @@ -152,31 +139,21 @@ pub enum GrantState { #[post("/admin/update_user_permission", data = "")] pub fn r_admin_user_permission( session: AdminSession, - database: &State, + database: &State, form: Form, ) -> MyResult> { drop(session); let perm = serde_json::from_str::(&form.permission) .context("parsing provided permission")?; - let txn = database.begin_write()?; - let mut users = txn.open_table(T_USER)?; - - let mut user = users - .get(&*form.name)? - .ok_or(anyhow!("user missing"))? - .value() - .0; - - match form.action { - GrantState::Grant => drop(user.permissions.0.insert(perm.clone(), true)), - GrantState::Revoke => drop(user.permissions.0.insert(perm.clone(), false)), - GrantState::Unset => drop(user.permissions.0.remove(&perm)), - } - - users.insert(&*form.name, Ser(user))?; - drop(users); - txn.commit()?; + database.update_user(&form.name, |user| { + match form.action { + GrantState::Grant => drop(user.permissions.0.insert(perm.clone(), true)), + GrantState::Revoke => drop(user.permissions.0.insert(perm.clone(), false)), + GrantState::Unset => drop(user.permissions.0.remove(&perm)), + } + Ok(()) + })?; manage_single_user( database, @@ -188,12 +165,12 @@ pub fn r_admin_user_permission( #[post("/admin/remove_user", data = "")] pub fn r_admin_remove_user( session: AdminSession, - database: &State, + database: &State, form: Form, ) -> MyResult> { drop(session); - T_USER - .remove(database, form.name.as_str())? - .ok_or(anyhow!("user did not exist"))?; + if !database.delete_user(&form.name)? { + Err(anyhow!("user did not exist"))?; + } user_management(database, Some(Ok("User removed".into()))) } diff --git a/server/src/routes/ui/assets.rs b/server/src/routes/ui/assets.rs index 7eb8e98..689c7f1 100644 --- a/server/src/routes/ui/assets.rs +++ b/server/src/routes/ui/assets.rs @@ -7,12 +7,7 @@ use crate::routes::ui::{account::session::Session, error::MyResult, CacheControl use anyhow::{anyhow, Context}; use base64::Engine; use jellybase::{ - assetfed::AssetInner, - cache::async_cache_file, - database::{DataAcid, TableExt, T_NODE}, - federation::Federation, - permission::NodePermissionExt, - CONF, + assetfed::AssetInner, cache::async_cache_file, database::Database, federation::Federation, CONF, }; use jellycommon::{LocalTrack, PeopleGroup, SourceTrackKind, TrackSource}; use log::info; @@ -67,23 +62,21 @@ pub async fn resolve_asset(asset: AssetInner) -> anyhow::Result { #[get("/n//poster?")] pub async fn r_item_poster( - session: Session, - db: &State, + _session: Session, + db: &State, id: &str, width: Option, ) -> MyResult { - let node = T_NODE - .get(db, id)? - .only_if_permitted(&session.user.permissions) + // TODO perm + let node = db + .get_node_slug(id)? .ok_or(anyhow!("node does not exist"))?; - let mut asset = node.poster; + let mut asset = node.poster.clone(); if asset.is_none() { - if let Some(parent) = &node.parents.last() { - let parent = T_NODE - .get(db, parent.as_str())? - .ok_or(anyhow!("node does not exist"))?; - asset = parent.poster; + if let Some(parent) = node.parents.last().copied() { + let parent = db.get_node(parent)?.ok_or(anyhow!("node does not exist"))?; + asset = parent.poster.clone(); } }; let asset = asset.unwrap_or_else(|| { @@ -94,23 +87,21 @@ pub async fn r_item_poster( } #[get("/n//backdrop?")] pub async fn r_item_backdrop( - session: Session, - db: &State, + _session: Session, + db: &State, id: &str, width: Option, ) -> MyResult { - let node = T_NODE - .get(db, id)? - .only_if_permitted(&session.user.permissions) + // TODO perm + let node = db + .get_node_slug(id)? .ok_or(anyhow!("node does not exist"))?; - let mut asset = node.poster; + let mut asset = node.backdrop.clone(); if asset.is_none() { - if let Some(parent) = &node.parents.last() { - let parent = T_NODE - .get(db, parent.as_str())? - .ok_or(anyhow!("node does not exist"))?; - asset = parent.poster; + if let Some(parent) = node.parents.last().copied() { + let parent = db.get_node(parent)?.ok_or(anyhow!("node does not exist"))?; + asset = parent.backdrop.clone(); } }; let asset = asset.unwrap_or_else(|| { @@ -122,19 +113,18 @@ pub async fn r_item_backdrop( #[get("/n//person//asset?&")] pub async fn r_person_asset( - session: Session, - db: &State, + _session: Session, + db: &State, id: &str, index: usize, group: String, width: Option, ) -> MyResult { - T_NODE - .get(db, id)? - .only_if_permitted(&session.user.permissions) - .ok_or(anyhow!("node does not exist"))?; + // TODO perm - let node = T_NODE.get(db, id)?.unwrap_or_default(); + let node = db + .get_node_slug(id)? + .ok_or(anyhow!("node does not exist"))?; let app = node .people .get(&PeopleGroup::from_str(&group).map_err(|()| anyhow!("unknown people group"))?) @@ -155,19 +145,18 @@ pub async fn r_person_asset( #[get("/n//thumbnail?&")] pub async fn r_node_thumbnail( - session: Session, - db: &State, + _session: Session, + db: &State, fed: &State, id: &str, t: f64, width: Option, ) -> MyResult { - let node = T_NODE - .get(db, id)? - .only_if_permitted(&session.user.permissions) + let node = db + .get_node_slug(id)? .ok_or(anyhow!("node does not exist"))?; - let media = node.media.ok_or(anyhow!("no media"))?; + let media = node.media.as_ref().ok_or(anyhow!("no media"))?; let (thumb_track_index, thumb_track) = media .tracks .iter() diff --git a/server/src/routes/ui/browser.rs b/server/src/routes/ui/browser.rs index 9a5fb6c..a15dc27 100644 --- a/server/src/routes/ui/browser.rs +++ b/server/src/routes/ui/browser.rs @@ -10,8 +10,7 @@ use super::{ node::NodeCard, sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm, SortOrder, SortProperty}, }; -use crate::{database::DataAcid, uri}; -use jellybase::database::{redb::ReadableTable, T_NODE, T_USER_NODE}; +use crate::{database::Database, uri}; use rocket::{get, State}; /// This function is a stub and only useful for use in the uri! macro. @@ -21,30 +20,11 @@ pub fn r_all_items() {} #[get("/items?&")] pub fn r_all_items_filter( sess: Session, - db: &State, + db: &State, page: Option, filter: NodeFilterSort, ) -> Result, MyError> { - let mut items = { - let txn = db.begin_read()?; - let nodes = txn.open_table(T_NODE)?; - let node_users = txn.open_table(T_USER_NODE)?; - let i = nodes - .iter()? - .map(|a| { - let (x, y) = a.unwrap(); - let (x, y) = (x.value().to_owned(), y.value().0); - let z = node_users - .get(&(sess.user.name.as_str(), x.as_str())) - .unwrap() - .map(|z| z.value().0) - .unwrap_or_default(); - (x, y, z) - }) - .collect::>(); - drop(nodes); - i - }; + let mut items = db.list_nodes_with_udata(sess.user.name.as_str())?; filter_and_sort_nodes( &filter, @@ -65,8 +45,8 @@ pub fn r_all_items_filter( .page.dir { h1 { "All Items" } @NodeFilterSortForm { f: &filter } - ul.children { @for (id, node, udata) in &items[from..to] { - li {@NodeCard { id, node, udata }} + ul.children { @for (node, udata) in &items[from..to] { + li {@NodeCard { node, udata }} }} p.pagecontrols { span.current { "Page " @{page + 1} " of " @max_page " " } diff --git a/server/src/routes/ui/error.rs b/server/src/routes/ui/error.rs index 67924e2..ee593a2 100644 --- a/server/src/routes/ui/error.rs +++ b/server/src/routes/ui/error.rs @@ -102,45 +102,3 @@ impl From for MyError { MyError(anyhow::anyhow!("{err}")) } } -impl From for MyError { - fn from(err: jellybase::database::redb::CommitError) -> Self { - MyError(anyhow::anyhow!("database oopsie during commit: {err}")) - } -} -impl From for MyError { - fn from(err: jellybase::database::redb::CompactionError) -> Self { - MyError(anyhow::anyhow!("database oopsie during compaction: {err}")) - } -} -impl From for MyError { - fn from(err: jellybase::database::redb::DatabaseError) -> Self { - MyError(anyhow::anyhow!("generic database oopsie: {err}")) - } -} -impl From for MyError { - fn from(err: jellybase::database::redb::SavepointError) -> Self { - MyError(anyhow::anyhow!( - "database oopsie during savepointing: {err}" - )) - } -} -impl From for MyError { - fn from(err: jellybase::database::redb::StorageError) -> Self { - MyError(anyhow::anyhow!("database oopsie, storage error: {err}")) - } -} -impl From for MyError { - fn from(err: jellybase::database::redb::TableError) -> Self { - MyError(anyhow::anyhow!("database oopsie, table error: {err}")) - } -} -impl From for MyError { - fn from(err: jellybase::database::redb::TransactionError) -> Self { - MyError(anyhow::anyhow!("database oopsie during transaction: {err}")) - } -} -impl From for MyError { - fn from(err: jellybase::database::tantivy::TantivyError) -> Self { - MyError(anyhow::anyhow!("database during search: {err}")) - } -} diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs index 8eacfde..ebed647 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/routes/ui/home.rs @@ -5,40 +5,18 @@ */ use super::{account::session::Session, layout::LayoutPage, node::NodeCard}; use crate::{ - database::DataAcid, + database::Database, routes::ui::{error::MyResult, layout::DynLayoutPage}, }; use chrono::{Datelike, Utc}; -use jellybase::{ - database::{redb::ReadableTable, T_NODE, T_USER_NODE}, - CONF, -}; +use jellybase::CONF; use jellycommon::{user::WatchedState, Rating}; use rocket::{get, State}; use tokio::fs::read_to_string; #[get("/")] -pub fn r_home(sess: Session, db: &State) -> MyResult { - let mut items = { - let txn = db.begin_read()?; - let nodes = txn.open_table(T_NODE)?; - let node_users = txn.open_table(T_USER_NODE)?; - let i = nodes - .iter()? - .map(|a| { - let (x, y) = a.unwrap(); - let (x, y) = (x.value().to_owned(), y.value().0); - let z = node_users - .get(&(sess.user.name.as_str(), x.as_str())) - .unwrap() - .map(|z| z.value().0) - .unwrap_or_default(); - (x, y, z) - }) - .collect::>(); - drop(nodes); - i - }; +pub fn r_home(sess: Session, db: &State) -> MyResult { + let mut items = db.list_nodes_with_udata(&sess.user.name)?; let random = (0..16) .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone())) .collect::>(); @@ -52,7 +30,7 @@ pub fn r_home(sess: Session, db: &State) -> MyResult { // .into_iter() // .collect::>(); - items.sort_by_key(|(_, n, _)| { + items.sort_by_key(|(n, _)| { n.ratings .get(&Rating::Tmdb) .map(|x| (*x * -1000.) as i32) @@ -62,11 +40,11 @@ pub fn r_home(sess: Session, db: &State) -> MyResult { let top_rated = items .iter() .take(16) - .filter(|(_, n, _)| n.ratings.contains_key(&Rating::Tmdb)) + .filter(|(n, _)| n.ratings.contains_key(&Rating::Tmdb)) .map(|k| k.to_owned()) .collect::>(); - items.sort_by_key(|(_, n, _)| n.release_date.map(|d| -d).unwrap_or(i64::MAX)); + items.sort_by_key(|(n, _)| n.release_date.map(|d| -d).unwrap_or(i64::MAX)); let latest = items .iter() @@ -76,13 +54,13 @@ pub fn r_home(sess: Session, db: &State) -> MyResult { let continue_watching = items .iter() - .filter(|(_, _, u)| matches!(u.watched, WatchedState::Progress(_))) + .filter(|(_, u)| matches!(u.watched, WatchedState::Progress(_))) .map(|k| k.to_owned()) .collect::>(); let watchlist = items .iter() - .filter(|(_, _, u)| matches!(u.watched, WatchedState::Pending)) + .filter(|(_, u)| matches!(u.watched, WatchedState::Pending)) .map(|k| k.to_owned()) .collect::>(); @@ -95,28 +73,28 @@ pub fn r_home(sess: Session, db: &State) -> MyResult { // }} @if !continue_watching.is_empty() { h2 { "Continue Watching" } - ul.children.hlist {@for (id, node, udata) in &continue_watching { - li { @NodeCard { id, node, udata } } + ul.children.hlist {@for (node, udata) in &continue_watching { + li { @NodeCard { node, udata } } }} } @if !watchlist.is_empty() { h2 { "Watchlist" } - ul.children.hlist {@for (id, node, udata) in &watchlist { - li { @NodeCard { id, node, udata } } + ul.children.hlist {@for (node, udata) in &watchlist { + li { @NodeCard { node, udata } } }} } h2 { "Today's Picks" } - ul.children.hlist {@for (id, node, udata) in &random { - li { @NodeCard { id, node, udata } } + ul.children.hlist {@for (node, udata) in &random { + li { @NodeCard { node, udata } } }} h2 { "Latest Releases" } - ul.children.hlist {@for (id, node, udata) in &latest { - li { @NodeCard { id, node, udata } } + ul.children.hlist {@for (node, udata) in &latest { + li { @NodeCard { node, udata } } }} @if !top_rated.is_empty() { h2 { "Top Rated" } - ul.children.hlist {@for (id, node, udata) in &top_rated { - li { @NodeCard { id, node, udata } } + ul.children.hlist {@for (node, udata) in &top_rated { + li { @NodeCard { node, udata } } }} } }, diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 3307d50..5cc8a2f 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + /* 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. @@ -12,7 +14,7 @@ use super::{ sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm, SortOrder, SortProperty}, }; use crate::{ - database::DataAcid, + database::Database, routes::{ api::AcceptJson, ui::{ @@ -30,13 +32,9 @@ use crate::{ }; use anyhow::{anyhow, Result}; use chrono::DateTime; -use jellybase::{ - database::{TableExt, T_NODE, T_USER_NODE}, - permission::NodePermissionExt, -}; use jellycommon::{ user::{NodeUserData, WatchedState}, - Chapter, MediaInfo, Node, NodeKind, PeopleGroup, Rating, SourceTrackKind, + Chapter, MediaInfo, Node, NodeID, NodeKind, PeopleGroup, Rating, SourceTrackKind, }; use rocket::{get, serde::json::Json, Either, State}; @@ -50,21 +48,14 @@ pub fn r_library_node(id: String) { pub async fn r_library_node_filter<'a>( session: Session, id: &'a str, - db: &'a State, + db: &'a State, aj: AcceptJson, filter: NodeFilterSort, ) -> MyResult, Json>> { - let node = T_NODE - .get(db, id)? - .only_if_permitted(&session.user.permissions) - .ok_or(anyhow!("node does not exist"))?; - - let udata = T_USER_NODE - .get(db, &(session.user.name.as_str(), id))? - .unwrap_or_default(); + let (node, udata) = db.get_node_with_userdata(NodeID::from_slug(id), &session)?; if *aj { - return Ok(Either::Right(Json(node))); + return Ok(Either::Right(Json((*node).clone()))); } // let mut children = node @@ -111,22 +102,22 @@ pub async fn r_library_node_filter<'a>( } markup::define! { - NodeCard<'a>(id: &'a str, node: &'a Node, udata: &'a NodeUserData) { + NodeCard<'a>(node: &'a Node, udata: &'a NodeUserData) { @let cls = format!("node card poster {}", aspect_class(node.kind.unwrap_or_default())); div[class=cls] { .poster { - a[href=uri!(r_library_node(id))] { - img[src=uri!(r_item_poster(id, Some(1024))), loading="lazy"]; + a[href=uri!(r_library_node(&node.slug))] { + img[src=uri!(r_item_poster(&node.slug, Some(1024))), loading="lazy"]; } .cardhover.item { @if node.media.is_some() { - a.play.icon[href=&uri!(r_player(id, PlayerConfig::default()))] { "play_arrow" } + a.play.icon[href=&uri!(r_player(&node.slug, PlayerConfig::default()))] { "play_arrow" } } @Props { node, udata, full: false } } } div.title { - a[href=uri!(r_library_node(id))] { + a[href=uri!(r_library_node(&node.slug))] { @node.title } } @@ -137,7 +128,7 @@ markup::define! { } } } - NodePage<'a>(id: &'a str, node: &'a Node, udata: &'a NodeUserData, children: &'a [(String, Node, NodeUserData)], path: &'a [(String, Node)], filter: &'a NodeFilterSort) { + NodePage<'a>(id: &'a str, node: &'a Node, udata: &'a NodeUserData, children: &'a [(Arc, NodeUserData)], path: &'a [(String, Node)], filter: &'a NodeFilterSort) { @if !matches!(node.kind.unwrap_or_default(), NodeKind::Collection) { img.backdrop[src=uri!(r_item_backdrop(id, Some(2048))), loading="lazy"]; } @@ -240,13 +231,13 @@ markup::define! { } @match node.kind.unwrap_or_default() { NodeKind::Show | NodeKind::Series | NodeKind::Season => { - ol { @for (id, c, _) in children.iter() { - li { a[href=uri!(r_library_node(id))] { @c.title } } + ol { @for (c, _) in children.iter() { + li { a[href=uri!(r_library_node(&c.slug))] { @c.title } } }} } NodeKind::Collection | NodeKind::Channel | _ => { - ul.children {@for (id, node, udata) in children.iter() { - li { @NodeCard { id, node, udata } } + ul.children {@for (node, udata) in children.iter() { + li { @NodeCard { node, udata } } }} } } @@ -324,24 +315,19 @@ pub fn format_duration(mut d: f64) -> String { pub trait DatabaseNodeUserDataExt { fn get_node_with_userdata( &self, - id: &str, + id: NodeID, session: &Session, - ) -> Result<(String, Node, NodeUserData)>; + ) -> Result<(Arc, NodeUserData)>; } -impl DatabaseNodeUserDataExt for DataAcid { +impl DatabaseNodeUserDataExt for Database { fn get_node_with_userdata( &self, - id: &str, + id: NodeID, session: &Session, - ) -> Result<(String, Node, NodeUserData)> { + ) -> Result<(Arc, NodeUserData)> { Ok(( - id.to_owned(), - T_NODE - .get(self, id)? - .only_if_permitted(&session.user.permissions) - .ok_or(anyhow!("node does not exist: {id}"))?, - T_USER_NODE - .get(self, &(session.user.name.as_str(), id))? + self.get_node(id)?.ok_or(anyhow!("node does not exist"))?, + self.get_node_udata(id, &session.user.name)? .unwrap_or_default(), )) } diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index 178cbba..b24e5e9 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -8,7 +8,7 @@ use super::{ layout::LayoutPage, }; use crate::{ - database::DataAcid, + database::Database, routes::{ stream::rocket_uri_macro_r_stream, ui::{assets::rocket_uri_macro_r_item_backdrop, error::MyResult, layout::DynLayoutPage}, @@ -16,11 +16,7 @@ use crate::{ uri, }; use anyhow::anyhow; -use jellybase::{ - database::{TableExt, T_NODE}, - permission::PermissionSetExt, - CONF, -}; +use jellybase::{permission::PermissionSetExt, CONF}; use jellycommon::{ stream::{StreamFormat, StreamSpec}, user::{PermissionSet, PlayerKind, UserPermission}, @@ -28,6 +24,7 @@ use jellycommon::{ }; use markup::DynRender; use rocket::{get, response::Redirect, Either, FromForm, State, UriDisplayQuery}; +use std::sync::Arc; #[derive(FromForm, Default, Clone, Debug, UriDisplayQuery)] pub struct PlayerConfig { @@ -63,11 +60,13 @@ fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: & #[get("/n//player?", rank = 4)] pub fn r_player<'a>( sess: Session, - db: &'a State, + db: &'a State, id: &'a str, conf: PlayerConfig, ) -> MyResult, Redirect>> { - let item = T_NODE.get(db, id)?.ok_or(anyhow!("node does not exist"))?; + let item = db + .get_node_slug(id)? + .ok_or(anyhow!("node does not exist"))?; let native_session = |action: &str| { let perm = [ @@ -130,7 +129,7 @@ pub fn r_player<'a>( })) } -pub fn player_conf<'a>(item: Node, playing: bool) -> anyhow::Result> { +pub fn player_conf<'a>(item: Arc, playing: bool) -> anyhow::Result> { let mut audio_tracks = vec![]; let mut video_tracks = vec![]; let mut sub_tracks = vec![]; diff --git a/server/src/routes/ui/search.rs b/server/src/routes/ui/search.rs index c1f9865..ac37b80 100644 --- a/server/src/routes/ui/search.rs +++ b/server/src/routes/ui/search.rs @@ -2,63 +2,27 @@ use super::{ account::session::Session, error::MyResult, layout::{DynLayoutPage, LayoutPage}, - node::NodeCard, -}; -use anyhow::{anyhow, Context}; -use jellybase::{ - database::{ - tantivy::{ - collector::{Count, TopDocs}, - query::QueryParser, - schema::Value, - TantivyDocument, - }, - DataAcid, TableExt, T_NODE, T_USER_NODE, - }, - permission::NodePermissionExt, + node::{DatabaseNodeUserDataExt, NodeCard}, }; +use jellybase::database::Database; use rocket::{get, State}; use std::time::Instant; #[get("/search?&")] pub async fn r_search<'a>( session: Session, - db: &State, + db: &State, query: Option<&str>, page: Option, ) -> MyResult> { let timing = Instant::now(); let results = if let Some(query) = query { - let query = QueryParser::for_index( - &db.node_index.index, - vec![db.node_index.title, db.node_index.description], - ) - .parse_query(query) - .context("parsing query")?; - - let searcher = db.node_index.reader.searcher(); - let sres = searcher.search( - &query, - &TopDocs::with_limit(32).and_offset(page.unwrap_or_default() * 32), - )?; - let scount = searcher.search(&query, &Count)?; - - let mut results = Vec::new(); - for (_, daddr) in sres { - let doc: TantivyDocument = searcher.doc(daddr)?; - let id = doc.get_first(db.node_index.id).unwrap().as_str().unwrap(); - - let node = T_NODE - .get(db, id)? - .only_if_permitted(&session.user.permissions) - .ok_or(anyhow!("node does not exist"))?; - let udata = T_USER_NODE - .get(db, &(session.user.name.as_str(), id))? - .unwrap_or_default(); - - results.push((id.to_owned(), node, udata)); - } - Some((scount, results)) + let (count, ids) = db.search(query, page.unwrap_or_default())?; + let nodes = ids + .into_iter() + .map(|id| db.get_node_with_userdata(id, &session)) + .collect::, anyhow::Error>>()?; + Some((count, nodes)) } else { None }; @@ -77,8 +41,8 @@ pub async fn r_search<'a>( @if let Some((count, results)) = &results { h2 { "Results" } p.stats { @format!("Found {count} nodes in {search_dur:?}.") } - ul.children {@for (id, node, udata) in results.iter() { - li { @NodeCard { id, node, udata } } + ul.children {@for (node, udata) in results.iter() { + li { @NodeCard { node, udata } } }} // TODO pagination } diff --git a/server/src/routes/ui/sort.rs b/server/src/routes/ui/sort.rs index bb71184..705b616 100644 --- a/server/src/routes/ui/sort.rs +++ b/server/src/routes/ui/sort.rs @@ -134,10 +134,10 @@ pub enum SortOrder { pub fn filter_and_sort_nodes( f: &NodeFilterSort, default_sort: (SortProperty, SortOrder), - nodes: &mut Vec<(String, Node, NodeUserData)>, + nodes: &mut Vec<(Node, NodeUserData)>, ) { let sort_prop = f.sort_by.unwrap_or(default_sort.0); - nodes.retain(|(_id, node, udata)| { + nodes.retain(|(node, udata)| { let mut o = true; if let Some(prop) = &f.filter_kind { for p in FilterProperty::ALL { @@ -175,34 +175,32 @@ pub fn filter_and_sort_nodes( }); match sort_prop { SortProperty::Duration => { - nodes.sort_by_key(|(_, n, _)| (n.media.as_ref().unwrap().duration * 1000.) as i64) + nodes.sort_by_key(|(n, _)| (n.media.as_ref().unwrap().duration * 1000.) as i64) } SortProperty::ReleaseDate => { - nodes.sort_by_key(|(_, n, _)| n.release_date.expect("asserted above")) + nodes.sort_by_key(|(n, _)| n.release_date.expect("asserted above")) } - SortProperty::Title => nodes.sort_by(|(_, a, _), (_, b, _)| a.title.cmp(&b.title)), - SortProperty::RatingRottenTomatoes => nodes.sort_by_cached_key(|(_, n, _)| { + SortProperty::Title => nodes.sort_by(|(a, _), (b, _)| a.title.cmp(&b.title)), + SortProperty::RatingRottenTomatoes => nodes.sort_by_cached_key(|(n, _)| { SortAnyway(*n.ratings.get(&Rating::RottenTomatoes).unwrap_or(&0.)) }), - SortProperty::RatingMetacritic => nodes.sort_by_cached_key(|(_, n, _)| { + SortProperty::RatingMetacritic => nodes.sort_by_cached_key(|(n, _)| { SortAnyway(*n.ratings.get(&Rating::Metacritic).unwrap_or(&0.)) }), - SortProperty::RatingImdb => nodes.sort_by_cached_key(|(_, n, _)| { - SortAnyway(*n.ratings.get(&Rating::Imdb).unwrap_or(&0.)) - }), - SortProperty::RatingTmdb => nodes.sort_by_cached_key(|(_, n, _)| { - SortAnyway(*n.ratings.get(&Rating::Tmdb).unwrap_or(&0.)) - }), - SortProperty::RatingYoutubeViews => nodes.sort_by_cached_key(|(_, n, _)| { + SortProperty::RatingImdb => nodes + .sort_by_cached_key(|(n, _)| SortAnyway(*n.ratings.get(&Rating::Imdb).unwrap_or(&0.))), + SortProperty::RatingTmdb => nodes + .sort_by_cached_key(|(n, _)| SortAnyway(*n.ratings.get(&Rating::Tmdb).unwrap_or(&0.))), + SortProperty::RatingYoutubeViews => nodes.sort_by_cached_key(|(n, _)| { SortAnyway(*n.ratings.get(&Rating::YoutubeViews).unwrap_or(&0.)) }), - SortProperty::RatingYoutubeLikes => nodes.sort_by_cached_key(|(_, n, _)| { + SortProperty::RatingYoutubeLikes => nodes.sort_by_cached_key(|(n, _)| { SortAnyway(*n.ratings.get(&Rating::YoutubeLikes).unwrap_or(&0.)) }), - SortProperty::RatingYoutubeFollowers => nodes.sort_by_cached_key(|(_, n, _)| { + SortProperty::RatingYoutubeFollowers => nodes.sort_by_cached_key(|(n, _)| { SortAnyway(*n.ratings.get(&Rating::YoutubeFollowers).unwrap_or(&0.)) }), - SortProperty::RatingUser => nodes.sort_by_cached_key(|(_, _, u)| u.rating), + SortProperty::RatingUser => nodes.sort_by_cached_key(|(_, u)| u.rating), } match f.sort_order.unwrap_or(default_sort.1) { -- cgit v1.2.3-70-g09d2