/* 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 */ use anyhow::{Context, Result}; use jellybase::database::Database; use jellycommon::{ NodeID, NodeKind, Rating, Visibility, api::ApiHomeResponse, chrono::{Datelike, Utc}, user::WatchedState, }; pub fn home(db: Database) -> Result { 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::>>()?; 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(ApiHomeResponse { toplevel, categories, }) } 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 }