/* 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 super::{error::MyResult, sort::filter_and_sort_nodes}; use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage, logic::session::Session}; use anyhow::{anyhow, Result}; use jellycommon::{ api::{ApiNodeResponse, NodeFilterSort, SortOrder, SortProperty}, user::NodeUserData, Node, NodeID, NodeKind, Visibility, }; use rocket::{get, serde::json::Json, Either, State}; use std::{cmp::Reverse, collections::BTreeMap, sync::Arc}; /// This function is a stub and only useful for use in the uri! macro. #[get("/n/")] pub fn r_library_node(id: NodeID) { let _ = id; } #[get("/n/?&&")] pub async fn r_library_node_filter<'a>( session: Session, id: NodeID, db: &'a State, aj: AcceptJson, filter: NodeFilterSort, lang: AcceptLanguage, parents: bool, children: bool, ) -> MyResult, Json>> { let AcceptLanguage(lang) = lang; let (node, udata) = db.get_node_with_userdata(id, &session)?; let mut children = if !*aj || children { db.get_node_children(id)? .into_iter() .map(|c| db.get_node_with_userdata(c, &session)) .collect::>>()? } else { Vec::new() }; let mut parents = if !*aj || parents { node.parents .iter() .map(|pid| db.get_node_with_userdata(*pid, &session)) .collect::>>()? } else { Vec::new() }; let mut similar = get_similar_media(&node, db, &session)?; similar.retain(|(n, _)| n.visibility >= Visibility::Reduced); children.retain(|(n, _)| n.visibility >= Visibility::Reduced); parents.retain(|(n, _)| n.visibility >= Visibility::Reduced); filter_and_sort_nodes( &filter, match node.kind { NodeKind::Channel => (SortProperty::ReleaseDate, SortOrder::Descending), NodeKind::Season | NodeKind::Show => (SortProperty::Index, SortOrder::Ascending), _ => (SortProperty::Title, SortOrder::Ascending), }, &mut children, ); Ok(if *aj { Either::Right(Json(ApiNodeResponse { children, parents, node, userdata: udata, })) } else { Either::Left(LayoutPage { title: node.title.clone().unwrap_or_default(), content: markup::new!(@NodePage { node: &node, udata: &udata, children: &children, parents: &parents, filter: &filter, player: false, similar: &similar, lang: &lang, }), ..Default::default() }) }) } pub fn get_similar_media( node: &Node, db: &Database, session: &Session, ) -> Result, NodeUserData)>> { let this_id = NodeID::from_slug(&node.slug); let mut ranking = BTreeMap::::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::>(); ranking.sort_by_key(|(_, k)| Reverse(*k)); ranking .into_iter() .take(32) .map(|(pid, _)| db.get_node_with_userdata(pid, session)) .collect::>>() } pub trait DatabaseNodeUserDataExt { fn get_node_with_userdata( &self, id: NodeID, session: &Session, ) -> Result<(Arc, NodeUserData)>; } impl DatabaseNodeUserDataExt for Database { fn get_node_with_userdata( &self, id: NodeID, session: &Session, ) -> Result<(Arc, NodeUserData)> { Ok(( self.get_node(id)?.ok_or(anyhow!("node does not exist"))?, self.get_node_udata(id, &session.user.name)? .unwrap_or_default(), )) } }