aboutsummaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-28 18:27:03 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-28 18:27:03 +0200
commit51761cbdefa39107b9e1f931f1aa8df6aebb2a94 (patch)
tree957ca180786ece777e6e1153ada91da741d845ec /server/src
parent80d28b764c95891551e28c395783f5ff9d065743 (diff)
downloadjellything-51761cbdefa39107b9e1f931f1aa8df6aebb2a94.tar
jellything-51761cbdefa39107b9e1f931f1aa8df6aebb2a94.tar.bz2
jellything-51761cbdefa39107b9e1f931f1aa8df6aebb2a94.tar.zst
many much more generic refactor
Diffstat (limited to 'server/src')
-rw-r--r--server/src/logic/session.rs94
-rw-r--r--server/src/ui/account/settings.rs52
-rw-r--r--server/src/ui/browser.rs25
-rw-r--r--server/src/ui/home.rs10
-rw-r--r--server/src/ui/mod.rs1
-rw-r--r--server/src/ui/node.rs46
-rw-r--r--server/src/ui/search.rs16
-rw-r--r--server/src/ui/sort.rs103
-rw-r--r--server/src/ui/stats.rs68
9 files changed, 20 insertions, 395 deletions
diff --git a/server/src/logic/session.rs b/server/src/logic/session.rs
index 790e070..d77c4fc 100644
--- a/server/src/logic/session.rs
+++ b/server/src/logic/session.rs
@@ -13,6 +13,7 @@ use base64::Engine;
use chrono::{DateTime, Duration, Utc};
use jellybase::{database::Database, SECRETS};
use jellycommon::user::{PermissionSet, User};
+use jellylogic::session::validate;
use log::warn;
use rocket::{
async_trait,
@@ -24,19 +25,6 @@ use rocket::{
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
-pub struct Session {
- pub user: User,
-}
-
-pub struct AdminSession(pub Session);
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct SessionData {
- username: String,
- expire: DateTime<Utc>,
- permissions: PermissionSet,
-}
-
impl Session {
pub async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> {
let username;
@@ -126,83 +114,3 @@ impl<'r> FromRequest<'r> for AdminSession {
}
}
}
-
-static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| {
- if let Some(sk) = &SECRETS.session_key {
- let r = base64::engine::general_purpose::STANDARD
- .decode(sk)
- .expect("key invalid; should be valid base64");
- r.try_into()
- .expect("key has the wrong length; should be 32 bytes")
- } else {
- warn!("session_key not configured; generating a random one.");
- [(); 32].map(|_| rand::random())
- }
-});
-
-pub fn create(username: String, permissions: PermissionSet, expire: Duration) -> String {
- let session_data = SessionData {
- expire: Utc::now() + expire,
- username: username.to_owned(),
- permissions,
- };
- let mut plaintext =
- bincode::serde::encode_to_vec(&session_data, bincode::config::standard()).unwrap();
-
- while plaintext.len() % 16 == 0 {
- plaintext.push(0);
- }
-
- let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap();
- let nonce = [(); 12].map(|_| rand::random());
- let mut ciphertext = cipher
- .encrypt(&GenericArray::from(nonce), plaintext.as_slice())
- .unwrap();
- ciphertext.extend(nonce);
-
- base64::engine::general_purpose::URL_SAFE.encode(&ciphertext)
-}
-
-pub fn validate(token: &str) -> anyhow::Result<String> {
- let ciphertext = base64::engine::general_purpose::URL_SAFE.decode(token)?;
- let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap();
- let (ciphertext, nonce) = ciphertext.split_at(ciphertext.len() - 12);
- let plaintext = cipher
- .decrypt(nonce.into(), ciphertext)
- .map_err(|e| anyhow!("decryption failed: {e:?}"))?;
-
- let (session_data, _): (SessionData, _) =
- bincode::serde::decode_from_slice(&plaintext, bincode::config::standard())?;
-
- if session_data.expire < Utc::now() {
- Err(anyhow!("session expired"))?
- }
-
- Ok(session_data.username)
-}
-
-#[test]
-fn test() {
- jellybase::use_test_config();
- let tok = create(
- "blub".to_string(),
- jellycommon::user::PermissionSet::default(),
- Duration::days(1),
- );
- validate(&tok).unwrap();
-}
-
-#[test]
-fn test_crypto() {
- jellybase::use_test_config();
- let nonce = [(); 12].map(|_| rand::random());
- let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap();
- let plaintext = b"testing stuff---";
- let ciphertext = cipher
- .encrypt(&GenericArray::from(nonce), plaintext.as_slice())
- .unwrap();
- let plaintext2 = cipher
- .decrypt((&nonce).into(), ciphertext.as_slice())
- .unwrap();
- assert_eq!(plaintext, plaintext2.as_slice());
-}
diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs
index 4047e4f..3b53bc4 100644
--- a/server/src/ui/account/settings.rs
+++ b/server/src/ui/account/settings.rs
@@ -51,57 +51,7 @@ fn settings_page(
LayoutPage {
title: "Settings".to_string(),
class: Some("settings"),
- content: markup::new! {
- h1 { "Settings" }
- @if let Some(flash) = &flash {
- @match flash {
- Ok(mesg) => { section.message { p.success { @mesg } } }
- Err(err) => { section.message { p.error { @format!("{err}") } } }
- }
- }
- h2 { @trs(&lang, "account") }
- a.switch_account[href=uri!(r_account_login())] { "Switch Account" }
- form[method="POST", action=uri!(r_account_settings_post())] {
- label[for="username"] { @trs(&lang, "account.username") }
- input[type="text", id="username", disabled, value=&session.user.name];
- input[type="submit", disabled, value=&*tr(lang, "settings.immutable")];
- }
- form[method="POST", action=uri!(r_account_settings_post())] {
- label[for="display_name"] { @trs(&lang, "account.display_name") }
- input[type="text", id="display_name", name="display_name", value=&session.user.display_name];
- input[type="submit", value=&*tr(lang, "settings.update")];
- }
- form[method="POST", action=uri!(r_account_settings_post())] {
- label[for="password"] { @trs(&lang, "account.password") }
- input[type="password", id="password", name="password"];
- input[type="submit", value=&*tr(lang, "settings.update")];
- }
- h2 { @trs(&lang, "settings.appearance") }
- form[method="POST", action=uri!(r_account_settings_post())] {
- fieldset {
- legend { @trs(&lang, "settings.appearance.theme") }
- @for (t, tlabel) in Theme::LIST {
- label { input[type="radio", name="theme", value=A(*t), checked=session.user.theme==*t]; @tlabel } br;
- }
- }
- input[type="submit", value=&*tr(lang, "settings.apply")];
- }
- form[method="POST", action=uri!(r_account_settings_post())] {
- fieldset {
- legend { @trs(&lang, "settings.player_preference") }
- @for (t, tlabel) in PlayerKind::LIST {
- label { input[type="radio", name="player_preference", value=A(*t), checked=session.user.player_preference==*t]; @tlabel } br;
- }
- }
- input[type="submit", value=&*tr(lang, "settings.apply")];
- }
- form[method="POST", action=uri!(r_account_settings_post())] {
- label[for="native_secret"] { "Native Secret" }
- input[type="password", id="native_secret", name="native_secret"];
- input[type="submit", value=&*tr(lang, "settings.update")];
- p { "The secret can be found in " code{"$XDG_CONFIG_HOME/jellynative_secret"} " or by clicking " a.button[href="jellynative://show-secret-v1"] { "Show Secret" } "." }
- }
- },
+ content:
}
}
diff --git a/server/src/ui/browser.rs b/server/src/ui/browser.rs
index f7eac93..b780934 100644
--- a/server/src/ui/browser.rs
+++ b/server/src/ui/browser.rs
@@ -30,29 +30,10 @@ pub fn r_all_items_filter(
lang: AcceptLanguage,
) -> Result<Either<DynLayoutPage<'_>, Json<ApiItemsResponse>>, MyError> {
let AcceptLanguage(lang) = lang;
- let mut items = db.list_nodes_with_udata(sess.user.name.as_str())?;
-
- items.retain(|(n, _)| matches!(n.visibility, Visibility::Visible));
-
- filter_and_sort_nodes(
- &filter,
- (SortProperty::Title, SortOrder::Ascending),
- &mut items,
- );
-
- let page_size = 100;
- let page = page.unwrap_or(0);
- let offset = page * page_size;
- let from = offset.min(items.len());
- let to = (offset + page_size).min(items.len());
- let max_page = items.len().div_ceil(page_size);
-
+
+ let data = all_items()?;
Ok(if *aj {
- Either::Right(Json(ApiItemsResponse {
- count: items.len(),
- pages: max_page,
- items: items[from..to].to_vec(),
- }))
+ Either::Right(Json(data))
} else {
Either::Left(LayoutPage {
title: "All Items".to_owned(),
diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs
index 96b1dc2..2a79965 100644
--- a/server/src/ui/home.rs
+++ b/server/src/ui/home.rs
@@ -18,14 +18,12 @@ pub fn r_home(
lang: AcceptLanguage,
) -> MyResult<Either<DynLayoutPage, Json<ApiHomeResponse>>> {
let AcceptLanguage(lang) = lang;
-
+
+ let resp = jellylogic::home::home(&db, sess)?;
Ok(if *aj {
- Either::Right(Json(ApiHomeResponse {
- toplevel,
- categories,
- }))
+ Either::Right(Json(resp))
} else {
- Either::Left()
+ Either::Left(jellyui::home::home_page(resp))
})
}
diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs
index 89c0e9a..6728b81 100644
--- a/server/src/ui/mod.rs
+++ b/server/src/ui/mod.rs
@@ -38,7 +38,6 @@ pub mod home;
pub mod node;
pub mod player;
pub mod search;
-pub mod sort;
pub mod stats;
pub mod style;
diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs
index 1efcc10..5d0f1ff 100644
--- a/server/src/ui/node.rs
+++ b/server/src/ui/node.rs
@@ -92,49 +92,3 @@ pub async fn r_library_node_filter<'a>(
})
})
}
-
-pub fn get_similar_media(
- node: &Node,
- db: &Database,
- session: &Session,
-) -> 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 weight = 1_000_000 / nodes.len();
- for n in nodes {
- if n != this_id {
- *ranking.entry(n).or_default() += weight;
- }
- }
- }
- let mut ranking = ranking.into_iter().collect::<Vec<_>>();
- ranking.sort_by_key(|(_, k)| Reverse(*k));
- ranking
- .into_iter()
- .take(32)
- .map(|(pid, _)| db.get_node_with_userdata(pid, session))
- .collect::<anyhow::Result<Vec<_>>>()
-}
-
-pub trait DatabaseNodeUserDataExt {
- fn get_node_with_userdata(
- &self,
- id: NodeID,
- session: &Session,
- ) -> Result<(Arc<Node>, NodeUserData)>;
-}
-impl DatabaseNodeUserDataExt for Database {
- fn get_node_with_userdata(
- &self,
- id: NodeID,
- session: &Session,
- ) -> Result<(Arc<Node>, NodeUserData)> {
- Ok((
- self.get_node(id)?.ok_or(anyhow!("node does not exist"))?,
- self.get_node_udata(id, &session.user.name)?
- .unwrap_or_default(),
- ))
- }
-}
diff --git a/server/src/ui/search.rs b/server/src/ui/search.rs
index bfe51a8..51fdcb8 100644
--- a/server/src/ui/search.rs
+++ b/server/src/ui/search.rs
@@ -25,21 +25,7 @@ pub async fn r_search<'a>(
lang: AcceptLanguage,
) -> MyResult<Either<DynLayoutPage<'a>, Json<ApiSearchResponse>>> {
let AcceptLanguage(lang) = lang;
- let results = if let Some(query) = query {
- let timing = Instant::now();
- let (count, ids) = db.search(query, 32, page.unwrap_or_default() * 32)?;
- let mut nodes = ids
- .into_iter()
- .map(|id| db.get_node_with_userdata(id, &session))
- .collect::<Result<Vec<_>, anyhow::Error>>()?;
- nodes.retain(|(n, _)| n.visibility >= Visibility::Reduced);
- let search_dur = timing.elapsed();
- Some((count, nodes, search_dur))
- } else {
- None
- };
- let query = query.unwrap_or_default().to_string();
-
+
Ok(if *aj {
let Some((count, results, _)) = results else {
Err(anyhow!("no query"))?
diff --git a/server/src/ui/sort.rs b/server/src/ui/sort.rs
deleted file mode 100644
index 441bac6..0000000
--- a/server/src/ui/sort.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- 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 jellycommon::{
- api::{FilterProperty, NodeFilterSort, SortOrder, SortProperty},
- helpers::SortAnyway,
- user::NodeUserData,
- Node, NodeKind, Rating,
-};
-use rocket::{
- http::uri::fmt::{Query, UriDisplay},
- FromForm, FromFormField, UriDisplayQuery,
-};
-use std::sync::Arc;
-
-pub fn filter_and_sort_nodes(
- f: &NodeFilterSort,
- default_sort: (SortProperty, SortOrder),
- nodes: &mut Vec<(Arc<Node>, NodeUserData)>,
-) {
- let sort_prop = f.sort_by.unwrap_or(default_sort.0);
- nodes.retain(|(node, _udata)| {
- let mut o = true;
- if let Some(prop) = &f.filter_kind {
- o = false;
- for p in prop {
- o |= match p {
- // FilterProperty::FederationLocal => node.federated.is_none(),
- // FilterProperty::FederationRemote => node.federated.is_some(),
- FilterProperty::KindMovie => node.kind == NodeKind::Movie,
- FilterProperty::KindVideo => node.kind == NodeKind::Video,
- FilterProperty::KindShortFormVideo => node.kind == NodeKind::ShortFormVideo,
- FilterProperty::KindMusic => node.kind == NodeKind::Music,
- FilterProperty::KindCollection => node.kind == NodeKind::Collection,
- FilterProperty::KindChannel => node.kind == NodeKind::Channel,
- FilterProperty::KindShow => node.kind == NodeKind::Show,
- FilterProperty::KindSeries => node.kind == NodeKind::Series,
- FilterProperty::KindSeason => node.kind == NodeKind::Season,
- FilterProperty::KindEpisode => node.kind == NodeKind::Episode,
- // FilterProperty::Watched => udata.watched == WatchedState::Watched,
- // FilterProperty::Unwatched => udata.watched == WatchedState::None,
- // FilterProperty::WatchProgress => {
- // matches!(udata.watched, WatchedState::Progress(_))
- // }
- _ => false, // TODO
- }
- }
- }
- match sort_prop {
- SortProperty::ReleaseDate => o &= node.release_date.is_some(),
- SortProperty::Duration => o &= node.media.is_some(),
- _ => (),
- }
- o
- });
- match sort_prop {
- SortProperty::Duration => {
- nodes.sort_by_key(|(n, _)| (n.media.as_ref().unwrap().duration * 1000.) as i64)
- }
- SortProperty::ReleaseDate => {
- nodes.sort_by_key(|(n, _)| n.release_date.expect("asserted above"))
- }
- SortProperty::Title => nodes.sort_by(|(a, _), (b, _)| a.title.cmp(&b.title)),
- SortProperty::Index => nodes.sort_by(|(a, _), (b, _)| {
- a.index
- .unwrap_or(usize::MAX)
- .cmp(&b.index.unwrap_or(usize::MAX))
- }),
- SortProperty::RatingRottenTomatoes => nodes.sort_by_cached_key(|(n, _)| {
- SortAnyway(*n.ratings.get(&Rating::RottenTomatoes).unwrap_or(&0.))
- }),
- SortProperty::RatingMetacritic => nodes.sort_by_cached_key(|(n, _)| {
- SortAnyway(*n.ratings.get(&Rating::Metacritic).unwrap_or(&0.))
- }),
- SortProperty::RatingImdb => nodes
- .sort_by_cached_key(|(n, _)| SortAnyway(*n.ratings.get(&Rating::Imdb).unwrap_or(&0.))),
- SortProperty::RatingTmdb => nodes
- .sort_by_cached_key(|(n, _)| SortAnyway(*n.ratings.get(&Rating::Tmdb).unwrap_or(&0.))),
- SortProperty::RatingYoutubeViews => nodes.sort_by_cached_key(|(n, _)| {
- SortAnyway(*n.ratings.get(&Rating::YoutubeViews).unwrap_or(&0.))
- }),
- SortProperty::RatingYoutubeLikes => nodes.sort_by_cached_key(|(n, _)| {
- SortAnyway(*n.ratings.get(&Rating::YoutubeLikes).unwrap_or(&0.))
- }),
- SortProperty::RatingYoutubeFollowers => nodes.sort_by_cached_key(|(n, _)| {
- SortAnyway(*n.ratings.get(&Rating::YoutubeFollowers).unwrap_or(&0.))
- }),
- SortProperty::RatingLikesDivViews => nodes.sort_by_cached_key(|(n, _)| {
- SortAnyway(
- *n.ratings.get(&Rating::YoutubeLikes).unwrap_or(&0.)
- / (1. + *n.ratings.get(&Rating::YoutubeViews).unwrap_or(&0.)),
- )
- }),
- SortProperty::RatingUser => nodes.sort_by_cached_key(|(_, u)| u.rating),
- }
-
- match f.sort_order.unwrap_or(default_sort.1) {
- SortOrder::Ascending => (),
- SortOrder::Descending => nodes.reverse(),
- }
-}
diff --git a/server/src/ui/stats.rs b/server/src/ui/stats.rs
index 345586a..a91e670 100644
--- a/server/src/ui/stats.rs
+++ b/server/src/ui/stats.rs
@@ -8,21 +8,14 @@ use super::{
layout::{DynLayoutPage, LayoutPage},
};
use crate::{
- api::AcceptJson,
- database::Database,
- locale::AcceptLanguage,
- logic::session::Session,
- ui::{
- layout::trs,
- node::{
- format_duration, format_duration_long, format_kind, format_size,
- rocket_uri_macro_r_library_node,
- },
- },
- uri,
+ api::AcceptJson, database::Database, locale::AcceptLanguage, logic::session::Session, uri,
};
use jellybase::locale::tr;
-use jellycommon::{Node, NodeID, NodeKind, Visibility};
+use jellycommon::{
+ api::{ApiStatsResponse, StatsBin},
+ Node, NodeID, NodeKind, Visibility,
+};
+use jellylogic::stats::stats;
use markup::raw;
use rocket::{get, serde::json::Json, Either, State};
use serde::Serialize;
@@ -35,54 +28,13 @@ pub fn r_stats(
db: &State<Database>,
aj: AcceptJson,
lang: AcceptLanguage,
-) -> Result<Either<DynLayoutPage<'_>, Json<Value>>, MyError> {
+) -> Result<Either<DynLayoutPage<'_>, Json<ApiStatsResponse>>, MyError> {
let AcceptLanguage(lang) = lang;
- let mut items = db.list_nodes_with_udata(sess.user.name.as_str())?;
- items.retain(|(n, _)| n.visibility >= Visibility::Reduced);
-
- #[derive(Default, Serialize)]
- struct Bin {
- runtime: f64,
- size: u64,
- count: usize,
- max_runtime: (f64, String),
- max_size: (u64, String),
- }
- impl Bin {
- fn update(&mut self, node: &Node) {
- self.count += 1;
- self.size += node.storage_size;
- if node.storage_size > self.max_size.0 {
- self.max_size = (node.storage_size, node.slug.clone())
- }
- if let Some(m) = &node.media {
- self.runtime += m.duration;
- if m.duration > self.max_runtime.0 {
- self.max_runtime = (m.duration, node.slug.clone())
- }
- }
- }
- fn average_runtime(&self) -> f64 {
- self.runtime / self.count as f64
- }
- fn average_size(&self) -> f64 {
- self.size as f64 / self.count as f64
- }
- }
-
- let mut all = Bin::default();
- let mut kinds = BTreeMap::<NodeKind, Bin>::new();
- for (i, _) in items {
- all.update(&i);
- kinds.entry(i.kind).or_default().update(&i);
- }
+ let data = stats(db)?;
Ok(if *aj {
- Either::Right(Json(json!({
- "all": all,
- "kinds": kinds,
- })))
+ Either::Right(Json(data))
} else {
- Either::Left()
+ Either::Left(1)
})
}