diff options
author | metamuffin <metamuffin@disroot.org> | 2025-01-29 18:03:06 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-01-29 18:03:06 +0100 |
commit | db511d3fe50f05329615f718515fab1b80d9e06a (patch) | |
tree | 7969fea01be100cbe4385ad13a14940a987ac513 /server/src | |
parent | 82e8a55a1496ae9132e13e7286fe1c0d57d586d3 (diff) | |
download | jellything-db511d3fe50f05329615f718515fab1b80d9e06a.tar jellything-db511d3fe50f05329615f718515fab1b80d9e06a.tar.bz2 jellything-db511d3fe50f05329615f718515fab1b80d9e06a.tar.zst |
no direct redb access
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/main.rs | 33 | ||||
-rw-r--r-- | server/src/routes/api/mod.rs | 25 | ||||
-rw-r--r-- | server/src/routes/mod.rs | 7 | ||||
-rw-r--r-- | server/src/routes/stream.rs | 20 | ||||
-rw-r--r-- | server/src/routes/ui/account/mod.rs | 54 | ||||
-rw-r--r-- | server/src/routes/ui/account/session/guard.rs | 17 | ||||
-rw-r--r-- | server/src/routes/ui/account/settings.rs | 66 | ||||
-rw-r--r-- | server/src/routes/ui/admin/mod.rs | 170 | ||||
-rw-r--r-- | server/src/routes/ui/admin/user.rs | 67 | ||||
-rw-r--r-- | server/src/routes/ui/assets.rs | 71 | ||||
-rw-r--r-- | server/src/routes/ui/browser.rs | 30 | ||||
-rw-r--r-- | server/src/routes/ui/error.rs | 42 | ||||
-rw-r--r-- | server/src/routes/ui/home.rs | 60 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 62 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 17 | ||||
-rw-r--r-- | server/src/routes/ui/search.rs | 58 | ||||
-rw-r--r-- | server/src/routes/ui/sort.rs | 32 | ||||
-rw-r--r-- | server/src/routes/userdata.rs | 113 |
18 files changed, 310 insertions, 634 deletions
diff --git a/server/src/main.rs b/server/src/main.rs index cc7ba5f..407e127 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -8,13 +8,8 @@ use crate::routes::ui::{account::hash_password, admin::log::enable_logging}; use anyhow::Context; -use database::DataAcid; -use jellybase::{ - database::{redb::ReadableTable, Ser, T_USER}, - federation::Federation, - CONF, SECRETS, -}; -use jellycommon::user::User; +use database::Database; +use jellybase::{federation::Federation, CONF, SECRETS}; use log::{error, info, warn}; use routes::build_rocket; use tokio::fs::create_dir_all; @@ -29,7 +24,7 @@ async fn main() { log::warn!("authentification bypass enabled"); create_dir_all(&CONF.cache_path).await.unwrap(); - let database = DataAcid::open(&CONF.database_path) + let database = Database::open(&CONF.database_path) .context("opening database") .unwrap(); let federation = Federation::initialize(); @@ -37,27 +32,9 @@ async fn main() { if let Some(username) = &CONF.admin_username && let Some(password) = &SECRETS.admin_password { - let txn = database.begin_write().unwrap(); - let mut users = txn.open_table(T_USER).unwrap(); - - let admin = users.get(username.as_str()).unwrap().map(|x| x.value().0); - users - .insert( - username.as_str(), - Ser(User { - admin: true, - name: username.clone(), - password: hash_password(username, password), - ..admin.unwrap_or_else(|| User { - display_name: "Admin".to_string(), - ..Default::default() - }) - }), - ) + database + .create_admin_user(username, hash_password(username, password)) .unwrap(); - - drop(users); - txn.commit().unwrap(); } else { info!("admin account disabled") } diff --git a/server/src/routes/api/mod.rs b/server/src/routes/api/mod.rs index 4bd5fa9..065c136 100644 --- a/server/src/routes/api/mod.rs +++ b/server/src/routes/api/mod.rs @@ -7,13 +7,9 @@ use super::ui::{ account::{login_logic, session::AdminSession}, error::MyResult, }; -use crate::database::DataAcid; -use anyhow::{anyhow, Context}; -use jellybase::{ - assetfed::AssetInner, - database::{TableExt, T_NODE}, -}; -use jellycommon::{user::CreateSessionParams, Node}; +use crate::database::Database; +use jellybase::assetfed::AssetInner; +use jellycommon::user::CreateSessionParams; use rocket::{ get, http::MediaType, @@ -39,7 +35,7 @@ pub fn r_api_version() -> &'static str { #[post("/api/create_session", data = "<data>")] pub fn r_api_account_login( - database: &State<DataAcid>, + database: &State<Database>, data: Json<CreateSessionParams>, ) -> MyResult<Value> { let token = login_logic( @@ -52,19 +48,6 @@ pub fn r_api_account_login( Ok(json!(token)) } -#[get("/api/node_raw/<id>")] -pub fn r_api_node_raw( - admin: AdminSession, - database: &State<DataAcid>, - id: &str, -) -> MyResult<Json<Node>> { - drop(admin); - let node = T_NODE - .get(database, id) - .context("retrieving library node")? - .ok_or(anyhow!("node does not exist"))?; - Ok(Json(node)) -} #[get("/api/asset_token_raw/<token>")] pub fn r_api_asset_token_raw(admin: AdminSession, token: &str) -> MyResult<Json<AssetInner>> { drop(admin); diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 0853ef9..98bde38 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -4,8 +4,8 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use self::playersync::{r_streamsync, PlayersyncChannels}; -use crate::{database::DataAcid, routes::ui::error::MyResult}; -use api::{r_api_account_login, r_api_asset_token_raw, r_api_node_raw, r_api_root, r_api_version}; +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; use jellybase::{federation::Federation, CONF, SECRETS}; use log::warn; @@ -60,7 +60,7 @@ macro_rules! uri { }; } -pub fn build_rocket(database: DataAcid, federation: Federation) -> Rocket<Build> { +pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> { rocket::build() .configure(Config { address: std::env::var("BIND_ADDR") @@ -138,7 +138,6 @@ pub fn build_rocket(database: DataAcid, federation: Federation) -> Rocket<Build> r_api_version, r_api_account_login, r_api_root, - r_api_node_raw, r_api_asset_token_raw, ], ) diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index d057aa7..401faba 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -4,14 +4,9 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::ui::{account::session::Session, error::MyError}; -use crate::database::DataAcid; +use crate::database::Database; use anyhow::{anyhow, Result}; -use jellybase::{ - database::{TableExt, T_NODE}, - federation::Federation, - permission::{NodePermissionExt, PermissionSetExt}, - SECRETS, -}; +use jellybase::{federation::Federation, permission::PermissionSetExt, SECRETS}; use jellycommon::{ config::FederationAccount, stream::StreamSpec, @@ -48,15 +43,16 @@ pub async fn r_stream_head( pub async fn r_stream( session: Session, federation: &State<Federation>, - db: &State<DataAcid>, + db: &State<Database>, id: &str, range: Option<RequestRange>, spec: StreamSpec, ) -> Result<Either<StreamResponse, RedirectResponse>, MyError> { - 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 media = node .media .as_ref() @@ -71,7 +67,7 @@ pub async fn r_stream( .permissions .assert(&UserPermission::FederatedContent)?; - let track = &node.media.ok_or(anyhow!("no media"))?.tracks[ti]; + let track = &node.media.as_ref().ok_or(anyhow!("no media"))?.tracks[ti]; let host = track .federated .last() 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 = "<form>")] pub fn r_account_register_post<'a>( - database: &'a State<DataAcid>, + database: &'a State<Database>, _sess: Option<Session>, form: Form<Contextual<'a, RegisterForm>>, ) -> MyResult<DynLayoutPage<'a>> { @@ -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 = "<form>")] pub fn r_account_login_post( - database: &State<DataAcid>, + database: &State<Database>, jar: &CookieJar, form: Form<Contextual<LoginForm>>, ) -> MyResult<Redirect> { @@ -202,17 +184,17 @@ pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Redirect> { } pub fn login_logic( - database: &DataAcid, + database: &Database, username: &str, password: &str, expire: Option<i64>, drop_permissions: Option<HashSet<UserPermission>>, ) -> MyResult<String> { - // 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 <metamuffin.org> */ 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<DataAcid>>().await.unwrap(); + let db = req.guard::<&State<Database>>().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 = "<form>")] pub fn r_account_settings_post( session: Session, - database: &State<DataAcid>, + database: &State<Database>, form: Form<Contextual<SettingsForm>>, ) -> MyResult<DynLayoutPage<'static>> { 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<DataAcid>, + database: &State<Database>, ) -> MyResult<DynLayoutPage<'static>> { admin_dashboard(database, None).await } pub async fn admin_dashboard<'a>( - database: &DataAcid, + database: &Database, flash: Option<MyResult<String>>, ) -> MyResult<DynLayoutPage<'a>> { - 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::<Vec<_>>(); - 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<DataAcid>, + database: &State<Database>, ) -> MyResult<DynLayoutPage<'static>> { let i = format!("{}", rand::rng().random::<u128>()); - 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 = "<form>")] pub async fn r_admin_remove_invite( session: AdminSession, - database: &State<DataAcid>, + database: &State<Database>, form: Form<DeleteInvite>, ) -> MyResult<DynLayoutPage<'static>> { 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<DataAcid>, + database: &State<Database>, ) -> MyResult<DynLayoutPage<'static>> { 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<DataAcid>, + database: &State<Database>, ) -> MyResult<DynLayoutPage<'static>> { 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<DynRender> { - 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<DynRender> { + // 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 <metamuffin.org> */ 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<DataAcid>, + database: &State<Database>, ) -> MyResult<DynLayoutPage<'static>> { user_management(database, None) } fn user_management<'a>( - database: &DataAcid, + database: &Database, flash: Option<MyResult<String>>, ) -> MyResult<DynLayoutPage<'a>> { // 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::<Vec<_>>(); - 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/<name>")] pub fn r_admin_user<'a>( _session: AdminSession, - database: &State<DataAcid>, + database: &State<Database>, name: &'a str, ) -> MyResult<DynLayoutPage<'a>> { manage_single_user(database, None, name.to_string()) } fn manage_single_user<'a>( - database: &DataAcid, + database: &Database, flash: Option<MyResult<String>>, name: String, ) -> MyResult<DynLayoutPage<'a>> { - 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 = "<form>")] pub fn r_admin_user_permission( session: AdminSession, - database: &State<DataAcid>, + database: &State<Database>, form: Form<UserPermissionForm>, ) -> MyResult<DynLayoutPage<'static>> { drop(session); let perm = serde_json::from_str::<UserPermission>(&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 = "<form>")] pub fn r_admin_remove_user( session: AdminSession, - database: &State<DataAcid>, + database: &State<Database>, form: Form<DeleteUser>, ) -> MyResult<DynLayoutPage<'static>> { 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<PathBuf> { #[get("/n/<id>/poster?<width>")] pub async fn r_item_poster( - session: Session, - db: &State<DataAcid>, + _session: Session, + db: &State<Database>, id: &str, width: Option<usize>, ) -> MyResult<Redirect> { - 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/<id>/backdrop?<width>")] pub async fn r_item_backdrop( - session: Session, - db: &State<DataAcid>, + _session: Session, + db: &State<Database>, id: &str, width: Option<usize>, ) -> MyResult<Redirect> { - 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/<id>/person/<index>/asset?<group>&<width>")] pub async fn r_person_asset( - session: Session, - db: &State<DataAcid>, + _session: Session, + db: &State<Database>, id: &str, index: usize, group: String, width: Option<usize>, ) -> MyResult<Redirect> { - 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/<id>/thumbnail?<t>&<width>")] pub async fn r_node_thumbnail( - session: Session, - db: &State<DataAcid>, + _session: Session, + db: &State<Database>, fed: &State<Federation>, id: &str, t: f64, width: Option<usize>, ) -> MyResult<Redirect> { - 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?<page>&<filter..>")] pub fn r_all_items_filter( sess: Session, - db: &State<DataAcid>, + db: &State<Database>, page: Option<usize>, filter: NodeFilterSort, ) -> Result<DynLayoutPage<'_>, 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::<Vec<_>>(); - 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<serde_json::Error> for MyError { MyError(anyhow::anyhow!("{err}")) } } -impl From<jellybase::database::redb::CommitError> for MyError { - fn from(err: jellybase::database::redb::CommitError) -> Self { - MyError(anyhow::anyhow!("database oopsie during commit: {err}")) - } -} -impl From<jellybase::database::redb::CompactionError> for MyError { - fn from(err: jellybase::database::redb::CompactionError) -> Self { - MyError(anyhow::anyhow!("database oopsie during compaction: {err}")) - } -} -impl From<jellybase::database::redb::DatabaseError> for MyError { - fn from(err: jellybase::database::redb::DatabaseError) -> Self { - MyError(anyhow::anyhow!("generic database oopsie: {err}")) - } -} -impl From<jellybase::database::redb::SavepointError> for MyError { - fn from(err: jellybase::database::redb::SavepointError) -> Self { - MyError(anyhow::anyhow!( - "database oopsie during savepointing: {err}" - )) - } -} -impl From<jellybase::database::redb::StorageError> for MyError { - fn from(err: jellybase::database::redb::StorageError) -> Self { - MyError(anyhow::anyhow!("database oopsie, storage error: {err}")) - } -} -impl From<jellybase::database::redb::TableError> for MyError { - fn from(err: jellybase::database::redb::TableError) -> Self { - MyError(anyhow::anyhow!("database oopsie, table error: {err}")) - } -} -impl From<jellybase::database::redb::TransactionError> for MyError { - fn from(err: jellybase::database::redb::TransactionError) -> Self { - MyError(anyhow::anyhow!("database oopsie during transaction: {err}")) - } -} -impl From<jellybase::database::tantivy::TantivyError> 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<DataAcid>) -> MyResult<DynLayoutPage> { - 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::<Vec<_>>(); - drop(nodes); - i - }; +pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { + 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::<Vec<_>>(); @@ -52,7 +30,7 @@ pub fn r_home(sess: Session, db: &State<DataAcid>) -> MyResult<DynLayoutPage> { // .into_iter() // .collect::<Vec<_>>(); - 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<DataAcid>) -> MyResult<DynLayoutPage> { 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::<Vec<_>>(); - 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<DataAcid>) -> MyResult<DynLayoutPage> { 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::<Vec<_>>(); let watchlist = items .iter() - .filter(|(_, _, u)| matches!(u.watched, WatchedState::Pending)) + .filter(|(_, u)| matches!(u.watched, WatchedState::Pending)) .map(|k| k.to_owned()) .collect::<Vec<_>>(); @@ -95,28 +73,28 @@ pub fn r_home(sess: Session, db: &State<DataAcid>) -> MyResult<DynLayoutPage> { // }} @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<DataAcid>, + db: &'a State<Database>, aj: AcceptJson, filter: NodeFilterSort, ) -> MyResult<Either<DynLayoutPage<'a>, Json<Node>>> { - 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<Node>, 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<Node>, 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<Node>, 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/<id>/player?<conf..>", rank = 4)] pub fn r_player<'a>( sess: Session, - db: &'a State<DataAcid>, + db: &'a State<Database>, id: &'a str, conf: PlayerConfig, ) -> MyResult<Either<DynLayoutPage<'a>, 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<DynRender<'a>> { +pub fn player_conf<'a>(item: Arc<Node>, playing: bool) -> anyhow::Result<DynRender<'a>> { 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?<query>&<page>")] pub async fn r_search<'a>( session: Session, - db: &State<DataAcid>, + db: &State<Database>, query: Option<&str>, page: Option<usize>, ) -> MyResult<DynLayoutPage<'a>> { 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::<Result<Vec<_>, 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) { diff --git a/server/src/routes/userdata.rs b/server/src/routes/userdata.rs index cf6b0af..6fcd7a0 100644 --- a/server/src/routes/userdata.rs +++ b/server/src/routes/userdata.rs @@ -3,14 +3,13 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::ui::{account::session::Session, error::MyResult, node::DatabaseNodeUserDataExt}; +use super::ui::{account::session::Session, error::MyResult}; use crate::routes::ui::node::rocket_uri_macro_r_library_node; -use anyhow::anyhow; -use jellybase::{ - database::{redb::ReadableTable, DataAcid, Ser, TableExt, T_NODE, T_USER_NODE}, - permission::NodePermissionExt, +use jellybase::database::Database; +use jellycommon::{ + user::{NodeUserData, WatchedState}, + NodeID, }; -use jellycommon::user::{NodeUserData, WatchedState}; use rocket::{ form::Form, get, post, response::Redirect, serde::json::Json, FromForm, FromFormField, State, UriDisplayQuery, @@ -26,43 +25,31 @@ pub enum UrlWatchedState { #[get("/n/<id>/userdata")] pub fn r_node_userdata( session: Session, - db: &State<DataAcid>, + db: &State<Database>, id: &str, ) -> MyResult<Json<NodeUserData>> { - let (_, _, u) = db.get_node_with_userdata(id, &session)?; + let u = db + .get_node_udata(NodeID::from_slug(id), &session.user.name)? + .unwrap_or_default(); Ok(Json(u)) } #[post("/n/<id>/watched?<state>")] pub async fn r_node_userdata_watched( session: Session, - db: &State<DataAcid>, + db: &State<Database>, id: &str, state: UrlWatchedState, ) -> MyResult<Redirect> { - T_NODE - .get(db, id)? - .only_if_permitted(&session.user.permissions) - .ok_or(anyhow!("node does not exist"))?; - - let txn = db.begin_write()?; - let mut user_nodes = txn.open_table(T_USER_NODE)?; - - let mut udata = user_nodes - .get((session.user.name.as_str(), id))? - .map(|x| x.value().0) - .unwrap_or_default(); - - udata.watched = match state { - UrlWatchedState::None => WatchedState::None, - UrlWatchedState::Watched => WatchedState::Watched, - UrlWatchedState::Pending => WatchedState::Pending, - }; - - user_nodes.insert((session.user.name.as_str(), id), Ser(udata))?; - drop(user_nodes); - txn.commit()?; - + // TODO perm + db.update_node_udata(NodeID::from_slug(id), &session.user.name, |udata| { + udata.watched = match state { + UrlWatchedState::None => WatchedState::None, + UrlWatchedState::Watched => WatchedState::Watched, + UrlWatchedState::Pending => WatchedState::Pending, + }; + Ok(()) + })?; Ok(Redirect::found(rocket::uri!(r_library_node(id)))) } @@ -75,62 +62,34 @@ pub struct UpdateRating { #[post("/n/<id>/update_rating", data = "<form>")] pub async fn r_node_userdata_rating( session: Session, - db: &State<DataAcid>, + db: &State<Database>, id: &str, form: Form<UpdateRating>, ) -> MyResult<Redirect> { - T_NODE - .get(db, id)? - .only_if_permitted(&session.user.permissions) - .ok_or(anyhow!("node does not exist"))?; - - let txn = db.begin_write()?; - let mut user_nodes = txn.open_table(T_USER_NODE)?; - - let mut udata = user_nodes - .get((session.user.name.as_str(), id))? - .map(|x| x.value().0) - .unwrap_or_default(); - - udata.rating = form.rating; - - user_nodes.insert((session.user.name.as_str(), id), Ser(udata))?; - drop(user_nodes); - txn.commit()?; - + // TODO perm + db.update_node_udata(NodeID::from_slug(id), &session.user.name, |udata| { + udata.rating = form.rating; + Ok(()) + })?; Ok(Redirect::found(rocket::uri!(r_library_node(id)))) } #[post("/n/<id>/progress?<t>")] pub async fn r_node_userdata_progress( session: Session, - db: &State<DataAcid>, + db: &State<Database>, id: &str, t: f64, ) -> MyResult<()> { - T_NODE - .get(db, id)? - .only_if_permitted(&session.user.permissions) - .ok_or(anyhow!("node does not exist"))?; - - let txn = db.begin_write()?; - let mut user_nodes = txn.open_table(T_USER_NODE)?; - - let mut udata = user_nodes - .get((session.user.name.as_str(), id))? - .map(|x| x.value().0) - .unwrap_or_default(); - - udata.watched = match udata.watched { - WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => { - WatchedState::Progress(t) - } - WatchedState::Watched => WatchedState::Watched, - }; - - user_nodes.insert((session.user.name.as_str(), id), Ser(udata))?; - drop(user_nodes); - txn.commit()?; - + // TODO perm + db.update_node_udata(NodeID::from_slug(id), &session.user.name, |udata| { + udata.watched = match udata.watched { + WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => { + WatchedState::Progress(t) + } + WatchedState::Watched => WatchedState::Watched, + }; + Ok(()) + })?; Ok(()) } |