/* 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 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, WatchedState}, }; use jellydb::Database; use std::{cmp::Reverse, collections::BTreeMap, sync::Arc}; pub fn get_node( session: &Session, id: NodeID, children: bool, parents: bool, filter: NodeFilterSort, ) -> Result { let (node, udata) = DATABASE.get_node_with_userdata(id, &session)?; let mut children = if children { DATABASE .get_node_children(id)? .into_iter() .map(|c| DATABASE.get_node_with_userdata(c, &session)) .collect::>>()? } else { Vec::new() }; let mut parents = if parents { node.parents .iter() .map(|pid| DATABASE.get_node_with_userdata(*pid, &session)) .collect::>>()? } else { Vec::new() }; let mut similar = get_similar_media(&session, &node)?; 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(ApiNodeResponse { children, parents, node, userdata: udata, }) } pub fn get_similar_media(session: &Session, node: &Node) -> Result, NodeUserData)>> { let this_id = NodeID::from_slug(&node.slug); let mut ranking = BTreeMap::::new(); for tag in &node.tags { let nodes = DATABASE.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, _)| DATABASE.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(), )) } } pub fn get_nodes_modified_since(_session: &Session, since: u64) -> Result> { 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> { DATABASE.get_node_external_id(platform, eid) } pub fn node_id_to_slug(_session: &Session, id: NodeID) -> Result { 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(()) }) }