diff options
author | metamuffin <metamuffin@disroot.org> | 2025-05-26 18:24:16 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-05-26 18:24:16 +0200 |
commit | 3b15caade07e8fbe351fed9aceb3f435bf58368e (patch) | |
tree | cce91c229b78061ad36f29d76a76d67c3c737c59 | |
parent | 1eeff5c03e8985d16d4f2b6283741dd82b369bd3 (diff) | |
download | jellything-3b15caade07e8fbe351fed9aceb3f435bf58368e.tar jellything-3b15caade07e8fbe351fed9aceb3f435bf58368e.tar.bz2 jellything-3b15caade07e8fbe351fed9aceb3f435bf58368e.tar.zst |
move all direct database access to logic crate
41 files changed, 728 insertions, 665 deletions
@@ -1851,6 +1851,7 @@ dependencies = [ "jellydb", "jellyimport", "jellyimport-asset-token", + "jellytranscoder", "log", "rand 0.9.1", "serde", diff --git a/common/src/config.rs b/common/src/config.rs index 9368247..016bdeb 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -49,7 +49,6 @@ pub struct FederationAccount { pub tls: bool, } - fn login_expire() -> i64 { 60 * 60 * 24 } diff --git a/common/src/routes.rs b/common/src/routes.rs index 437f469..31e31d4 100644 --- a/common/src/routes.rs +++ b/common/src/routes.rs @@ -77,9 +77,6 @@ pub fn u_admin_invite_remove() -> String { pub fn u_admin_import(incremental: bool) -> String { format!("/admin/import?incremental={incremental}") } -pub fn u_admin_transcode_posters() -> String { - format!("/admin/transcode_posters") -} pub fn u_admin_update_search() -> String { format!("/admin/update_search") } diff --git a/logic/Cargo.toml b/logic/Cargo.toml index fd70b73..a60f90b 100644 --- a/logic/Cargo.toml +++ b/logic/Cargo.toml @@ -8,6 +8,7 @@ jellyimport-asset-token = { path = "../import/asset_token" } jellyimport = { path = "../import" } jellycommon = { path = "../common" } jellydb = { path = "../database" } +jellytranscoder = { path = "../transcoder" } log = "0.4.27" anyhow = "1.0.98" base64 = "0.22.1" diff --git a/logic/src/account.rs b/logic/src/account.rs new file mode 100644 index 0000000..a352437 --- /dev/null +++ b/logic/src/account.rs @@ -0,0 +1,60 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2025 metamuffin <metamuffin.org> +*/ + +use crate::{DATABASE, login::hash_password, session::Session}; +use anyhow::Result; +use jellycommon::user::{PlayerKind, Theme, User}; + +pub fn update_user_password(session: &Session, password: &str) -> Result<()> { + DATABASE.update_user(&session.user.name, |user| { + user.password = hash_password(&session.user.name, password); + Ok(()) + })?; + Ok(()) +} +pub fn update_user_display_name(session: &Session, display_name: &str) -> Result<()> { + DATABASE.update_user(&session.user.name, |user| { + user.display_name = display_name.to_owned(); + Ok(()) + })?; + Ok(()) +} +pub fn update_user_native_secret(session: &Session, native_secret: &str) -> Result<()> { + DATABASE.update_user(&session.user.name, |user| { + user.native_secret = native_secret.to_owned(); + Ok(()) + })?; + Ok(()) +} +pub fn update_user_theme(session: &Session, theme: Theme) -> Result<()> { + DATABASE.update_user(&session.user.name, |user| { + user.theme = theme; + Ok(()) + })?; + Ok(()) +} +pub fn update_user_player_preference( + session: &Session, + player_preference: PlayerKind, +) -> Result<()> { + DATABASE.update_user(&session.user.name, |user| { + user.player_preference = player_preference; + Ok(()) + })?; + Ok(()) +} +pub fn register_user(invitation: &str, username: &str, password: &str) -> Result<()> { + DATABASE.register_user( + &invitation, + &username, + User { + display_name: username.to_owned(), + name: username.to_owned(), + password: hash_password(&username, &password), + ..Default::default() + }, + ) +} diff --git a/logic/src/admin/mod.rs b/logic/src/admin/mod.rs index 2545ba4..804cb2b 100644 --- a/logic/src/admin/mod.rs +++ b/logic/src/admin/mod.rs @@ -7,14 +7,42 @@ pub mod log; pub mod user; -use crate::session::AdminSession; -use anyhow::Result; -use jellydb::Database; -use jellyimport::IMPORT_ERRORS; +use crate::{DATABASE, session::AdminSession}; +use anyhow::{Result, anyhow}; +use jellyimport::{IMPORT_ERRORS, import_wrap}; +use rand::Rng; +use std::time::{Duration, Instant}; +use tokio::task::spawn_blocking; pub async fn get_import_errors(_session: &AdminSession) -> Vec<String> { IMPORT_ERRORS.read().await.to_owned() } -pub fn list_invites(_session: &AdminSession, database: &Database) -> Result<Vec<String>> { - database.list_invites() +pub fn list_invites(_session: &AdminSession) -> Result<Vec<String>> { + DATABASE.list_invites() +} + +pub fn create_invite(_session: &AdminSession) -> Result<String> { + let i = format!("{}", rand::rng().random::<u128>()); + DATABASE.create_invite(&i)?; + Ok(i) +} +pub fn delete_invite(_session: &AdminSession, invite: &str) -> Result<()> { + if !DATABASE.delete_invite(invite)? { + Err(anyhow!("invite does not exist"))?; + }; + Ok(()) +} +pub async fn update_search_index(_session: &AdminSession) -> Result<()> { + spawn_blocking(move || DATABASE.search_create_index()).await? +} +pub async fn do_import( + _session: &AdminSession, + incremental: bool, +) -> Result<(Duration, Result<()>)> { + let t = Instant::now(); + if !incremental { + DATABASE.clear_nodes()?; + } + let r = import_wrap((*DATABASE).clone(), incremental).await; + Ok((t.elapsed(), r)) } diff --git a/logic/src/admin/user.rs b/logic/src/admin/user.rs index 3ec3852..e277077 100644 --- a/logic/src/admin/user.rs +++ b/logic/src/admin/user.rs @@ -4,14 +4,48 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::session::AdminSession; -use anyhow::Result; -use jellycommon::api::ApiAdminUsersResponse; -use jellydb::Database; +use crate::{DATABASE, session::AdminSession}; +use anyhow::{Result, anyhow}; +use jellycommon::{ + api::ApiAdminUsersResponse, + user::{User, UserPermission}, +}; -pub fn admin_users(db: &Database, _session: &AdminSession) -> Result<ApiAdminUsersResponse> { +pub fn admin_users(_session: &AdminSession) -> Result<ApiAdminUsersResponse> { // TODO dont return useless info like passwords Ok(ApiAdminUsersResponse { - users: db.list_users()?, + users: DATABASE.list_users()?, + }) +} +pub fn get_user(_session: &AdminSession, username: &str) -> Result<User> { + DATABASE + .get_user(username)? + .ok_or(anyhow!("user not found")) +} +pub fn delete_user(_session: &AdminSession, username: &str) -> Result<()> { + if !DATABASE.delete_user(&username)? { + Err(anyhow!("user did not exist"))?; + } + Ok(()) +} + +pub enum GrantState { + Grant, + Revoke, + Unset, +} +pub fn update_user_perms( + _session: &AdminSession, + username: &str, + perm: UserPermission, + action: GrantState, +) -> Result<()> { + DATABASE.update_user(username, |user| { + match 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(()) }) } diff --git a/logic/src/assets.rs b/logic/src/assets.rs new file mode 100644 index 0000000..7be3845 --- /dev/null +++ b/logic/src/assets.rs @@ -0,0 +1,131 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2025 metamuffin <metamuffin.org> +*/ + +use crate::{DATABASE, session::Session}; +use anyhow::{Result, anyhow}; +use jellycommon::{Asset, LocalTrack, NodeID, PeopleGroup, SourceTrackKind, TrackSource}; +use jellyimport_asset_token::AssetInner; + +pub fn get_node_backdrop(_session: &Session, id: NodeID) -> Result<Asset> { + // TODO perm + let node = DATABASE + .get_node(id)? + .ok_or(anyhow!("node does not exist"))?; + + let mut asset = node.backdrop.clone(); + if asset.is_none() { + if let Some(parent) = node.parents.last().copied() { + let parent = DATABASE + .get_node(parent)? + .ok_or(anyhow!("node does not exist"))?; + asset = parent.backdrop.clone(); + } + }; + Ok(asset.unwrap_or_else(|| { + AssetInner::Assets(format!("fallback-{:?}.avif", node.kind).into()).ser() + })) +} +pub fn get_node_poster(_session: &Session, id: NodeID) -> Result<Asset> { + // TODO perm + let node = DATABASE + .get_node(id)? + .ok_or(anyhow!("node does not exist"))?; + + let mut asset = node.poster.clone(); + if asset.is_none() { + if let Some(parent) = node.parents.last().copied() { + let parent = DATABASE + .get_node(parent)? + .ok_or(anyhow!("node does not exist"))?; + asset = parent.poster.clone(); + } + }; + Ok(asset.unwrap_or_else(|| { + AssetInner::Assets(format!("fallback-{:?}.avif", node.kind).into()).ser() + })) +} + +pub fn get_node_person_asset( + _session: &Session, + id: NodeID, + group: PeopleGroup, + index: usize, +) -> Result<Asset> { + // TODO perm + + let node = DATABASE + .get_node(id)? + .ok_or(anyhow!("node does not exist"))?; + let app = node + .people + .get(&group) + .ok_or(anyhow!("group has no members"))? + .get(index) + .ok_or(anyhow!("person does not exist"))?; + + let asset = app + .person + .headshot + .to_owned() + .unwrap_or(AssetInner::Assets("fallback-Person.avif".into()).ser()); + + Ok(asset) +} + +pub async fn get_node_thumbnail(_session: &Session, id: NodeID, t: f64) -> Result<Asset> { + let node = DATABASE + .get_node(id)? + .ok_or(anyhow!("node does not exist"))?; + + let media = node.media.as_ref().ok_or(anyhow!("no media"))?; + let (thumb_track_index, _thumb_track) = media + .tracks + .iter() + .enumerate() + .find(|(_i, t)| matches!(t.kind, SourceTrackKind::Video { .. })) + .ok_or(anyhow!("no video track to create a thumbnail of"))?; + let source = media + .tracks + .get(thumb_track_index) + .ok_or(anyhow!("no source"))?; + let thumb_track_source = source.source.clone(); + + if t < 0. || t > media.duration { + Err(anyhow!("thumbnail instant not within media duration"))? + } + + let step = 8.; + let t = (t / step).floor() * step; + + let asset = match thumb_track_source { + TrackSource::Local(a) => { + let AssetInner::LocalTrack(LocalTrack { path, .. }) = AssetInner::deser(&a.0)? else { + return Err(anyhow!("track set to wrong asset type").into()); + }; + // the track selected might be different from thumb_track + jellytranscoder::thumbnail::create_thumbnail(&path, t).await? + } + TrackSource::Remote(_) => { + // // TODO in the new system this is preferrably a property of node ext for regular fed + // let session = fed + // .get_session( + // thumb_track + // .federated + // .last() + // .ok_or(anyhow!("federation broken"))?, + // ) + // .await?; + + // async_cache_file("fed-thumb", (id.0, t as i64), |out| { + // session.node_thumbnail(out, id.0.into(), 2048, t) + // }) + // .await? + todo!() + } + }; + + Ok(AssetInner::Cache(asset).ser()) +} diff --git a/logic/src/home.rs b/logic/src/home.rs index ad3fee5..1957a94 100644 --- a/logic/src/home.rs +++ b/logic/src/home.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{node::DatabaseNodeUserDataExt, session::Session}; +use crate::{DATABASE, node::DatabaseNodeUserDataExt, session::Session}; use anyhow::{Context, Result}; use jellycommon::{ NodeID, NodeKind, Rating, Visibility, @@ -12,16 +12,15 @@ use jellycommon::{ chrono::{Datelike, Utc}, user::WatchedState, }; -use jellydb::Database; -pub fn home(db: &Database, session: &Session) -> Result<ApiHomeResponse> { - let mut items = db.list_nodes_with_udata(&session.user.name)?; +pub fn home(session: &Session) -> Result<ApiHomeResponse> { + let mut items = DATABASE.list_nodes_with_udata(&session.user.name)?; - let mut toplevel = db + let mut toplevel = DATABASE .get_node_children(NodeID::from_slug("library")) .context("root node missing")? .into_iter() - .map(|n| db.get_node_with_userdata(n, &session)) + .map(|n| DATABASE.get_node_with_userdata(n, &session)) .collect::<anyhow::Result<Vec<_>>>()?; toplevel.sort_by_key(|(n, _)| n.index.unwrap_or(usize::MAX)); diff --git a/logic/src/items.rs b/logic/src/items.rs index 99fb767..eddfb03 100644 --- a/logic/src/items.rs +++ b/logic/src/items.rs @@ -4,21 +4,19 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{filter_sort::filter_and_sort_nodes, session::Session}; +use crate::{DATABASE, filter_sort::filter_and_sort_nodes, session::Session}; use anyhow::Result; use jellycommon::{ Visibility, api::{ApiItemsResponse, NodeFilterSort, SortOrder, SortProperty}, }; -use jellydb::Database; pub fn all_items( - db: &Database, session: &Session, page: Option<usize>, filter: NodeFilterSort, ) -> Result<ApiItemsResponse> { - let mut items = db.list_nodes_with_udata(session.user.name.as_str())?; + let mut items = DATABASE.list_nodes_with_udata(session.user.name.as_str())?; items.retain(|(n, _)| matches!(n.visibility, Visibility::Visible)); diff --git a/logic/src/lib.rs b/logic/src/lib.rs index 004e008..9988ed2 100644 --- a/logic/src/lib.rs +++ b/logic/src/lib.rs @@ -6,6 +6,7 @@ #![feature(duration_constructors, let_chains)] pub mod admin; +pub mod assets; pub mod filter_sort; pub mod home; pub mod items; @@ -14,10 +15,13 @@ pub mod node; pub mod search; pub mod session; pub mod stats; +pub mod account; -pub use jellydb::Database; - +use anyhow::Context; +use anyhow::Result; +use jellydb::Database; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use std::sync::LazyLock; use std::sync::Mutex; @@ -28,6 +32,7 @@ pub struct Config { session_key: Option<String>, admin_username:Option<String>, admin_password:Option<String>, + database_path: PathBuf, } pub static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None); @@ -47,3 +52,11 @@ static DATABASE: LazyLock<Database> = LazyLock::new(|| { .take() .expect("database not preloaded. logic error") }); + +pub fn init_database() -> Result<()> { + let database = Database::open(&CONF.database_path) + .context("opening database") + .unwrap(); + *DATABASE_PRELOAD.lock().unwrap() = Some(database); + Ok(()) +} diff --git a/logic/src/login.rs b/logic/src/login.rs index 72a5903..5e255a0 100644 --- a/logic/src/login.rs +++ b/logic/src/login.rs @@ -3,19 +3,18 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{CONF, session::create}; +use crate::{CONF, DATABASE, session::create}; use anyhow::{Result, anyhow}; use argon2::{Argon2, PasswordHasher, password_hash::Salt}; use jellycommon::user::UserPermission; -use jellydb::Database; use log::info; use std::{collections::HashSet, time::Duration}; -pub fn create_admin_account(database: &Database) -> Result<()> { +pub fn create_admin_account() -> Result<()> { if let Some(username) = &CONF.admin_username && let Some(password) = &CONF.admin_password { - database + DATABASE .create_admin_user(username, hash_password(username, password)) .unwrap(); } else { @@ -25,7 +24,6 @@ pub fn create_admin_account(database: &Database) -> Result<()> { } pub fn login_logic( - database: &Database, username: &str, password: &str, expire: Option<i64>, @@ -34,7 +32,7 @@ pub fn login_logic( // hashing the password regardless if the accounts exists to better resist timing attacks let password = hash_password(username, password); - let mut user = database + let mut user = DATABASE .get_user(username)? .ok_or(anyhow!("invalid password"))?; diff --git a/logic/src/node.rs b/logic/src/node.rs index c8ff820..820116f 100644 --- a/logic/src/node.rs +++ b/logic/src/node.rs @@ -3,30 +3,30 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{filter_sort::filter_and_sort_nodes, session::Session}; +use crate::{DATABASE, filter_sort::filter_and_sort_nodes, session::Session}; use anyhow::{Result, anyhow}; use jellycommon::{ Node, NodeID, NodeKind, Visibility, api::{ApiNodeResponse, NodeFilterSort, SortOrder, SortProperty}, - user::NodeUserData, + user::{NodeUserData, WatchedState}, }; use jellydb::Database; use std::{cmp::Reverse, collections::BTreeMap, sync::Arc}; pub fn get_node( - db: &Database, - id: NodeID, session: &Session, + id: NodeID, children: bool, parents: bool, filter: NodeFilterSort, ) -> Result<ApiNodeResponse> { - let (node, udata) = db.get_node_with_userdata(id, &session)?; + let (node, udata) = DATABASE.get_node_with_userdata(id, &session)?; let mut children = if children { - db.get_node_children(id)? + DATABASE + .get_node_children(id)? .into_iter() - .map(|c| db.get_node_with_userdata(c, &session)) + .map(|c| DATABASE.get_node_with_userdata(c, &session)) .collect::<anyhow::Result<Vec<_>>>()? } else { Vec::new() @@ -35,13 +35,13 @@ pub fn get_node( let mut parents = if parents { node.parents .iter() - .map(|pid| db.get_node_with_userdata(*pid, &session)) + .map(|pid| DATABASE.get_node_with_userdata(*pid, &session)) .collect::<anyhow::Result<Vec<_>>>()? } else { Vec::new() }; - let mut similar = get_similar_media(&node, db, &session)?; + let mut similar = get_similar_media(&session, &node)?; similar.retain(|(n, _)| n.visibility >= Visibility::Reduced); children.retain(|(n, _)| n.visibility >= Visibility::Reduced); @@ -65,15 +65,11 @@ pub fn get_node( }) } -pub fn get_similar_media( - node: &Node, - db: &Database, - session: &Session, -) -> Result<Vec<(Arc<Node>, NodeUserData)>> { +pub fn get_similar_media(session: &Session, node: &Node) -> Result<Vec<(Arc<Node>, NodeUserData)>> { let this_id = NodeID::from_slug(&node.slug); let mut ranking = BTreeMap::<NodeID, usize>::new(); for tag in &node.tags { - let nodes = db.get_tag_nodes(tag)?; + let nodes = DATABASE.get_tag_nodes(tag)?; let weight = 1_000_000 / nodes.len(); for n in nodes { if n != this_id { @@ -86,7 +82,7 @@ pub fn get_similar_media( ranking .into_iter() .take(32) - .map(|(pid, _)| db.get_node_with_userdata(pid, session)) + .map(|(pid, _)| DATABASE.get_node_with_userdata(pid, session)) .collect::<anyhow::Result<Vec<_>>>() } @@ -110,3 +106,60 @@ impl DatabaseNodeUserDataExt for Database { )) } } + +pub fn get_nodes_modified_since(_session: &Session, since: u64) -> Result<Vec<NodeID>> { + let mut nodes = DATABASE.get_nodes_modified_since(since)?; + nodes.retain(|id| { + DATABASE.get_node(*id).is_ok_and(|n| { + n.as_ref() + .is_some_and(|n| n.visibility >= Visibility::Reduced) + }) + }); + Ok(nodes) +} + +pub fn get_node_by_eid(_session: &Session, platform: &str, eid: &str) -> Result<Option<NodeID>> { + DATABASE.get_node_external_id(platform, eid) +} +pub fn node_id_to_slug(_session: &Session, id: NodeID) -> Result<String> { + Ok(DATABASE + .get_node(id)? + .ok_or(anyhow!("node does not exist"))? + .slug + .to_owned()) +} + +pub fn update_node_userdata_watched( + session: &Session, + node: NodeID, + state: WatchedState, +) -> Result<()> { + // TODO perm + DATABASE.update_node_udata(node, &session.user.name, |udata| { + udata.watched = state; + Ok(()) + }) +} +pub fn update_node_userdata_watched_progress( + session: &Session, + node: NodeID, + time: f64, +) -> Result<()> { + // TODO perm + DATABASE.update_node_udata(node, &session.user.name, |udata| { + udata.watched = match udata.watched { + WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => { + WatchedState::Progress(time) + } + WatchedState::Watched => WatchedState::Watched, + }; + Ok(()) + }) +} +pub fn update_node_userdata_rating(session: &Session, node: NodeID, rating: i32) -> Result<()> { + // TODO perm + DATABASE.update_node_udata(node, &session.user.name, |udata| { + udata.rating = rating; + Ok(()) + }) +} diff --git a/logic/src/search.rs b/logic/src/search.rs index 68975f1..304676b 100644 --- a/logic/src/search.rs +++ b/logic/src/search.rs @@ -3,23 +3,17 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{node::DatabaseNodeUserDataExt, session::Session}; +use crate::{DATABASE, node::DatabaseNodeUserDataExt, session::Session}; use anyhow::Result; use jellycommon::{Visibility, api::ApiSearchResponse}; -use jellydb::Database; use std::time::Instant; -pub fn search( - db: &Database, - session: &Session, - query: &str, - page: Option<usize>, -) -> Result<ApiSearchResponse> { +pub fn search(session: &Session, query: &str, page: Option<usize>) -> Result<ApiSearchResponse> { let timing = Instant::now(); - let (count, ids) = db.search(query, 32, page.unwrap_or_default() * 32)?; + let (count, ids) = DATABASE.search(query, 32, page.unwrap_or_default() * 32)?; let mut results = ids .into_iter() - .map(|id| db.get_node_with_userdata(id, &session)) + .map(|id| DATABASE.get_node_with_userdata(id, &session)) .collect::<Result<Vec<_>, anyhow::Error>>()?; results.retain(|(n, _)| n.visibility >= Visibility::Reduced); let duration = timing.elapsed(); diff --git a/logic/src/session.rs b/logic/src/session.rs index 72a1089..615694c 100644 --- a/logic/src/session.rs +++ b/logic/src/session.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::CONF; +use crate::{CONF, DATABASE}; use aes_gcm_siv::{ KeyInit, aead::{Aead, generic_array::GenericArray}, @@ -85,10 +85,27 @@ pub fn validate(token: &str) -> anyhow::Result<String> { Ok(session_data.username) } +pub fn token_to_session(token: &str) -> anyhow::Result<Session> { + let username = validate(token)?; + let user = DATABASE + .get_user(&username)? + .ok_or(anyhow!("user does not exist"))?; + Ok(Session { user }) +} +pub fn bypass_auth_session() -> anyhow::Result<Session> { + let user = DATABASE + .get_user(&CONF.admin_username.as_ref().unwrap())? + .ok_or(anyhow!("user does not exist"))?; + Ok(Session { user }) +} + #[cfg(test)] fn load_test_config() { + use std::path::PathBuf; + use crate::{CONF_PRELOAD, Config}; *CONF_PRELOAD.lock().unwrap() = Some(Config { + database_path: PathBuf::default(), login_expire: 10, session_key: None, admin_password: None, diff --git a/logic/src/stats.rs b/logic/src/stats.rs index 2e962e2..c7464f9 100644 --- a/logic/src/stats.rs +++ b/logic/src/stats.rs @@ -4,17 +4,16 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::session::Session; +use crate::{DATABASE, session::Session}; use anyhow::Result; use jellycommon::{ Node, NodeKind, Visibility, api::{ApiStatsResponse, StatsBin}, }; -use jellydb::Database; use std::collections::BTreeMap; -pub fn stats(db: &Database, session: &Session) -> Result<ApiStatsResponse> { - let mut items = db.list_nodes_with_udata(session.user.name.as_str())?; +pub fn stats(session: &Session) -> Result<ApiStatsResponse> { + let mut items = DATABASE.list_nodes_with_udata(session.user.name.as_str())?; items.retain(|(n, _)| n.visibility >= Visibility::Reduced); trait BinExt { diff --git a/server/src/api.rs b/server/src/api.rs index 38bab08..d983548 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -5,15 +5,15 @@ */ use super::ui::error::MyResult; use crate::helper::{accept::AcceptJson, language::AcceptLanguage, A}; -use jellycommon::{user::CreateSessionParams, NodeID, Visibility}; +use jellycommon::{user::CreateSessionParams, NodeID}; use jellyimport::asset_token::AssetInner; use jellylogic::{ login::login_logic, + node::get_nodes_modified_since, session::{AdminSession, Session}, - Database, }; use jellyui::locale::get_translation_table; -use rocket::{get, post, response::Redirect, serde::json::Json, Either, State}; +use rocket::{get, post, response::Redirect, serde::json::Json, Either}; use serde_json::{json, Value}; use std::collections::HashMap; @@ -49,12 +49,8 @@ pub fn r_translations( } #[post("/api/create_session", data = "<data>")] -pub fn r_api_account_login( - database: &State<Database>, - data: Json<CreateSessionParams>, -) -> MyResult<Value> { +pub fn r_api_account_login(data: Json<CreateSessionParams>) -> MyResult<Value> { let token = login_logic( - database, &data.username, &data.password, data.expire, @@ -69,17 +65,7 @@ pub fn r_api_asset_token_raw(_admin: A<AdminSession>, token: &str) -> MyResult<J } #[get("/nodes_modified?<since>")] -pub fn r_nodes_modified_since( - _session: A<Session>, - database: &State<Database>, - since: u64, -) -> MyResult<Json<Vec<NodeID>>> { - let mut nodes = database.get_nodes_modified_since(since)?; - nodes.retain(|id| { - database.get_node(*id).is_ok_and(|n| { - n.as_ref() - .is_some_and(|n| n.visibility >= Visibility::Reduced) - }) - }); +pub fn r_nodes_modified_since(session: A<Session>, since: u64) -> MyResult<Json<Vec<NodeID>>> { + let nodes = get_nodes_modified_since(&session.0, since)?; Ok(Json(nodes)) } diff --git a/server/src/compat/jellyfin/mod.rs b/server/src/compat/jellyfin/mod.rs index e8a74d7..0a901b2 100644 --- a/server/src/compat/jellyfin/mod.rs +++ b/server/src/compat/jellyfin/mod.rs @@ -6,19 +6,22 @@ pub mod models; use crate::{helper::A, ui::error::MyResult}; -use anyhow::{anyhow, Context}; +use anyhow::anyhow; use jellycommon::{ - api::{FilterProperty, NodeFilterSort, SortOrder, SortProperty}, + api::{NodeFilterSort, SortOrder, SortProperty}, routes::{u_asset, u_node_slug_backdrop, u_node_slug_poster}, stream::{StreamContainer, StreamSpec}, user::{NodeUserData, WatchedState}, MediaInfo, Node, NodeID, NodeKind, SourceTrack, SourceTrackKind, Visibility, }; use jellylogic::{ - filter_sort::filter_and_sort_nodes, login::login_logic, node::DatabaseNodeUserDataExt, - session::Session, Database, + login::login_logic, + node::{get_node, update_node_userdata_watched_progress}, + search::search, + session::Session, }; use jellyui::{get_brand, get_slogan, node_page::aspect_class}; +use log::warn; use models::*; use rocket::{ get, @@ -26,16 +29,18 @@ use rocket::{ post, response::Redirect, serde::json::Json, - FromForm, State, + FromForm, }; use serde::Deserialize; use serde_json::{json, Value}; use std::{collections::BTreeMap, net::IpAddr}; +// these are both random values. idk what they are for const SERVER_ID: &str = "1694a95daf70708147f16103ce7b7566"; const USER_ID: &str = "33f772aae6c2495ca89fe00340dbd17c"; + const VERSION: &str = "10.10.0"; -const LOCAL_ADDRESS: &str = "http://127.0.0.1:8000"; +const LOCAL_ADDRESS: &str = "http://127.0.0.1:8000"; // TODO #[get("/System/Info/Public")] pub fn r_jellyfin_system_info_public_case() -> Json<Value> { @@ -182,24 +187,26 @@ pub fn r_jellyfin_items_images_backdrop( #[get("/Items/<id>")] #[allow(private_interfaces)] -pub fn r_jellyfin_items_item( - session: A<Session>, - database: &State<Database>, - id: &str, -) -> MyResult<Json<JellyfinItem>> { - let (n, ud) = database.get_node_with_userdata(NodeID::from_slug(id), &session.0)?; - Ok(Json(item_object(&n, &ud))) +pub fn r_jellyfin_items_item(session: A<Session>, id: &str) -> MyResult<Json<JellyfinItem>> { + let r = get_node( + &session.0, + NodeID::from_slug(id), + false, + false, + NodeFilterSort::default(), + )?; + Ok(Json(item_object(&r.node, &r.userdata))) } + #[get("/Users/<uid>/Items/<id>")] #[allow(private_interfaces)] pub fn r_jellyfin_users_items_item( session: A<Session>, - database: &State<Database>, uid: &str, id: &str, ) -> MyResult<Json<JellyfinItem>> { let _ = uid; - r_jellyfin_items_item(session, database, id) + r_jellyfin_items_item(session, id) } #[derive(Debug, FromForm)] @@ -223,112 +230,118 @@ struct JellyfinItemQuery { #[allow(private_interfaces)] pub fn r_jellyfin_users_items( session: A<Session>, - database: &State<Database>, uid: &str, query: JellyfinItemQuery, -) -> MyResult<Json<Value>> { +) -> MyResult<Json<JellyfinItemsResponse>> { let _ = uid; - r_jellyfin_items(session, database, query) + r_jellyfin_items(session, query) } #[get("/Artists?<query..>")] #[allow(private_interfaces)] pub fn r_jellyfin_artists( session: A<Session>, - database: &State<Database>, mut query: JellyfinItemQuery, -) -> MyResult<Json<Value>> { +) -> MyResult<Json<JellyfinItemsResponse>> { query.internal_artists = true; - r_jellyfin_items(session, database, query)?; // TODO - Ok(Json(json!({ - "Items": [], - "TotalRecordCount": 0, - "StartIndex": 0 - }))) + r_jellyfin_items(session, query)?; // TODO + Ok(Json(JellyfinItemsResponse::default())) } #[get("/Persons?<query..>")] #[allow(private_interfaces)] pub fn r_jellyfin_persons( session: A<Session>, - database: &State<Database>, mut query: JellyfinItemQuery, -) -> MyResult<Json<Value>> { +) -> MyResult<Json<JellyfinItemsResponse>> { query.internal_persons = true; - r_jellyfin_items(session, database, query)?; // TODO - Ok(Json(json!({ - "Items": [], - "TotalRecordCount": 0, - "StartIndex": 0 - }))) + r_jellyfin_items(session, query)?; // TODO + Ok(Json(JellyfinItemsResponse::default())) } #[get("/Items?<query..>")] #[allow(private_interfaces)] pub fn r_jellyfin_items( session: A<Session>, - database: &State<Database>, query: JellyfinItemQuery, -) -> MyResult<Json<Value>> { - let (nodes, parent_kind) = if let Some(q) = query.search_term { - ( - database - .search(&q, query.limit, query.start_index.unwrap_or_default())? - .1, - None, - ) +) -> MyResult<Json<JellyfinItemsResponse>> { + // let (nodes, parent_kind) = if let Some(q) = query.search_term { + // ( + // database + // .search(&q, query.limit, query.start_index.unwrap_or_default())? + // .1, + // None, + // ) + // } else if let Some(parent) = query.parent_id { + // let parent = NodeID::from_slug(&parent); + // ( + // database + // .get_node_children(parent)? + // .into_iter() + // .skip(query.start_index.unwrap_or_default()) + // .take(query.limit) + // .collect(), + // database.get_node(parent)?.map(|n| n.kind), + // ) + // } else { + // (vec![], None) + // }; + + // let filter_kind = query + // .include_item_types + // .map(|n| match n.as_str() { + // "Movie" => vec![FilterProperty::KindMovie], + // "Audio" => vec![FilterProperty::KindMusic], + // "Video" => vec![FilterProperty::KindVideo], + // "TvChannel" => vec![FilterProperty::KindChannel], + // _ => vec![], + // }) + // .or(if query.internal_artists { + // Some(vec![]) + // } else { + // None + // }) + // .or(if query.internal_persons { + // Some(vec![]) + // } else { + // None + // }); + + // let mut nodes = nodes + // .into_iter() + // .map(|nid| database.get_node_with_userdata(nid, &session.0)) + // .collect::<Result<Vec<_>, anyhow::Error>>()?; + + // filter_and_sort_nodes( + // &NodeFilterSort { + // sort_by: None, + // filter_kind, + // sort_order: None, + // }, + // match parent_kind { + // Some(NodeKind::Channel) => (SortProperty::ReleaseDate, SortOrder::Descending), + // _ => (SortProperty::Title, SortOrder::Ascending), + // }, + // &mut nodes, + // ); + + let nodes = if let Some(q) = query.search_term { + search(&session.0, &q, query.start_index.map(|x| x / 50))?.results // TODO } else if let Some(parent) = query.parent_id { - let parent = NodeID::from_slug(&parent); - ( - database - .get_node_children(parent)? - .into_iter() - .skip(query.start_index.unwrap_or_default()) - .take(query.limit) - .collect(), - database.get_node(parent)?.map(|n| n.kind), - ) + get_node( + &session.0, + NodeID::from_slug(&parent), + true, + false, + NodeFilterSort::default(), + )? + .children } else { - (vec![], None) + warn!("unknown items request"); + vec![] }; - let filter_kind = query - .include_item_types - .map(|n| match n.as_str() { - "Movie" => vec![FilterProperty::KindMovie], - "Audio" => vec![FilterProperty::KindMusic], - "Video" => vec![FilterProperty::KindVideo], - "TvChannel" => vec![FilterProperty::KindChannel], - _ => vec![], - }) - .or(if query.internal_artists { - Some(vec![]) - } else { - None - }) - .or(if query.internal_persons { - Some(vec![]) - } else { - None - }); - - let mut nodes = nodes - .into_iter() - .map(|nid| database.get_node_with_userdata(nid, &session.0)) - .collect::<Result<Vec<_>, anyhow::Error>>()?; - - filter_and_sort_nodes( - &NodeFilterSort { - sort_by: None, - filter_kind, - sort_order: None, - }, - match parent_kind { - Some(NodeKind::Channel) => (SortProperty::ReleaseDate, SortOrder::Descending), - _ => (SortProperty::Title, SortOrder::Ascending), - }, - &mut nodes, - ); + // TODO reimplemnt filter behaviour let items = nodes .into_iter() @@ -336,37 +349,33 @@ pub fn r_jellyfin_items( .map(|(n, ud)| item_object(&n, &ud)) .collect::<Vec<_>>(); - Ok(Json(json!({ - "Items": items, - "TotalRecordCount": items.len(), - "StartIndex": query.start_index.unwrap_or_default() - }))) + Ok(Json(JellyfinItemsResponse { + total_record_count: items.len(), + start_index: query.start_index.unwrap_or_default(), + items, + })) } #[get("/UserViews?<userId>")] #[allow(non_snake_case)] -pub fn r_jellyfin_users_views( - session: A<Session>, - database: &State<Database>, - userId: &str, -) -> MyResult<Json<Value>> { +pub fn r_jellyfin_users_views(session: A<Session>, userId: &str) -> MyResult<Json<Value>> { let _ = userId; - let mut toplevel = database - .get_node_children(NodeID::from_slug("library")) - .context("root node missing")? - .into_iter() - .map(|nid| database.get_node_with_userdata(nid, &session.0)) - .collect::<Result<Vec<_>, anyhow::Error>>()?; - - toplevel.sort_by_key(|(n, _)| n.index.unwrap_or(usize::MAX)); - - let mut items = Vec::new(); - for (n, ud) in toplevel { - if n.visibility >= Visibility::Reduced { - items.push(item_object(&n, &ud)) - } - } + let items = get_node( + &session.0, + NodeID::from_slug("library"), + false, + true, + NodeFilterSort { + sort_by: Some(SortProperty::Index), + sort_order: Some(SortOrder::Ascending), + filter_kind: None, + }, + )? + .children + .into_iter() + .map(|(node, udata)| item_object(&node, &udata)) + .collect::<Vec<_>>(); Ok(Json(json!({ "Items": items, @@ -414,14 +423,15 @@ pub fn r_jellyfin_shows_nextup(_session: A<Session>) -> Json<Value> { } #[post("/Items/<id>/PlaybackInfo")] -pub fn r_jellyfin_items_playbackinfo( - _session: A<Session>, - database: &State<Database>, - id: &str, -) -> MyResult<Json<Value>> { - let node = database - .get_node_slug(id)? - .ok_or(anyhow!("node does not exist"))?; +pub fn r_jellyfin_items_playbackinfo(session: A<Session>, id: &str) -> MyResult<Json<Value>> { + let node = get_node( + &session.0, + NodeID::from_slug(id), + false, + false, + NodeFilterSort::default(), + )? + .node; let media = node.media.as_ref().ok_or(anyhow!("node has no media"))?; let ms = media_source_object(&node, media); Ok(Json(json!({ @@ -431,14 +441,15 @@ pub fn r_jellyfin_items_playbackinfo( } #[get("/Videos/<id>/stream.webm")] -pub fn r_jellyfin_video_stream( - _session: A<Session>, - database: &State<Database>, - id: &str, -) -> MyResult<Redirect> { - let node = database - .get_node_slug(id)? - .ok_or(anyhow!("node does not exist"))?; +pub fn r_jellyfin_video_stream(session: A<Session>, id: &str) -> MyResult<Redirect> { + let node = get_node( + &session.0, + NodeID::from_slug(id), + false, + false, + NodeFilterSort::default(), + )? + .node; let media = node.media.as_ref().ok_or(anyhow!("node has no media"))?; let params = StreamSpec::Remux { tracks: (0..media.tracks.len()).collect(), @@ -458,23 +469,10 @@ struct JellyfinProgressData { #[allow(private_interfaces)] pub fn r_jellyfin_sessions_playing_progress( session: A<Session>, - database: &State<Database>, data: Json<JellyfinProgressData>, ) -> MyResult<()> { let position = data.position_ticks / 10_000_000.; - database.update_node_udata( - NodeID::from_slug(&data.item_id), - &session.0.user.name, - |udata| { - udata.watched = match udata.watched { - WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => { - WatchedState::Progress(position) - } - WatchedState::Watched => WatchedState::Watched, - }; - Ok(()) - }, - )?; + update_node_userdata_watched_progress(&session.0, NodeID::from_slug(&data.item_id), position)?; Ok(()) } @@ -501,22 +499,20 @@ struct AuthData { #[allow(private_interfaces)] pub fn r_jellyfin_users_authenticatebyname_case( client_addr: IpAddr, - database: &State<Database>, data: Json<AuthData>, jar: &CookieJar, ) -> MyResult<Json<Value>> { - r_jellyfin_users_authenticatebyname(client_addr, database, data, jar) + r_jellyfin_users_authenticatebyname(client_addr, data, jar) } #[post("/Users/authenticatebyname", data = "<data>")] #[allow(private_interfaces)] pub fn r_jellyfin_users_authenticatebyname( client_addr: IpAddr, - database: &State<Database>, data: Json<AuthData>, jar: &CookieJar, ) -> MyResult<Json<Value>> { - let token = login_logic(database, &data.username, &data.pw, None, None)?; + let token = login_logic(&data.username, &data.pw, None, None)?; // setting the session cookie too because image requests carry no auth headers for some reason. // TODO find alternative, non-web clients might not understand cookies diff --git a/server/src/compat/jellyfin/models.rs b/server/src/compat/jellyfin/models.rs index 6a68455..9dbad9c 100644 --- a/server/src/compat/jellyfin/models.rs +++ b/server/src/compat/jellyfin/models.rs @@ -7,6 +7,14 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap; +#[derive(Debug, Serialize, Default)] +#[serde(rename_all = "PascalCase")] +pub(super) struct JellyfinItemsResponse { + pub items: Vec<JellyfinItem>, + pub total_record_count: usize, + pub start_index: usize, +} + #[derive(Debug, Serialize, Deserialize)] pub(super) enum JellyfinItemType { AudioBook, diff --git a/server/src/compat/youtube.rs b/server/src/compat/youtube.rs index 0a69d14..7126781 100644 --- a/server/src/compat/youtube.rs +++ b/server/src/compat/youtube.rs @@ -6,48 +6,47 @@ use crate::{helper::A, ui::error::MyResult}; use anyhow::anyhow; use jellycommon::routes::{u_node_slug, u_node_slug_player}; -use jellylogic::{session::Session, Database}; -use rocket::{get, response::Redirect, State}; +use jellylogic::{ + node::{get_node_by_eid, node_id_to_slug}, + session::Session, +}; +use rocket::{get, response::Redirect}; #[get("/watch?<v>")] -pub fn r_youtube_watch(_session: A<Session>, db: &State<Database>, v: &str) -> MyResult<Redirect> { +pub fn r_youtube_watch(session: A<Session>, v: &str) -> MyResult<Redirect> { if v.len() != 11 { Err(anyhow!("video id length incorrect"))? } - let Some(id) = db.get_node_external_id("youtube:video", v)? else { + let Some(id) = get_node_by_eid(&session.0, "youtube:video", v)? else { Err(anyhow!("element not found"))? }; - let node = db.get_node(id)?.ok_or(anyhow!("node missing"))?; - Ok(Redirect::to(u_node_slug_player(&node.slug))) + let slug = node_id_to_slug(&session.0, id)?; + Ok(Redirect::to(u_node_slug_player(&slug))) } #[get("/channel/<id>")] -pub fn r_youtube_channel( - _session: A<Session>, - db: &State<Database>, - id: &str, -) -> MyResult<Redirect> { +pub fn r_youtube_channel(session: A<Session>, id: &str) -> MyResult<Redirect> { let Some(id) = (if id.starts_with("UC") { - db.get_node_external_id("youtube:channel", id)? + get_node_by_eid(&session.0, "youtube:channel", id)? } else if id.starts_with("@") { - db.get_node_external_id("youtube:channel-name", id)? + get_node_by_eid(&session.0, "youtube:channel-name", id)? } else { Err(anyhow!("unknown channel id format"))? }) else { Err(anyhow!("channel not found"))? }; - let node = db.get_node(id)?.ok_or(anyhow!("node missing"))?; - Ok(Redirect::to(u_node_slug(&node.slug))) + let slug = node_id_to_slug(&session.0, id)?; + Ok(Redirect::to(u_node_slug(&slug))) } #[get("/embed/<v>")] -pub fn r_youtube_embed(_session: A<Session>, db: &State<Database>, v: &str) -> MyResult<Redirect> { +pub fn r_youtube_embed(session: A<Session>, v: &str) -> MyResult<Redirect> { if v.len() != 11 { Err(anyhow!("video id length incorrect"))? } - let Some(id) = db.get_node_external_id("youtube:video", v)? else { + let Some(id) = get_node_by_eid(&session.0, "youtube:video", v)? else { Err(anyhow!("element not found"))? }; - let node = db.get_node(id)?.ok_or(anyhow!("node missing"))?; - Ok(Redirect::to(u_node_slug_player(&node.slug))) + let slug = node_id_to_slug(&session.0, id)?; + Ok(Redirect::to(u_node_slug_player(&slug))) } diff --git a/server/src/config.rs b/server/src/config.rs index 202948a..28fcf90 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -5,6 +5,7 @@ */ use anyhow::{anyhow, Context, Result}; +use jellylogic::init_database; use serde::Deserialize; use std::env::{args, var}; use tokio::fs::read_to_string; @@ -41,5 +42,7 @@ pub async fn load_config() -> Result<()> { *crate::CONF_PRELOAD.lock().unwrap() = Some(config.server); *jellyui::CONF_PRELOAD.lock().unwrap() = Some(config.ui); + init_database()?; + Ok(()) } diff --git a/server/src/helper/session.rs b/server/src/helper/session.rs index d51acd3..090330b 100644 --- a/server/src/helper/session.rs +++ b/server/src/helper/session.rs @@ -6,24 +6,19 @@ use super::A; use crate::ui::error::MyError; use anyhow::anyhow; -use jellylogic::{ - session::{validate, AdminSession, Session}, - Database, -}; +use jellylogic::session::{bypass_auth_session, token_to_session, AdminSession, Session}; use log::warn; use rocket::{ async_trait, http::Status, outcome::Outcome, request::{self, FromRequest}, - Request, State, + Request, }; pub(super) async fn session_from_request(req: &Request<'_>) -> Result<Session, MyError> { - let username; - if cfg!(feature = "bypass-auth") { - username = "admin".to_string(); + Ok(bypass_auth_session()?) } else { let token = req .query_value("session") @@ -40,14 +35,8 @@ pub(super) async fn session_from_request(req: &Request<'_>) -> Result<Session, M // jellyfin urlescapes the token for *some* requests let token = token.replace("%3D", "="); - username = validate(&token)?; - }; - - let db = req.guard::<&State<Database>>().await.unwrap(); - - let user = db.get_user(&username)?.ok_or(anyhow!("user not found"))?; - - Ok(Session { user }) + Ok(token_to_session(&token)?) + } } fn parse_jellyfin_auth(h: &str) -> Option<&str> { diff --git a/server/src/logic/mod.rs b/server/src/logic/mod.rs index 26f45de..a7991f7 100644 --- a/server/src/logic/mod.rs +++ b/server/src/logic/mod.rs @@ -6,4 +6,3 @@ pub mod playersync; pub mod stream; pub mod userdata; - diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs index c21edaa..1e518e2 100644 --- a/server/src/logic/stream.rs +++ b/server/src/logic/stream.rs @@ -5,9 +5,9 @@ */ use crate::{helper::A, ui::error::MyError}; use anyhow::{anyhow, Result}; -use jellycommon::{stream::StreamSpec, TrackSource}; +use jellycommon::{api::NodeFilterSort, stream::StreamSpec, NodeID, TrackSource}; use jellyimport::asset_token::AssetInner; -use jellylogic::{session::Session, Database}; +use jellylogic::{node::get_node, session::Session}; use jellystream::SMediaInfo; use log::{info, warn}; use rocket::{ @@ -15,7 +15,7 @@ use rocket::{ http::{Header, Status}, request::{self, FromRequest}, response::{self, Redirect, Responder}, - Either, Request, Response, State, + Either, Request, Response, }; use std::{ collections::{BTreeMap, BTreeSet}, @@ -42,17 +42,21 @@ pub async fn r_stream_head( #[get("/n/<id>/stream?<spec..>")] pub async fn r_stream( - _session: A<Session>, - db: &State<Database>, + session: A<Session>, id: &str, range: Option<RequestRange>, spec: BTreeMap<String, String>, ) -> Result<Either<StreamResponse, RedirectResponse>, MyError> { let spec = StreamSpec::from_query_kv(&spec).map_err(|x| anyhow!("spec invalid: {x}"))?; // TODO perm - let node = db - .get_node_slug(id)? - .ok_or(anyhow!("node does not exist"))?; + let node = get_node( + &session.0, + NodeID::from_slug(id), + false, + false, + NodeFilterSort::default(), + )? + .node; let media = Arc::new( node.media diff --git a/server/src/logic/userdata.rs b/server/src/logic/userdata.rs index ac3cb83..52c3688 100644 --- a/server/src/logic/userdata.rs +++ b/server/src/logic/userdata.rs @@ -5,13 +5,20 @@ */ use crate::{helper::A, ui::error::MyResult}; use jellycommon::{ + api::NodeFilterSort, routes::u_node_id, user::{NodeUserData, WatchedState}, NodeID, }; -use jellylogic::{session::Session, Database}; +use jellylogic::{ + node::{ + get_node, update_node_userdata_rating, update_node_userdata_watched, + update_node_userdata_watched_progress, + }, + session::Session, +}; use rocket::{ - form::Form, get, post, response::Redirect, serde::json::Json, FromForm, FromFormField, State, + form::Form, get, post, response::Redirect, serde::json::Json, FromForm, FromFormField, UriDisplayQuery, }; @@ -23,33 +30,26 @@ pub enum UrlWatchedState { } #[get("/n/<id>/userdata")] -pub fn r_node_userdata( - session: A<Session>, - db: &State<Database>, - id: A<NodeID>, -) -> MyResult<Json<NodeUserData>> { - let u = db - .get_node_udata(id.0, &session.0.user.name)? - .unwrap_or_default(); +pub fn r_node_userdata(session: A<Session>, id: A<NodeID>) -> MyResult<Json<NodeUserData>> { + let u = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?.userdata; Ok(Json(u)) } #[post("/n/<id>/watched?<state>")] pub async fn r_node_userdata_watched( session: A<Session>, - db: &State<Database>, id: A<NodeID>, state: UrlWatchedState, ) -> MyResult<Redirect> { - // TODO perm - db.update_node_udata(id.0, &session.0.user.name, |udata| { - udata.watched = match state { + update_node_userdata_watched( + &session.0, + id.0, + match state { UrlWatchedState::None => WatchedState::None, UrlWatchedState::Watched => WatchedState::Watched, UrlWatchedState::Pending => WatchedState::Pending, - }; - Ok(()) - })?; + }, + )?; Ok(Redirect::found(u_node_id(id.0))) } @@ -62,34 +62,15 @@ pub struct UpdateRating { #[post("/n/<id>/update_rating", data = "<form>")] pub async fn r_node_userdata_rating( session: A<Session>, - db: &State<Database>, id: A<NodeID>, form: Form<UpdateRating>, ) -> MyResult<Redirect> { - // TODO perm - db.update_node_udata(id.0, &session.0.user.name, |udata| { - udata.rating = form.rating; - Ok(()) - })?; + update_node_userdata_rating(&session.0, id.0, form.rating)?; Ok(Redirect::found(u_node_id(id.0))) } #[post("/n/<id>/progress?<t>")] -pub async fn r_node_userdata_progress( - session: A<Session>, - db: &State<Database>, - id: A<NodeID>, - t: f64, -) -> MyResult<()> { - // TODO perm - db.update_node_udata(id.0, &session.0.user.name, |udata| { - udata.watched = match udata.watched { - WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => { - WatchedState::Progress(t) - } - WatchedState::Watched => WatchedState::Watched, - }; - Ok(()) - })?; +pub async fn r_node_userdata_progress(session: A<Session>, id: A<NodeID>, t: f64) -> MyResult<()> { + update_node_userdata_watched_progress(&session.0, id.0, t)?; Ok(()) } diff --git a/server/src/main.rs b/server/src/main.rs index 5113542..7c7bbd2 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -7,9 +7,7 @@ #![allow(clippy::needless_borrows_for_generic_args)] #![recursion_limit = "4096"] -use anyhow::Context; use config::load_config; -use jellylogic::Database; use jellylogic::{admin::log::enable_logging, login::create_admin_account}; use log::{error, info, warn}; use routes::build_rocket; @@ -28,7 +26,6 @@ pub mod ui; #[rustfmt::skip] #[derive(Debug, Deserialize, Serialize, Default)] pub struct Config { - database_path: PathBuf, asset_path: PathBuf, cookie_key: Option<String>, tls:bool, @@ -56,15 +53,11 @@ async fn main() { #[cfg(feature = "bypass-auth")] log::warn!("authentification bypass enabled"); - let database = Database::open(&CONF.database_path) - .context("opening database") - .unwrap(); - - if let Err(e) = create_admin_account(&database) { + if let Err(e) = create_admin_account() { error!("failed to create admin account: {e:?}"); } - let r = build_rocket(database).launch().await; + let r = build_rocket().launch().await; match r { Ok(_) => warn!("server shutdown"), Err(e) => error!("server exited: {e}"), diff --git a/server/src/routes.rs b/server/src/routes.rs index e14eb44..3f3518b 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -13,7 +13,7 @@ use crate::ui::{ admin::{ log::{r_admin_log, r_admin_log_stream}, r_admin_dashboard, r_admin_import, r_admin_invite, r_admin_remove_invite, - r_admin_transcode_posters, r_admin_update_search, + r_admin_update_search, user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users}, }, assets::{r_asset, r_item_backdrop, r_item_poster, r_node_thumbnail, r_person_asset}, @@ -61,7 +61,6 @@ use crate::{ }, }; use base64::Engine; -use jellylogic::Database; use log::warn; use rand::random; use rocket::{ @@ -76,7 +75,7 @@ macro_rules! uri { }; } -pub fn build_rocket(database: Database) -> Rocket<Build> { +pub fn build_rocket() -> Rocket<Build> { rocket::build() .configure(Config { address: std::env::var("BIND_ADDR") @@ -97,7 +96,6 @@ pub fn build_rocket(database: Database) -> Rocket<Build> { ip_header: Some("x-real-ip".into()), ..Default::default() }) - .manage(database) .manage(PlayersyncChannels::default()) .attach(AdHoc::on_response("set server header", |_req, res| { res.set_header(Header::new("server", "jellything")); @@ -133,7 +131,6 @@ pub fn build_rocket(database: Database) -> Rocket<Build> { r_admin_log, r_admin_remove_invite, r_admin_remove_user, - r_admin_transcode_posters, r_admin_update_search, r_admin_user_permission, r_admin_user, diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index 51da348..2a513a9 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -11,13 +11,8 @@ use crate::{ ui::{error::MyResult, home::rocket_uri_macro_r_home}, }; use anyhow::anyhow; -use jellycommon::user::User; use jellyimport::is_importing; -use jellylogic::{ - login::{hash_password, login_logic}, - session::Session, - Database, -}; +use jellylogic::{account::register_user, login::login_logic, session::Session}; use jellyui::{ account::{AccountLogin, AccountLogout, AccountRegister, AccountRegisterSuccess}, render_page, @@ -29,7 +24,7 @@ use rocket::{ http::{Cookie, CookieJar}, post, response::{content::RawHtml, Redirect}, - FromForm, State, + FromForm, }; use serde::{Deserialize, Serialize}; @@ -98,7 +93,6 @@ pub fn r_account_logout(session: Option<A<Session>>, lang: AcceptLanguage) -> Ra #[post("/account/register", data = "<form>")] pub fn r_account_register_post<'a>( - database: &'a State<Database>, session: Option<A<Session>>, form: Form<Contextual<'a, RegisterForm>>, lang: AcceptLanguage, @@ -110,16 +104,7 @@ pub fn r_account_register_post<'a>( None => return Err(MyError(anyhow!(format_form_error(form)))), }; - 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() - }, - )?; + register_user(&form.invitation, &form.username, &form.password)?; Ok(RawHtml(render_page( &AccountRegisterSuccess { @@ -136,7 +121,6 @@ pub fn r_account_register_post<'a>( #[post("/account/login", data = "<form>")] pub fn r_account_login_post( - database: &State<Database>, jar: &CookieJar, form: Form<Contextual<LoginForm>>, ) -> MyResult<Redirect> { @@ -147,7 +131,7 @@ pub fn r_account_login_post( jar.add( Cookie::build(( "session", - login_logic(database, &form.username, &form.password, None, None)?, + login_logic(&form.username, &form.password, None, None)?, )) .permanent() .build(), diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs index f1a367d..7d1b7af 100644 --- a/server/src/ui/account/settings.rs +++ b/server/src/ui/account/settings.rs @@ -3,14 +3,20 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::{format_form_error, hash_password}; +use super::format_form_error; use crate::{ helper::{language::AcceptLanguage, A}, ui::error::MyResult, }; use jellycommon::user::{PlayerKind, Theme}; use jellyimport::is_importing; -use jellylogic::{session::Session, Database}; +use jellylogic::{ + account::{ + update_user_display_name, update_user_native_secret, update_user_password, + update_user_player_preference, update_user_theme, + }, + session::Session, +}; use jellyui::{ account::settings::SettingsPage, locale::{tr, Language}, @@ -21,7 +27,7 @@ use rocket::{ form::{self, validate::len, Contextual, Form}, get, post, response::content::RawHtml, - FromForm, State, + FromForm, }; use std::ops::Range; @@ -70,7 +76,6 @@ pub fn r_account_settings(session: A<Session>, lang: AcceptLanguage) -> RawHtml< #[post("/account/settings", data = "<form>")] pub fn r_account_settings_post( session: A<Session>, - database: &State<Database>, form: Form<Contextual<SettingsForm>>, lang: AcceptLanguage, ) -> MyResult<RawHtml<String>> { @@ -90,33 +95,30 @@ pub fn r_account_settings_post( let mut out = String::new(); - database.update_user(&session.user.name, |user| { - if let Some(password) = &form.password { - user.password = hash_password(&session.user.name, password); - out += &*tr(lang, "settings.account.password.changed"); - out += "\n"; - } - if let Some(display_name) = &form.display_name { - user.display_name = display_name.clone(); - out += &*tr(lang, "settings.account.display_name.changed"); - out += "\n"; - } - if let Some(theme) = form.theme { - user.theme = theme.0; - out += &*tr(lang, "settings.account.theme.changed"); - out += "\n"; - } - if let Some(player_preference) = form.player_preference { - user.player_preference = player_preference.0; - out += &*tr(lang, "settings.player_preference.changed"); - out += "\n"; - } - if let Some(native_secret) = &form.native_secret { - user.native_secret = native_secret.to_owned(); - out += "Native secret updated.\n"; - } - Ok(()) - })?; + if let Some(password) = &form.password { + update_user_password(&session, password)?; + out += &*tr(lang, "settings.account.password.changed"); + out += "\n"; + } + if let Some(display_name) = &form.display_name { + update_user_display_name(&session, display_name)?; + out += &*tr(lang, "settings.account.display_name.changed"); + out += "\n"; + } + if let Some(theme) = form.theme { + update_user_theme(&session, theme.0)?; + out += &*tr(lang, "settings.account.theme.changed"); + out += "\n"; + } + if let Some(player_preference) = form.player_preference { + update_user_player_preference(&session, player_preference.0)?; + out += &*tr(lang, "settings.player_preference.changed"); + out += "\n"; + } + if let Some(native_secret) = &form.native_secret { + update_user_native_secret(&session, native_secret)?; + out += "Native secret updated.\n"; + } Ok(settings_page( session, // using the old session here, results in outdated theme being displayed diff --git a/server/src/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index 942f4f8..e3eb2d6 100644 --- a/server/src/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -6,50 +6,42 @@ pub mod log; pub mod user; -use super::{ - assets::{resolve_asset, AVIF_QUALITY, AVIF_SPEED}, - error::MyResult, -}; +use super::error::MyResult; use crate::helper::{language::AcceptLanguage, A}; -use anyhow::{anyhow, Context}; use jellycommon::routes::u_admin_dashboard; -use jellyimport::{asset_token::AssetInner, import_wrap, is_importing}; +use jellyimport::is_importing; use jellylogic::{ - admin::{get_import_errors, list_invites}, + admin::{ + create_invite, delete_invite, do_import, get_import_errors, list_invites, + update_search_index, + }, session::AdminSession, - Database, }; use jellyui::{ admin::AdminDashboardPage, render_page, scaffold::{RenderInfo, SessionInfo}, }; -use rand::Rng; use rocket::{ form::Form, get, post, response::{content::RawHtml, Redirect}, - FromForm, State, + FromForm, }; -use std::time::Instant; -use tokio::{sync::Semaphore, task::spawn_blocking}; #[get("/admin/dashboard")] pub async fn r_admin_dashboard( session: A<AdminSession>, - database: &State<Database>, lang: AcceptLanguage, ) -> MyResult<RawHtml<String>> { let AcceptLanguage(lang) = lang; let flash = None; - let invites = list_invites(&session.0, database)?; + let invites = list_invites(&session.0)?; let last_import_err = get_import_errors(&session.0).await; let busy = if is_importing() { Some("An import is currently running.") - } else if is_transcoding() { - Some("Currently transcoding posters.") } else { None }; @@ -73,13 +65,8 @@ pub async fn r_admin_dashboard( } #[post("/admin/generate_invite")] -pub async fn r_admin_invite( - _session: A<AdminSession>, - database: &State<Database>, -) -> MyResult<Redirect> { - let i = format!("{}", rand::rng().random::<u128>()); - database.create_invite(&i)?; - // admin_dashboard(database, Some(Ok(format!("Invite: {}", i)))).await +pub async fn r_admin_invite(session: A<AdminSession>) -> MyResult<Redirect> { + let _ = create_invite(&session.0)?; Ok(Redirect::temporary(u_admin_dashboard())) } @@ -91,97 +78,20 @@ pub struct DeleteInvite { #[post("/admin/remove_invite", data = "<form>")] pub async fn r_admin_remove_invite( session: A<AdminSession>, - database: &State<Database>, form: Form<DeleteInvite>, ) -> MyResult<Redirect> { - drop(session); - if !database.delete_invite(&form.invite)? { - Err(anyhow!("invite does not exist"))?; - }; - // admin_dashboard(database, Some(Ok("Invite invalidated".into()))).await + delete_invite(&session.0, &form.invite)?; Ok(Redirect::temporary(u_admin_dashboard())) } #[post("/admin/import?<incremental>")] -pub async fn r_admin_import( - session: A<AdminSession>, - database: &State<Database>, - incremental: bool, -) -> MyResult<Redirect> { - drop(session); - let t = Instant::now(); - if !incremental { - database.clear_nodes()?; - } - let r = import_wrap((*database).clone(), incremental).await; - // let flash = r - // .map_err(|e| e.into()) - // .map(|_| format!("Import successful; took {:?}", t.elapsed())); - // admin_dashboard(database, Some(flash)).await +pub async fn r_admin_import(session: A<AdminSession>, incremental: bool) -> MyResult<Redirect> { + do_import(&session.0, incremental).await?.1?; Ok(Redirect::temporary(u_admin_dashboard())) } #[post("/admin/update_search")] -pub async fn r_admin_update_search( - _session: A<AdminSession>, - database: &State<Database>, -) -> MyResult<Redirect> { - let db2 = (*database).clone(); - let r = spawn_blocking(move || db2.search_create_index()) - .await - .unwrap(); - // admin_dashboard( - // database, - // Some( - // r.map_err(|e| e.into()) - // .map(|_| "Search index updated".to_string()), - // ), - // ) - // .await - Ok(Redirect::temporary(u_admin_dashboard())) -} - -static SEM_TRANSCODING: Semaphore = Semaphore::const_new(1); -fn is_transcoding() -> bool { - SEM_TRANSCODING.available_permits() == 0 -} - -#[post("/admin/transcode_posters")] -pub async fn r_admin_transcode_posters( - session: A<AdminSession>, - database: &State<Database>, -) -> MyResult<Redirect> { - drop(session); - let _permit = SEM_TRANSCODING - .try_acquire() - .context("transcoding in progress")?; - - let t = Instant::now(); - - { - let nodes = database.list_nodes_with_udata("")?; - for (node, _) in nodes { - if let Some(poster) = &node.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, - // Some(Ok(format!( - // "All posters pre-transcoded; took {:?}", - // t.elapsed() - // ))), - // ) - // .await +pub async fn r_admin_update_search(session: A<AdminSession>) -> MyResult<Redirect> { + update_search_index(&session.0).await?; Ok(Redirect::temporary(u_admin_dashboard())) } diff --git a/server/src/ui/admin/user.rs b/server/src/ui/admin/user.rs index 939ee83..27d5256 100644 --- a/server/src/ui/admin/user.rs +++ b/server/src/ui/admin/user.rs @@ -7,25 +7,24 @@ use crate::{ helper::{language::AcceptLanguage, A}, ui::error::MyResult, }; -use anyhow::{anyhow, Context}; +use anyhow::Context; use jellycommon::user::UserPermission; use jellyimport::is_importing; -use jellylogic::{admin::user::admin_users, session::AdminSession, Database}; +use jellylogic::{ + admin::user::{admin_users, delete_user, get_user, update_user_perms, GrantState}, + session::AdminSession, +}; use jellyui::{ admin::user::{AdminUserPage, AdminUsersPage}, render_page, scaffold::{RenderInfo, SessionInfo}, }; -use rocket::{form::Form, get, post, response::content::RawHtml, FromForm, FromFormField, State}; +use rocket::{form::Form, get, post, response::content::RawHtml, FromForm, FromFormField}; #[get("/admin/users")] -pub fn r_admin_users( - session: A<AdminSession>, - database: &State<Database>, - lang: AcceptLanguage, -) -> MyResult<RawHtml<String>> { +pub fn r_admin_users(session: A<AdminSession>, lang: AcceptLanguage) -> MyResult<RawHtml<String>> { let AcceptLanguage(lang) = lang; - let r = admin_users(database, &session.0)?; + let r = admin_users(&session.0)?; Ok(RawHtml(render_page( &AdminUsersPage { flash: None, @@ -45,14 +44,11 @@ pub fn r_admin_users( #[get("/admin/user/<name>")] pub fn r_admin_user<'a>( session: A<AdminSession>, - database: &State<Database>, name: &'a str, lang: AcceptLanguage, ) -> MyResult<RawHtml<String>> { let AcceptLanguage(lang) = lang; - let user = database - .get_user(&name)? - .ok_or(anyhow!("user does not exist"))?; + let user = get_user(&session.0, name)?; Ok(RawHtml(render_page( &AdminUserPage { @@ -73,11 +69,11 @@ pub fn r_admin_user<'a>( #[derive(FromForm)] pub struct UserPermissionForm { permission: String, - action: GrantState, + action: UrlGrantState, } #[derive(FromFormField)] -pub enum GrantState { +pub enum UrlGrantState { Grant, Revoke, Unset, @@ -86,7 +82,6 @@ pub enum GrantState { #[post("/admin/user/<name>/update_permission", data = "<form>")] pub fn r_admin_user_permission( session: A<AdminSession>, - database: &State<Database>, form: Form<UserPermissionForm>, name: &str, lang: AcceptLanguage, @@ -95,18 +90,18 @@ pub fn r_admin_user_permission( let perm = serde_json::from_str::<UserPermission>(&form.permission) .context("parsing provided permission")?; - database.update_user(name, |user| { + update_user_perms( + &session.0, + name, + perm, 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(()) - })?; + UrlGrantState::Grant => GrantState::Grant, + UrlGrantState::Revoke => GrantState::Revoke, + UrlGrantState::Unset => GrantState::Unset, + }, + )?; - let user = database - .get_user(&name)? - .ok_or(anyhow!("user does not exist"))?; + let user = get_user(&session.0, name)?; Ok(RawHtml(render_page( &AdminUserPage { @@ -127,15 +122,12 @@ pub fn r_admin_user_permission( #[post("/admin/<name>/remove")] pub fn r_admin_remove_user( session: A<AdminSession>, - database: &State<Database>, name: &str, lang: AcceptLanguage, ) -> MyResult<RawHtml<String>> { let AcceptLanguage(lang) = lang; - if !database.delete_user(&name)? { - Err(anyhow!("user did not exist"))?; - } - let r = admin_users(database, &session.0)?; + delete_user(&session.0, name)?; + let r = admin_users(&session.0)?; Ok(RawHtml(render_page( &AdminUsersPage { diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs index 4e09417..97fd9c7 100644 --- a/server/src/ui/assets.rs +++ b/server/src/ui/assets.rs @@ -4,13 +4,19 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::{helper::{cache::CacheControlFile, A}, CONF}; +use crate::{ + helper::{cache::CacheControlFile, A}, + CONF, +}; use anyhow::{anyhow, bail, Context}; -use jellycommon::{LocalTrack, NodeID, PeopleGroup, SourceTrackKind, TrackSource}; +use jellycommon::{NodeID, PeopleGroup}; use jellyimport::asset_token::AssetInner; -use jellylogic::{session::Session, Database}; +use jellylogic::{ + assets::{get_node_backdrop, get_node_person_asset, get_node_poster, get_node_thumbnail}, + session::Session, +}; use log::info; -use rocket::{get, http::ContentType, response::Redirect, State}; +use rocket::{get, http::ContentType, response::Redirect}; use std::path::PathBuf; pub const AVIF_QUALITY: f32 = 50.; @@ -25,7 +31,6 @@ pub async fn r_asset( let width = width.unwrap_or(2048); let asset = AssetInner::deser(token)?; - let path = // if let AssetInner::Federated { host, asset } = asset { // let session = fed.get_session(&host).await?; @@ -35,7 +40,7 @@ pub async fn r_asset( // }) // .await? // } else - { + let path = { let source = resolve_asset(asset).await.context("resolving asset")?; // fit the resolution into a finite set so the maximum cache is finite too. @@ -62,136 +67,44 @@ pub async fn resolve_asset(asset: AssetInner) -> anyhow::Result<PathBuf> { #[get("/n/<id>/poster?<width>")] pub async fn r_item_poster( - _session: A<Session>, - db: &State<Database>, + session: A<Session>, id: A<NodeID>, width: Option<usize>, ) -> MyResult<Redirect> { - // TODO perm - let node = db.get_node(id.0)?.ok_or(anyhow!("node does not exist"))?; - - let mut asset = node.poster.clone(); - if asset.is_none() { - 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(|| { - AssetInner::Assets(format!("fallback-{:?}.avif", node.kind).into()).ser() - }); + let asset = get_node_poster(&session.0, id.0)?; Ok(Redirect::permanent(rocket::uri!(r_asset(asset.0, width)))) } #[get("/n/<id>/backdrop?<width>")] pub async fn r_item_backdrop( - _session: A<Session>, - db: &State<Database>, + session: A<Session>, id: A<NodeID>, width: Option<usize>, ) -> MyResult<Redirect> { - // TODO perm - let node = db.get_node(id.0)?.ok_or(anyhow!("node does not exist"))?; - - let mut asset = node.backdrop.clone(); - if asset.is_none() { - 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(|| { - AssetInner::Assets(format!("fallback-{:?}.avif", node.kind).into()).ser() - }); + let asset = get_node_backdrop(&session.0, id.0)?; Ok(Redirect::permanent(rocket::uri!(r_asset(asset.0, width)))) } #[get("/n/<id>/person/<index>/asset?<group>&<width>")] pub async fn r_person_asset( - _session: A<Session>, - db: &State<Database>, + session: A<Session>, id: A<NodeID>, index: usize, group: String, width: Option<usize>, ) -> MyResult<Redirect> { - // TODO perm - - let node = db.get_node(id.0)?.ok_or(anyhow!("node does not exist"))?; - let app = node - .people - .get(&PeopleGroup::from_str_opt(&group).ok_or(anyhow!("unknown people group"))?) - .ok_or(anyhow!("group has no members"))? - .get(index) - .ok_or(anyhow!("person does not exist"))?; - - let asset = app - .person - .headshot - .to_owned() - .unwrap_or(AssetInner::Assets("fallback-Person.avif".into()).ser()); + let group = PeopleGroup::from_str_opt(&group).ok_or(anyhow!("unknown people group"))?; + let asset = get_node_person_asset(&session.0, id.0, group, index)?; Ok(Redirect::permanent(rocket::uri!(r_asset(asset.0, width)))) } #[get("/n/<id>/thumbnail?<t>&<width>")] pub async fn r_node_thumbnail( - _session: A<Session>, - db: &State<Database>, + session: A<Session>, id: A<NodeID>, t: f64, width: Option<usize>, ) -> MyResult<Redirect> { - let node = db.get_node(id.0)?.ok_or(anyhow!("node does not exist"))?; - - let media = node.media.as_ref().ok_or(anyhow!("no media"))?; - let (thumb_track_index, _thumb_track) = media - .tracks - .iter() - .enumerate() - .find(|(_i, t)| matches!(t.kind, SourceTrackKind::Video { .. })) - .ok_or(anyhow!("no video track to create a thumbnail of"))?; - let source = media - .tracks - .get(thumb_track_index) - .ok_or(anyhow!("no source"))?; - let thumb_track_source = source.source.clone(); - - if t < 0. || t > media.duration { - Err(anyhow!("thumbnail instant not within media duration"))? - } - - let step = 8.; - let t = (t / step).floor() * step; - - let asset = match thumb_track_source { - TrackSource::Local(a) => { - let AssetInner::LocalTrack(LocalTrack { path, .. }) = AssetInner::deser(&a.0)? else { - return Err(anyhow!("track set to wrong asset type").into()); - }; - // the track selected might be different from thumb_track - jellytranscoder::thumbnail::create_thumbnail(&path, t).await? - } - TrackSource::Remote(_) => { - // // TODO in the new system this is preferrably a property of node ext for regular fed - // let session = fed - // .get_session( - // thumb_track - // .federated - // .last() - // .ok_or(anyhow!("federation broken"))?, - // ) - // .await?; - - // async_cache_file("fed-thumb", (id.0, t as i64), |out| { - // session.node_thumbnail(out, id.0.into(), 2048, t) - // }) - // .await? - todo!() - } - }; - - Ok(Redirect::temporary(rocket::uri!(r_asset( - AssetInner::Cache(asset).ser().0, - width - )))) + let asset = get_node_thumbnail(&session.0, id.0, t).await?; + Ok(Redirect::temporary(rocket::uri!(r_asset(asset.0, width)))) } diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs index 555b654..4a423cf 100644 --- a/server/src/ui/home.rs +++ b/server/src/ui/home.rs @@ -8,24 +8,23 @@ use super::error::MyResult; use crate::helper::{accept::AcceptJson, language::AcceptLanguage, A}; use jellycommon::api::ApiHomeResponse; use jellyimport::is_importing; -use jellylogic::{session::Session, Database}; +use jellylogic::session::Session; use jellyui::{ home::HomePage, render_page, scaffold::{RenderInfo, SessionInfo}, }; -use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either}; #[get("/home")] pub fn r_home( session: A<Session>, - db: &State<Database>, aj: AcceptJson, lang: AcceptLanguage, ) -> MyResult<Either<RawHtml<String>, Json<ApiHomeResponse>>> { let AcceptLanguage(lang) = lang; - let r = jellylogic::home::home(&db, &session.0)?; + let r = jellylogic::home::home(&session.0)?; Ok(if *aj { Either::Right(Json(r)) diff --git a/server/src/ui/items.rs b/server/src/ui/items.rs index ed16c61..1ac2c09 100644 --- a/server/src/ui/items.rs +++ b/server/src/ui/items.rs @@ -7,18 +7,17 @@ use super::error::MyError; use crate::helper::{accept::AcceptJson, language::AcceptLanguage, A}; use jellycommon::api::{ApiItemsResponse, NodeFilterSort}; use jellyimport::is_importing; -use jellylogic::{items::all_items, session::Session, Database}; +use jellylogic::{items::all_items, session::Session}; use jellyui::{ items::ItemsPage, render_page, scaffold::{RenderInfo, SessionInfo}, }; -use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either}; #[get("/items?<page>&<filter..>")] pub fn r_items( session: A<Session>, - db: &State<Database>, aj: AcceptJson, page: Option<usize>, filter: A<NodeFilterSort>, @@ -26,7 +25,7 @@ pub fn r_items( ) -> Result<Either<RawHtml<String>, Json<ApiItemsResponse>>, MyError> { let AcceptLanguage(lang) = lang; - let r = all_items(db, &session.0, page, filter.0.clone())?; + let r = all_items(&session.0, page, filter.0.clone())?; Ok(if *aj { Either::Right(Json(r)) diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs index 00445a9..0b1a92f 100644 --- a/server/src/ui/node.rs +++ b/server/src/ui/node.rs @@ -10,19 +10,18 @@ use jellycommon::{ NodeID, }; use jellyimport::is_importing; -use jellylogic::{node::get_node, session::Session, Database}; +use jellylogic::{node::get_node, session::Session}; use jellyui::{ node_page::NodePage, render_page, scaffold::{RenderInfo, SessionInfo}, }; -use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either}; #[get("/n/<id>?<parents>&<children>&<filter..>")] pub async fn r_node<'a>( session: A<Session>, id: A<NodeID>, - db: &'a State<Database>, aj: AcceptJson, filter: Option<A<NodeFilterSort>>, lang: AcceptLanguage, @@ -33,9 +32,8 @@ pub async fn r_node<'a>( let filter = filter.unwrap_or_default(); let r = get_node( - &db, - id.0, &session.0, + id.0, !*aj || children, !*aj || parents, filter.0.clone(), diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs index 1fd9e07..ae4468d 100644 --- a/server/src/ui/player.rs +++ b/server/src/ui/player.rs @@ -15,7 +15,7 @@ use jellycommon::{ NodeID, }; use jellyimport::is_importing; -use jellylogic::{node::get_node, session::Session, Database}; +use jellylogic::{node::get_node, session::Session}; use jellyui::{ node_page::NodePage, render_page, @@ -24,7 +24,7 @@ use jellyui::{ use rocket::{ get, response::{content::RawHtml, Redirect}, - Either, State, + Either, }; use std::time::Duration; @@ -46,20 +46,12 @@ fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: & pub fn r_player( session: A<Session>, lang: AcceptLanguage, - db: &State<Database>, t: Option<f64>, id: A<NodeID>, ) -> MyResult<Either<RawHtml<String>, Redirect>> { let AcceptLanguage(lang) = lang; - let r = get_node( - &db, - id.0, - &session.0, - false, - true, - NodeFilterSort::default(), - )?; + let r = get_node(&session.0, id.0, false, true, NodeFilterSort::default())?; let native_session = |action: &str| { Ok(Either::Right(Redirect::temporary(jellynative_url( diff --git a/server/src/ui/search.rs b/server/src/ui/search.rs index 750c8bd..e4afdd8 100644 --- a/server/src/ui/search.rs +++ b/server/src/ui/search.rs @@ -8,18 +8,17 @@ use crate::helper::{accept::AcceptJson, language::AcceptLanguage, A}; use anyhow::anyhow; use jellycommon::api::ApiSearchResponse; use jellyimport::is_importing; -use jellylogic::{search::search, session::Session, Database}; +use jellylogic::{search::search, session::Session}; use jellyui::{ render_page, scaffold::{RenderInfo, SessionInfo}, search::SearchPage, }; -use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either}; #[get("/search?<query>&<page>")] pub async fn r_search<'a>( session: A<Session>, - db: &State<Database>, aj: AcceptJson, query: Option<&str>, page: Option<usize>, @@ -28,7 +27,7 @@ pub async fn r_search<'a>( let AcceptLanguage(lang) = lang; let r = query - .map(|query| search(db, &session.0, query, page)) + .map(|query| search(&session.0, query, page)) .transpose()?; Ok(if *aj { diff --git a/server/src/ui/stats.rs b/server/src/ui/stats.rs index b6e9321..4ae592e 100644 --- a/server/src/ui/stats.rs +++ b/server/src/ui/stats.rs @@ -7,23 +7,22 @@ use super::error::MyError; use crate::helper::{accept::AcceptJson, language::AcceptLanguage, A}; use jellycommon::api::ApiStatsResponse; use jellyimport::is_importing; -use jellylogic::{session::Session, stats::stats, Database}; +use jellylogic::{session::Session, stats::stats}; use jellyui::{ render_page, scaffold::{RenderInfo, SessionInfo}, stats::StatsPage, }; -use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either}; #[get("/stats")] pub fn r_stats( session: A<Session>, - db: &State<Database>, aj: AcceptJson, lang: AcceptLanguage, ) -> Result<Either<RawHtml<String>, Json<ApiStatsResponse>>, MyError> { let AcceptLanguage(lang) = lang; - let r = stats(db, &session.0)?; + let r = stats(&session.0)?; Ok(if *aj { Either::Right(Json(r)) diff --git a/stream/src/hls.rs b/stream/src/hls.rs index 0ca7545..41b896b 100644 --- a/stream/src/hls.rs +++ b/stream/src/hls.rs @@ -6,7 +6,9 @@ use crate::{stream_info, SMediaInfo}; use anyhow::{anyhow, Result}; -use jellycommon::stream::{FormatNum, SegmentNum, StreamContainer, StreamSpec, TrackKind, TrackNum}; +use jellycommon::stream::{ + FormatNum, SegmentNum, StreamContainer, StreamSpec, TrackKind, TrackNum, +}; use std::{fmt::Write, ops::Range, sync::Arc}; use tokio::{ io::{AsyncWriteExt, DuplexStream}, diff --git a/ui/src/admin/mod.rs b/ui/src/admin/mod.rs index ade0d97..5898f45 100644 --- a/ui/src/admin/mod.rs +++ b/ui/src/admin/mod.rs @@ -10,7 +10,7 @@ pub mod user; use crate::{Page, locale::Language, scaffold::FlashDisplay}; use jellycommon::routes::{ u_admin_import, u_admin_invite_create, u_admin_invite_remove, u_admin_log, - u_admin_transcode_posters, u_admin_update_search, u_admin_users, + u_admin_update_search, u_admin_users, }; impl Page for AdminDashboardPage<'_> { @@ -50,9 +50,6 @@ markup::define!( form[method="POST", action=u_admin_import(false)] { input[type="submit", disabled=busy.is_some(), value="Start full import"]; } - form[method="POST", action=u_admin_transcode_posters()] { - input[type="submit", disabled=busy.is_some(), value="Transcode all posters with low resolution"]; - } form[method="POST", action=u_admin_update_search()] { input[type="submit", value="Regenerate full-text search index"]; } diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 0e7547e..9a81692 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -5,6 +5,7 @@ */ pub mod account; pub mod admin; +pub mod error; pub mod filter_sort; pub mod format; pub mod home; @@ -16,7 +17,6 @@ pub mod props; pub mod scaffold; pub mod search; pub mod stats; -pub mod error; use jellycommon::user::Theme; use locale::Language; |