aboutsummaryrefslogtreecommitdiff
path: root/server/src/ui/home.rs
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/ui/home.rs')
-rw-r--r--server/src/ui/home.rs173
1 files changed, 173 insertions, 0 deletions
diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs
new file mode 100644
index 0000000..fbce99b
--- /dev/null
+++ b/server/src/ui/home.rs
@@ -0,0 +1,173 @@
+/*
+ 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 super::{
+ error::MyResult,
+ layout::{trs, DynLayoutPage, LayoutPage},
+ node::{DatabaseNodeUserDataExt, NodeCard},
+};
+use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage, logic::session::Session};
+use anyhow::Context;
+use chrono::{Datelike, Utc};
+use jellybase::{locale::tr, CONF};
+use jellycommon::{api::ApiHomeResponse, user::WatchedState, NodeID, NodeKind, Rating, Visibility};
+use rocket::{get, serde::json::Json, Either, State};
+
+#[get("/home")]
+pub fn r_home(
+ sess: Session,
+ db: &State<Database>,
+ aj: AcceptJson,
+ lang: AcceptLanguage,
+) -> MyResult<Either<DynLayoutPage, Json<ApiHomeResponse>>> {
+ let AcceptLanguage(lang) = lang;
+ let mut items = db.list_nodes_with_udata(&sess.user.name)?;
+
+ let mut toplevel = db
+ .get_node_children(NodeID::from_slug("library"))
+ .context("root node missing")?
+ .into_iter()
+ .map(|n| db.get_node_with_userdata(n, &sess))
+ .collect::<anyhow::Result<Vec<_>>>()?;
+ toplevel.sort_by_key(|(n, _)| n.index.unwrap_or(usize::MAX));
+
+ let mut categories = Vec::<(String, Vec<_>)>::new();
+
+ categories.push((
+ "home.bin.continue_watching".to_string(),
+ items
+ .iter()
+ .filter(|(_, u)| matches!(u.watched, WatchedState::Progress(_)))
+ .cloned()
+ .collect(),
+ ));
+ categories.push((
+ "home.bin.watchlist".to_string(),
+ items
+ .iter()
+ .filter(|(_, u)| matches!(u.watched, WatchedState::Pending))
+ .cloned()
+ .collect(),
+ ));
+
+ items.retain(|(n, _)| matches!(n.visibility, Visibility::Visible));
+
+ items.sort_by_key(|(n, _)| n.release_date.map(|d| -d).unwrap_or(i64::MAX));
+
+ categories.push((
+ "home.bin.latest_video".to_string(),
+ items
+ .iter()
+ .filter(|(n, _)| matches!(n.kind, NodeKind::Video))
+ .take(16)
+ .cloned()
+ .collect(),
+ ));
+ categories.push((
+ "home.bin.latest_music".to_string(),
+ items
+ .iter()
+ .filter(|(n, _)| matches!(n.kind, NodeKind::Music))
+ .take(16)
+ .cloned()
+ .collect(),
+ ));
+ categories.push((
+ "home.bin.latest_short_form".to_string(),
+ items
+ .iter()
+ .filter(|(n, _)| matches!(n.kind, NodeKind::ShortFormVideo))
+ .take(16)
+ .cloned()
+ .collect(),
+ ));
+
+ items.sort_by_key(|(n, _)| {
+ n.ratings
+ .get(&Rating::Tmdb)
+ .map(|x| (*x * -1000.) as i32)
+ .unwrap_or(0)
+ });
+
+ categories.push((
+ "home.bin.max_rating".to_string(),
+ items
+ .iter()
+ .take(16)
+ .filter(|(n, _)| n.ratings.contains_key(&Rating::Tmdb))
+ .cloned()
+ .collect(),
+ ));
+
+ items.retain(|(n, _)| {
+ matches!(
+ n.kind,
+ NodeKind::Video | NodeKind::Movie | NodeKind::Episode | NodeKind::Music
+ )
+ });
+
+ categories.push((
+ "home.bin.daily_random".to_string(),
+ (0..16)
+ .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone()))
+ .collect(),
+ ));
+
+ {
+ let mut items = items.clone();
+ items.retain(|(_, u)| matches!(u.watched, WatchedState::Watched));
+ categories.push((
+ "home.bin.watch_again".to_string(),
+ (0..16)
+ .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone()))
+ .collect(),
+ ));
+ }
+
+ items.retain(|(n, _)| matches!(n.kind, NodeKind::Music));
+ categories.push((
+ "home.bin.daily_random_music".to_string(),
+ (0..16)
+ .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone()))
+ .collect(),
+ ));
+
+ Ok(if *aj {
+ Either::Right(Json(ApiHomeResponse {
+ toplevel,
+ categories,
+ }))
+ } else {
+ Either::Left(LayoutPage {
+ title: tr(lang, "home").to_string(),
+ content: markup::new! {
+ h2 { @tr(lang, "home.bin.root").replace("{title}", &CONF.brand) }
+ ul.children.hlist {@for (node, udata) in &toplevel {
+ li { @NodeCard { node, udata, lang: &lang } }
+ }}
+ @for (name, nodes) in &categories {
+ @if !nodes.is_empty() {
+ h2 { @trs(&lang, &name) }
+ ul.children.hlist {@for (node, udata) in nodes {
+ li { @NodeCard { node, udata, lang: &lang } }
+ }}
+ }
+ }
+ },
+ ..Default::default()
+ })
+ })
+}
+
+fn cheap_daily_random(i: usize) -> usize {
+ xorshift(xorshift(Utc::now().num_days_from_ce() as u64) + i as u64) as usize
+}
+
+fn xorshift(mut x: u64) -> u64 {
+ x ^= x << 13;
+ x ^= x >> 7;
+ x ^= x << 17;
+ x
+}