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 --- Cargo.lock | 26 ++ base/src/database.rs | 318 ++++++++++++++++++++----- base/src/permission.rs | 11 +- common/Cargo.toml | 1 + common/src/impl.rs | 22 +- common/src/lib.rs | 7 +- import/src/lib.rs | 1 + import/src/tmdb.rs | 2 +- server/src/main.rs | 33 +-- server/src/routes/api/mod.rs | 25 +- server/src/routes/mod.rs | 7 +- server/src/routes/stream.rs | 20 +- 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 ++- server/src/routes/userdata.rs | 113 +++------ stream/src/fragment.rs | 3 +- stream/src/hls.rs | 6 +- stream/src/jhls.rs | 3 +- stream/src/lib.rs | 6 +- stream/src/webvtt.rs | 5 +- tool/src/migrate.rs | 329 +++++++++++++------------- 32 files changed, 801 insertions(+), 883 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9fb643c..b89b14a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,6 +194,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -382,6 +388,19 @@ dependencies = [ "digest", ] +[[package]] +name = "blake3" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -584,6 +603,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "cookie" version = "0.18.1" @@ -1704,6 +1729,7 @@ name = "jellycommon" version = "0.1.0" dependencies = [ "bincode", + "blake3", "chrono", "rocket", "serde", diff --git a/base/src/database.rs b/base/src/database.rs index 0195209..0f18097 100644 --- a/base/src/database.rs +++ b/base/src/database.rs @@ -3,44 +3,40 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -use anyhow::Context; +use anyhow::{anyhow, bail, Context, Result}; use bincode::{Decode, Encode}; use jellycommon::{ user::{NodeUserData, User}, - Node, + Node, NodeID, }; use log::info; -use redb::{Database, TableDefinition}; -use serde::{Deserialize, Serialize}; +use redb::{ReadableTable, TableDefinition}; use std::{ - borrow::Borrow, fs::create_dir_all, - ops::Deref, path::Path, sync::{Arc, RwLock}, }; use tantivy::{ + collector::{Count, TopDocs}, directory::MmapDirectory, - schema::{Field, Schema, FAST, INDEXED, STORED, STRING, TEXT}, - DateOptions, Index, IndexReader, IndexWriter, ReloadPolicy, + query::QueryParser, + schema::{Field, Schema, Value, FAST, INDEXED, STORED, STRING, TEXT}, + DateOptions, Index, IndexReader, IndexWriter, ReloadPolicy, TantivyDocument, }; -pub use redb; -pub use tantivy; - -pub const T_USER: TableDefinition<&str, Ser> = TableDefinition::new("user"); -pub const T_USER_NODE: TableDefinition<(&str, &str), Ser> = +const T_USER: TableDefinition<&str, Ser> = TableDefinition::new("user"); +const T_USER_NODE: TableDefinition<(&str, [u8; 32]), Ser> = TableDefinition::new("user_node"); -pub const T_INVITE: TableDefinition<&str, Ser<()>> = TableDefinition::new("invite"); -pub const T_NODE: TableDefinition<&str, Ser> = TableDefinition::new("node"); +const T_INVITE: TableDefinition<&str, ()> = TableDefinition::new("invite"); +const T_NODE: TableDefinition<[u8; 32], Ser> = TableDefinition::new("node"); #[derive(Clone)] -pub struct DataAcid { - pub inner: Arc, - pub node_index: Arc, +pub struct Database { + inner: Arc, + node_index: Arc, } -impl DataAcid { +impl Database { pub fn open(path: &Path) -> Result { create_dir_all(path).context("creating database directory")?; info!("opening kv store..."); @@ -54,7 +50,7 @@ impl DataAcid { { // this creates all tables such that read operations on them do not fail. - let txn = r.begin_write()?; + let txn = r.inner.begin_write()?; drop(txn.open_table(T_INVITE)?); drop(txn.open_table(T_USER)?); drop(txn.open_table(T_USER_NODE)?); @@ -65,12 +61,212 @@ impl DataAcid { info!("ready"); Ok(r) } -} + pub fn get_node_slug(&self, slug: &str) -> Result>> { + self.get_node(NodeID::from_slug(slug)) + } + pub fn get_node(&self, id: NodeID) -> Result>> { + let txn = self.inner.begin_read()?; + let t_node = txn.open_table(T_NODE)?; + if let Some(node) = t_node.get(id.0)? { + Ok(Some(node.value().0.into())) + } else { + Ok(None) + } + } + pub fn get_node_udata(&self, id: NodeID, username: &str) -> Result> { + let txn = self.inner.begin_read()?; + let t_node = txn.open_table(T_USER_NODE)?; + if let Some(node) = t_node.get((username, id.0))? { + Ok(Some(node.value().0.into())) + } else { + Ok(None) + } + } + pub fn get_user(&self, username: &str) -> Result> { + let txn = self.inner.begin_read()?; + let t_user = txn.open_table(T_USER)?; + if let Some(user) = t_user.get(username)? { + Ok(Some(user.value().0)) + } else { + Ok(None) + } + } + pub fn update_user( + &self, + username: &str, + update: impl FnOnce(&mut User) -> Result<()>, + ) -> Result<()> { + let txn = self.inner.begin_write()?; + let mut users = txn.open_table(T_USER)?; + let mut user = users + .get(username)? + .ok_or(anyhow!("user does not exist"))? + .value() + .0; + update(&mut user)?; + users.insert(&username, Ser(user))?; + drop(users); + txn.commit()?; + + Ok(()) + } + pub fn update_node_udata( + &self, + node: NodeID, + username: &str, + update: impl FnOnce(&mut NodeUserData) -> Result<()>, + ) -> Result<()> { + let txn = self.inner.begin_write()?; + let mut user_nodes = txn.open_table(T_USER_NODE)?; + + let mut udata = user_nodes + .get((username, node.0))? + .map(|x| x.value().0) + .unwrap_or_default(); -impl Deref for DataAcid { - type Target = Database; - fn deref(&self) -> &Self::Target { - &self.inner + update(&mut udata)?; + + user_nodes.insert((username, node.0), Ser(udata))?; + drop(user_nodes); + txn.commit()?; + Ok(()) + } + pub fn delete_user(&self, username: &str) -> Result { + let txn = self.inner.begin_write()?; + let mut table = txn.open_table(T_USER)?; + let r = table.remove(username)?.is_some(); + drop(table); + txn.commit()?; + Ok(r) + } + pub fn list_users(&self) -> Result> { + let txn = self.inner.begin_read()?; + let table = txn.open_table(T_USER)?; + let i = table + .iter()? + .map(|a| { + let (_, y) = a.unwrap(); // TODO + y.value().0 + }) + .collect::>(); + drop(table); + Ok(i) + } + pub fn list_invites(&self) -> Result> { + let txn = self.inner.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); + Ok(i) + } + pub fn create_invite(&self, inv: &str) -> Result<()> { + let txn = self.inner.begin_write()?; + let mut table = txn.open_table(T_INVITE)?; + table.insert(inv, ())?; + drop(table); + txn.commit()?; + Ok(()) + } + pub fn delete_invite(&self, inv: &str) -> Result { + let txn = self.inner.begin_write()?; + let mut table = txn.open_table(T_INVITE)?; + let r = table.remove(inv)?.is_some(); + drop(table); + txn.commit()?; + Ok(r) + } + pub fn register_user(&self, invite: &str, username: &str, user: User) -> Result<()> { + let txn = self.inner.begin_write()?; + let mut invites = txn.open_table(T_INVITE)?; + let mut users = txn.open_table(T_USER)?; + + if invites.remove(invite)?.is_none() { + bail!("invitation invalid"); + } + let prev_user = users.insert(username, Ser(user))?.map(|x| x.value().0); + if prev_user.is_some() { + bail!("username taken"); + } + + drop(users); + drop(invites); + txn.commit()?; + Ok(()) + } + pub fn list_nodes_with_udata(&self, username: &str) -> Result> { + let txn = self.inner.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(&(username, x)) + .unwrap() + .map(|z| z.value().0) + .unwrap_or_default(); + (y, z) + }) + .collect::>(); + drop(nodes); + Ok(i) + } + pub fn search(&self, query: &str, page: usize) -> Result<(usize, Vec)> { + let query = QueryParser::for_index( + &self.node_index.index, + vec![self.node_index.title, self.node_index.description], + ) + .parse_query(query) + .context("parsing query")?; + + let searcher = self.node_index.reader.searcher(); + let sres = searcher.search(&query, &TopDocs::with_limit(32).and_offset(page * 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(self.node_index.id) + .unwrap() + .as_bytes() + .unwrap(); + let id = NodeID(id.try_into().unwrap()); + results.push(id); + } + Ok((scount, results)) + } + pub fn create_admin_user(&self, username: &str, password_hash: Vec) -> Result<()> { + let txn = self.inner.begin_write().unwrap(); + let mut users = txn.open_table(T_USER).unwrap(); + + let admin = users.get(username).unwrap().map(|x| x.value().0); + users + .insert( + username, + Ser(User { + admin: true, + name: username.to_owned(), + password: password_hash, + ..admin.unwrap_or_else(|| User { + display_name: "Admin".to_string(), + ..Default::default() + }) + }), + ) + .unwrap(); + + drop(users); + txn.commit().unwrap(); + Ok(()) } } @@ -126,42 +322,42 @@ impl NodeIndex { } } -pub trait TableExt { - fn get(self, db: &DataAcid, key: KeyRef) -> anyhow::Result>; - fn insert(self, db: &DataAcid, key: KeyRef, value: Value) -> anyhow::Result<()>; - fn remove(self, db: &DataAcid, key: KeyRef) -> anyhow::Result>; -} -impl<'a, 'b, 'c, Key, Value, KeyRef> TableExt - for redb::TableDefinition<'a, Key, Ser> -where - Key: Borrow<::SelfType<'b>> + redb::Key, - Value: Encode + Decode + std::fmt::Debug + Serialize + for<'x> Deserialize<'x>, - KeyRef: Borrow<::SelfType<'c>>, -{ - fn get(self, db: &DataAcid, key: KeyRef) -> anyhow::Result> { - let txn = db.inner.begin_read()?; - let table = txn.open_table(self)?; - let user = table.get(key)?.map(|v| v.value().0); - drop(table); - Ok(user) - } - fn insert(self, db: &DataAcid, key: KeyRef, value: Value) -> anyhow::Result<()> { - let txn = db.inner.begin_write()?; - let mut table = txn.open_table(self)?; - table.insert(key, Ser(value))?; - drop(table); - txn.commit()?; - Ok(()) - } - fn remove(self, db: &DataAcid, key: KeyRef) -> anyhow::Result> { - let txn = db.inner.begin_write()?; - let mut table = txn.open_table(self)?; - let prev = table.remove(key)?.map(|v| v.value().0); - drop(table); - txn.commit()?; - Ok(prev) - } -} +// pub trait TableExt { +// fn get(self, db: &Database, key: KeyRef) -> anyhow::Result>; +// fn insert(self, db: &Database, key: KeyRef, value: Value) -> anyhow::Result<()>; +// fn remove(self, db: &Database, key: KeyRef) -> anyhow::Result>; +// } +// impl<'a, 'b, 'c, Key, Value, KeyRef> TableExt +// for redb::TableDefinition<'a, Key, Ser> +// where +// Key: Borrow<::SelfType<'b>> + redb::Key, +// Value: Encode + Decode + std::fmt::Debug + Serialize + for<'x> Deserialize<'x>, +// KeyRef: Borrow<::SelfType<'c>>, +// { +// fn get(self, db: &Database, key: KeyRef) -> anyhow::Result> { +// let txn = db.inner.begin_read()?; +// let table = txn.open_table(self)?; +// let user = table.get(key)?.map(|v| v.value().0); +// drop(table); +// Ok(user) +// } +// fn insert(self, db: &Database, key: KeyRef, value: Value) -> anyhow::Result<()> { +// let txn = db.inner.begin_write()?; +// let mut table = txn.open_table(self)?; +// table.insert(key, Ser(value))?; +// drop(table); +// txn.commit()?; +// Ok(()) +// } +// fn remove(self, db: &Database, key: KeyRef) -> anyhow::Result> { +// let txn = db.inner.begin_write()?; +// let mut table = txn.open_table(self)?; +// let prev = table.remove(key)?.map(|v| v.value().0); +// drop(table); +// txn.commit()?; +// Ok(prev) +// } +// } // pub trait TableIterExt< // 'a, diff --git a/base/src/permission.rs b/base/src/permission.rs index 9dc8ffe..cec3833 100644 --- a/base/src/permission.rs +++ b/base/src/permission.rs @@ -54,11 +54,12 @@ fn check_node_permission(perms: &PermissionSet, node: &Node) -> bool { if let Some(v) = perms.check_explicit(&UserPermission::AccessNode(node.id.clone().unwrap())) { v } else { - for com in node.parents.clone().into_iter() { - if let Some(v) = perms.check_explicit(&UserPermission::AccessNode(com.to_owned())) { - return v; - } - } + // TODO + // for com in node.parents.clone().into_iter() { + // if let Some(v) = perms.check_explicit(&UserPermission::AccessNode(com.to_owned())) { + // return v; + // } + // } true } } diff --git a/common/Cargo.toml b/common/Cargo.toml index 13fe29b..066e79c 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -8,6 +8,7 @@ serde = { version = "1.0.217", features = ["derive"] } bincode = { version = "2.0.0-rc.3", features = ["derive"] } rocket = { workspace = true, optional = true } chrono = { version = "0.4.39", features = ["serde"] } +blake3 = "1.5.5" [features] rocket = ["dep:rocket"] diff --git a/common/src/impl.rs b/common/src/impl.rs index a35216b..a98015a 100644 --- a/common/src/impl.rs +++ b/common/src/impl.rs @@ -3,7 +3,9 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -use crate::{ObjectIds, PeopleGroup, SourceTrack, SourceTrackKind, TmdbKind, TraktKind}; +use crate::{ + Node, NodeID, ObjectIds, PeopleGroup, SourceTrack, SourceTrackKind, TmdbKind, TraktKind, +}; use std::{fmt::Display, str::FromStr}; impl SourceTrackKind { @@ -146,3 +148,21 @@ impl FromStr for PeopleGroup { }) } } + +impl NodeID { + pub fn from_slug(slug: &str) -> Self { + let mut h = blake3::Hasher::new(); + h.update(slug.as_bytes()); + Self(*h.finalize().as_bytes()) + } + #[inline] + pub fn from_node(node: &Node) -> Self { + Self::from_slug(&node.slug) + } +} +impl Node { + #[inline] + pub fn id(&self) -> NodeID { + NodeID::from_node(self) + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index a72f345..05d1573 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -17,10 +17,15 @@ use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, path::PathBuf}; +#[derive( + Clone, Copy, Debug, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, +)] +pub struct NodeID(pub [u8; 32]); + #[derive(Debug, Clone, Deserialize, Serialize, Default, Encode, Decode)] pub struct Node { pub slug: String, - pub parents: Vec, + pub parents: Vec, pub kind: Option, pub poster: Option, pub backdrop: Option, diff --git a/import/src/lib.rs b/import/src/lib.rs index c3daf7b..787c0cf 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -7,6 +7,7 @@ pub mod infojson; pub mod tmdb; pub mod trakt; + // use anyhow::{anyhow, bail, Context, Error, Ok}; // use async_recursion::async_recursion; // use base64::Engine; diff --git a/import/src/tmdb.rs b/import/src/tmdb.rs index 787ba5b..522d9d6 100644 --- a/import/src/tmdb.rs +++ b/import/src/tmdb.rs @@ -50,7 +50,7 @@ impl Tmdb { .client .get(&format!( "https://api.themoviedb.org/3/search/{kind}?query={}?api_key={}", - query.replace(" ", "+"), // TODO this is horrible, please find a proper urlencoding library that supports + for space + query.replace(" ", "+"), self.key )) .send() 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 = "")] pub fn r_api_account_login( - database: &State, + database: &State, data: Json, ) -> MyResult { let token = login_logic( @@ -52,19 +48,6 @@ pub fn r_api_account_login( Ok(json!(token)) } -#[get("/api/node_raw/")] -pub fn r_api_node_raw( - admin: AdminSession, - database: &State, - id: &str, -) -> MyResult> { - 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/")] pub fn r_api_asset_token_raw(admin: AdminSession, token: &str) -> MyResult> { 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 */ 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 { +pub fn build_rocket(database: Database, federation: Federation) -> Rocket { rocket::build() .configure(Config { address: std::env::var("BIND_ADDR") @@ -138,7 +138,6 @@ pub fn build_rocket(database: DataAcid, federation: Federation) -> Rocket 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 */ 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, - db: &State, + db: &State, id: &str, range: Option, spec: StreamSpec, ) -> Result, 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 = "
")] 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) { 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 */ -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//userdata")] pub fn r_node_userdata( session: Session, - db: &State, + db: &State, id: &str, ) -> MyResult> { - 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//watched?")] pub async fn r_node_userdata_watched( session: Session, - db: &State, + db: &State, id: &str, state: UrlWatchedState, ) -> 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 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//update_rating", data = "")] pub async fn r_node_userdata_rating( session: Session, - db: &State, + db: &State, id: &str, form: Form, ) -> 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.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//progress?")] pub async fn r_node_userdata_progress( session: Session, - db: &State, + db: &State, 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(()) } diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs index 17e7633..2dbc716 100644 --- a/stream/src/fragment.rs +++ b/stream/src/fragment.rs @@ -12,11 +12,12 @@ use jellycommon::{ }; use jellytranscoder::fragment::transcode; use log::warn; +use std::sync::Arc; use tokio::{fs::File, io::DuplexStream}; use tokio_util::io::SyncIoBridge; pub async fn fragment_stream( - node: Node, + node: Arc, local_tracks: Vec, spec: StreamSpec, mut b: DuplexStream, diff --git a/stream/src/hls.rs b/stream/src/hls.rs index 2203ee8..c09b77f 100644 --- a/stream/src/hls.rs +++ b/stream/src/hls.rs @@ -10,14 +10,14 @@ use jellycommon::{ stream::{StreamFormat, StreamSpec}, LocalTrack, Node, SourceTrackKind, }; -use std::{fmt::Write, ops::Range}; +use std::{fmt::Write, ops::Range, sync::Arc}; use tokio::{ io::{AsyncWriteExt, DuplexStream}, task::spawn_blocking, }; pub async fn hls_master_stream( - node: Node, + node: Arc, _local_tracks: Vec, _spec: StreamSpec, mut b: DuplexStream, @@ -50,7 +50,7 @@ pub async fn hls_master_stream( } pub async fn hls_variant_stream( - node: Node, + node: Arc, local_tracks: Vec, mut spec: StreamSpec, mut b: DuplexStream, diff --git a/stream/src/jhls.rs b/stream/src/jhls.rs index b0de837..28d383f 100644 --- a/stream/src/jhls.rs +++ b/stream/src/jhls.rs @@ -11,10 +11,11 @@ use jellycommon::{ user::{PermissionSet, UserPermission}, LocalTrack, Node, }; +use std::sync::Arc; use tokio::io::{AsyncWriteExt, DuplexStream}; pub async fn jhls_index( - node: Node, + node: Arc, local_tracks: &[LocalTrack], spec: StreamSpec, mut b: DuplexStream, diff --git a/stream/src/lib.rs b/stream/src/lib.rs index 14d3a4c..2055440 100644 --- a/stream/src/lib.rs +++ b/stream/src/lib.rs @@ -19,7 +19,7 @@ use jellycommon::{ LocalTrack, Node, TrackSource, }; use jhls::jhls_index; -use std::{io::SeekFrom, ops::Range}; +use std::{io::SeekFrom, ops::Range, sync::Arc}; use tokio::{ fs::File, io::{duplex, AsyncReadExt, AsyncSeekExt, AsyncWriteExt, DuplexStream}, @@ -47,7 +47,7 @@ pub fn stream_head(spec: &StreamSpec) -> StreamHead { } pub async fn stream( - node: Node, + node: Arc, spec: StreamSpec, range: Range, perms: &PermissionSet, @@ -94,7 +94,7 @@ pub async fn stream( } async fn remux_stream( - node: Node, + node: Arc, local_tracks: Vec, spec: StreamSpec, range: Range, diff --git a/stream/src/webvtt.rs b/stream/src/webvtt.rs index 4065e1b..02a4181 100644 --- a/stream/src/webvtt.rs +++ b/stream/src/webvtt.rs @@ -8,11 +8,12 @@ use jellybase::{cache::async_cache_memory, CONF}; use jellycommon::{stream::StreamSpec, LocalTrack, Node}; use jellyremuxer::extract::extract_track; use jellytranscoder::subtitles::{parse_subtitles, write_webvtt}; +use std::sync::Arc; use tokio::io::{AsyncWriteExt, DuplexStream}; pub async fn vtt_stream( json: bool, - node: Node, + node: Arc, local_tracks: Vec, spec: StreamSpec, mut b: DuplexStream, @@ -23,7 +24,7 @@ pub async fn vtt_stream( let tracki = *spec.track.first().ok_or(anyhow!("no track selected"))?; let local_track = local_tracks.first().ok_or(anyhow!("no tracks"))?.clone(); - let track = &node.media.unwrap().tracks[tracki]; + let track = &node.media.as_ref().unwrap().tracks[tracki]; let cp = local_track.codec_private.clone(); let subtitles = async_cache_memory( diff --git a/tool/src/migrate.rs b/tool/src/migrate.rs index ed70104..bb826b2 100644 --- a/tool/src/migrate.rs +++ b/tool/src/migrate.rs @@ -3,20 +3,6 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -use crate::cli::{Action, MigrateMode}; -use anyhow::{bail, Context}; -use indicatif::ProgressIterator; -use jellybase::database::redb::ReadableTableMetadata; -use jellybase::database::{redb::ReadableTable, DataAcid, Ser, T_INVITE, T_USER, T_USER_NODE}; -use jellycommon::user::{NodeUserData, User}; -use log::{info, warn}; -use std::io::{BufRead, BufReader}; -use std::{ - fs::File, - io::{BufWriter, Write}, - path::Path, -}; - // macro_rules! process_tree { // ($mode:ident, $save_location:ident, $da:ident, $name:literal, $table:ident, $dt:tt) => {{ // let path = $save_location.join($name); @@ -90,172 +76,175 @@ use std::{ // } // } -pub fn migrate(action: Action) -> anyhow::Result<()> { - match action { - Action::Migrate { - mode, - save_location, - database, - } => { - std::fs::create_dir_all(&save_location)?; +use crate::cli::Action; - let da = DataAcid::open(&database)?; +pub fn migrate(_action: Action) -> anyhow::Result<()> { + // TODO still needed? + // match action { + // Action::Migrate { + // mode, + // save_location, + // database, + // } => { + // std::fs::create_dir_all(&save_location)?; - info!("processing 'user'"); - { - let path: &Path = &save_location.join("user"); - let da = &da; - match mode { - MigrateMode::Export => { - let mut o = BufWriter::new(File::create(path)?); - let txn = da.begin_read()?; - let table = txn.open_table(T_USER)?; + // let da = Database::open(&database)?; - let len = table.len()?; - for r in table.iter()?.progress_count(len) { - let (k, v) = r?; - serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; - writeln!(&mut o)?; - } - drop(table); - } - MigrateMode::Import => { - { - let txn = da.begin_read()?; - let table = txn.open_table(T_USER)?; - if !table.is_empty()? { - bail!("tree not empty, `rm -rf` your db please :)") - } - } + // info!("processing 'user'"); + // { + // let path: &Path = &save_location.join("user"); + // let da = &da; + // match mode { + // MigrateMode::Export => { + // let mut o = BufWriter::new(File::create(path)?); + // let txn = da.begin_read()?; + // let table = txn.open_table(T_USER)?; - let Ok(i) = File::open(path) else { - warn!( - "{path:?} does not exist; the import of that tree will be skipped." - ); - return Ok(()); - }; - let i = BufReader::new(i); - for l in i.lines() { - let l = l?; - let (k, v) = serde_json::from_str::<(String, User)>(l.as_str()) - .context("reading db dump item")?; - { - let txn = da.begin_write()?; - let mut table = txn.open_table(T_USER)?; - table.insert(k.as_str(), Ser(v))?; - drop(table); - txn.commit()? - } - } - } - } - }; - info!("processing 'user_node'"); - { - let path: &Path = &save_location.join("user_node"); - let da = &da; - match mode { - MigrateMode::Export => { - let mut o = BufWriter::new(File::create(path)?); - let txn = da.begin_read()?; - let table = txn.open_table(T_USER_NODE)?; + // let len = table.len()?; + // for r in table.iter()?.progress_count(len) { + // let (k, v) = r?; + // serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; + // writeln!(&mut o)?; + // } + // drop(table); + // } + // MigrateMode::Import => { + // { + // let txn = da.begin_read()?; + // let table = txn.open_table(T_USER)?; + // if !table.is_empty()? { + // bail!("tree not empty, `rm -rf` your db please :)") + // } + // } - let len = table.len()?; - for r in table.iter()?.progress_count(len) { - let (k, v) = r?; - serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; - writeln!(&mut o)?; - } - drop(table); - } - MigrateMode::Import => { - { - let txn = da.begin_read()?; - let table = txn.open_table(T_USER_NODE)?; - if !table.is_empty()? { - bail!("tree not empty, `rm -rf` your db please :)") - } - } + // let Ok(i) = File::open(path) else { + // warn!( + // "{path:?} does not exist; the import of that tree will be skipped." + // ); + // return Ok(()); + // }; + // let i = BufReader::new(i); + // for l in i.lines() { + // let l = l?; + // let (k, v) = serde_json::from_str::<(String, User)>(l.as_str()) + // .context("reading db dump item")?; + // { + // let txn = da.begin_write()?; + // let mut table = txn.open_table(T_USER)?; + // table.insert(k.as_str(), Ser(v))?; + // drop(table); + // txn.commit()? + // } + // } + // } + // } + // }; + // info!("processing 'user_node'"); + // { + // let path: &Path = &save_location.join("user_node"); + // let da = &da; + // match mode { + // MigrateMode::Export => { + // let mut o = BufWriter::new(File::create(path)?); + // let txn = da.begin_read()?; + // let table = txn.open_table(T_USER_NODE)?; - let Ok(i) = File::open(path) else { - warn!( - "{path:?} does not exist; the import of that tree will be skipped." - ); - return Ok(()); - }; - let i = BufReader::new(i); - for l in i.lines() { - let l = l?; - let (k, v) = serde_json::from_str::<((String, String), NodeUserData)>( - l.as_str(), - ) - .context("reading db dump item")?; - { - let txn = da.begin_write()?; - let mut table = txn.open_table(T_USER_NODE)?; + // let len = table.len()?; + // for r in table.iter()?.progress_count(len) { + // let (k, v) = r?; + // serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; + // writeln!(&mut o)?; + // } + // drop(table); + // } + // MigrateMode::Import => { + // { + // let txn = da.begin_read()?; + // let table = txn.open_table(T_USER_NODE)?; + // if !table.is_empty()? { + // bail!("tree not empty, `rm -rf` your db please :)") + // } + // } - table.insert((k.0.as_str(), k.1.as_str()), Ser(v))?; - drop(table); - txn.commit()? - } - } - } - } - }; - info!("processing 'invite'"); - { - let path: &Path = &save_location.join("invite"); - let da = &da; - match mode { - MigrateMode::Export => { - let mut o = BufWriter::new(File::create(path)?); - let txn = da.begin_read()?; - let table = txn.open_table(T_INVITE)?; + // let Ok(i) = File::open(path) else { + // warn!( + // "{path:?} does not exist; the import of that tree will be skipped." + // ); + // return Ok(()); + // }; + // let i = BufReader::new(i); + // for l in i.lines() { + // let l = l?; + // let (k, v) = serde_json::from_str::<((String, String), NodeUserData)>( + // l.as_str(), + // ) + // .context("reading db dump item")?; + // { + // let txn = da.begin_write()?; + // let mut table = txn.open_table(T_USER_NODE)?; - let len = table.len()?; - for r in table.iter()?.progress_count(len) { - let (k, v) = r?; - serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; - writeln!(&mut o)?; - } - drop(table); - } - MigrateMode::Import => { - { - let txn = da.begin_read()?; - let table = txn.open_table(T_INVITE)?; - if !table.is_empty()? { - bail!("tree not empty, `rm -rf` your db please :)") - } - } + // table.insert((k.0.as_str(), k.1.as_str()), Ser(v))?; + // drop(table); + // txn.commit()? + // } + // } + // } + // } + // }; + // info!("processing 'invite'"); + // { + // let path: &Path = &save_location.join("invite"); + // let da = &da; + // match mode { + // MigrateMode::Export => { + // let mut o = BufWriter::new(File::create(path)?); + // let txn = da.begin_read()?; + // let table = txn.open_table(T_INVITE)?; - let Ok(i) = File::open(path) else { - warn!( - "{path:?} does not exist; the import of that tree will be skipped." - ); - return Ok(()); - }; - let i = BufReader::new(i); - for l in i.lines() { - let l = l?; - let (k, _v) = serde_json::from_str::<(String, ())>(l.as_str()) - .context("reading db dump item")?; - { - let txn = da.begin_write()?; - let mut table = txn.open_table(T_INVITE)?; + // let len = table.len()?; + // for r in table.iter()?.progress_count(len) { + // let (k, v) = r?; + // serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; + // writeln!(&mut o)?; + // } + // drop(table); + // } + // MigrateMode::Import => { + // { + // let txn = da.begin_read()?; + // let table = txn.open_table(T_INVITE)?; + // if !table.is_empty()? { + // bail!("tree not empty, `rm -rf` your db please :)") + // } + // } - table.insert(k.as_str(), Ser(()))?; - drop(table); - txn.commit()? - } - } - } - } - }; - info!("done"); - } - _ => unreachable!(), - } + // let Ok(i) = File::open(path) else { + // warn!( + // "{path:?} does not exist; the import of that tree will be skipped." + // ); + // return Ok(()); + // }; + // let i = BufReader::new(i); + // for l in i.lines() { + // let l = l?; + // let (k, _v) = serde_json::from_str::<(String, ())>(l.as_str()) + // .context("reading db dump item")?; + // { + // let txn = da.begin_write()?; + // let mut table = txn.open_table(T_INVITE)?; + + // table.insert(k.as_str(), Ser(()))?; + // drop(table); + // txn.commit()? + // } + // } + // } + // } + // }; + // info!("done"); + // } + // _ => unreachable!(), + // } Ok(()) } /* -- cgit v1.2.3-70-g09d2