aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-05-26 18:24:16 +0200
committermetamuffin <metamuffin@disroot.org>2025-05-26 18:24:16 +0200
commit3b15caade07e8fbe351fed9aceb3f435bf58368e (patch)
treecce91c229b78061ad36f29d76a76d67c3c737c59
parent1eeff5c03e8985d16d4f2b6283741dd82b369bd3 (diff)
downloadjellything-3b15caade07e8fbe351fed9aceb3f435bf58368e.tar
jellything-3b15caade07e8fbe351fed9aceb3f435bf58368e.tar.bz2
jellything-3b15caade07e8fbe351fed9aceb3f435bf58368e.tar.zst
move all direct database access to logic crate
-rw-r--r--Cargo.lock1
-rw-r--r--common/src/config.rs1
-rw-r--r--common/src/routes.rs3
-rw-r--r--logic/Cargo.toml1
-rw-r--r--logic/src/account.rs60
-rw-r--r--logic/src/admin/mod.rs40
-rw-r--r--logic/src/admin/user.rs46
-rw-r--r--logic/src/assets.rs131
-rw-r--r--logic/src/home.rs11
-rw-r--r--logic/src/items.rs6
-rw-r--r--logic/src/lib.rs17
-rw-r--r--logic/src/login.rs10
-rw-r--r--logic/src/node.rs85
-rw-r--r--logic/src/search.rs14
-rw-r--r--logic/src/session.rs19
-rw-r--r--logic/src/stats.rs7
-rw-r--r--server/src/api.rs26
-rw-r--r--server/src/compat/jellyfin/mod.rs296
-rw-r--r--server/src/compat/jellyfin/models.rs8
-rw-r--r--server/src/compat/youtube.rs37
-rw-r--r--server/src/config.rs3
-rw-r--r--server/src/helper/session.rs21
-rw-r--r--server/src/logic/mod.rs1
-rw-r--r--server/src/logic/stream.rs20
-rw-r--r--server/src/logic/userdata.rs59
-rw-r--r--server/src/main.rs11
-rw-r--r--server/src/routes.rs7
-rw-r--r--server/src/ui/account/mod.rs24
-rw-r--r--server/src/ui/account/settings.rs64
-rw-r--r--server/src/ui/admin/mod.rs120
-rw-r--r--server/src/ui/admin/user.rs54
-rw-r--r--server/src/ui/assets.rs129
-rw-r--r--server/src/ui/home.rs7
-rw-r--r--server/src/ui/items.rs7
-rw-r--r--server/src/ui/node.rs8
-rw-r--r--server/src/ui/player.rs14
-rw-r--r--server/src/ui/search.rs7
-rw-r--r--server/src/ui/stats.rs7
-rw-r--r--stream/src/hls.rs4
-rw-r--r--ui/src/admin/mod.rs5
-rw-r--r--ui/src/lib.rs2
41 files changed, 728 insertions, 665 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1fc8ceb..332b29c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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;