/* 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) 2023 metamuffin */ use super::{ assets::rocket_uri_macro_r_item_assets, error::MyError, sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm}, }; use crate::{ database::Database, routes::{ api::AcceptJson, ui::{ account::session::Session, assets::AssetRole, layout::{DynLayoutPage, LayoutPage}, player::{rocket_uri_macro_r_player, PlayerConfig}, }, }, uri, }; use anyhow::{anyhow, Context}; use jellybase::permission::NodePermissionExt; use jellycommon::{MediaInfo, NodeKind, NodePublic, Rating, SourceTrackKind}; use rocket::{get, serde::json::Json, Either, State}; /// This function is a stub and only useful for use in the uri! macro. #[get("/n/")] pub fn r_library_node(id: String) { drop(id) } #[get("/n/?")] pub async fn r_library_node_filter<'a>( session: Session, id: &'a str, db: &'a State, aj: AcceptJson, filter: NodeFilterSort, ) -> Result, Json>, MyError> { let node = db .node .get(&id.to_string()) .context("retrieving library node")? .only_if_permitted(&session.user.permissions) .ok_or(anyhow!("node does not exist"))? .public; if *aj { return Ok(Either::Right(Json(node))); } let mut children = node .children .iter() .map(|c| { Ok(( c.to_owned(), db.node .get(c)? .ok_or(anyhow!("child does not exist: {c}"))? .public, )) }) .collect::>>()? .into_iter() .collect(); filter_and_sort_nodes(&filter, &mut children); Ok(Either::Left(LayoutPage { title: node.title.to_string(), content: markup::new! { @NodePage { node: &node, id: &id, children: &children, filter: &filter } }, ..Default::default() })) } markup::define! { NodeCard<'a>(id: &'a str, node: &'a NodePublic) { @let cls = format!("node card poster {}", match node.kind {NodeKind::Channel => "aspect-square", NodeKind::Video => "aspect-thumb", NodeKind::Collection => "aspect-land", _ => "aspect-port"}); div[class=cls] { .poster { a[href=uri!(r_library_node(id))] { img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))]; } @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) { .cardhover.open { a[href=&uri!(r_library_node(id))] { "Open" } @Props { node } } } else { .cardhover.item { a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "▶" } @Props { node } } } // .inner { // a[href=uri!(r_library_node(id))] { // img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))]; // } // div.details { // h3 { @node.title } // p.descriptioüwn { @node.description } // @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) { // a[href=&uri!(r_library_node(id))] { "Open" } // } else { // a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" } // } // } // } } div.title { a[href=uri!(r_library_node(id))] { @node.title } } } } NodePage<'a>(id: &'a str, node: &'a NodePublic, children: &'a Vec<(String, NodePublic)>, filter: &'a NodeFilterSort) { @if !matches!(node.kind, NodeKind::Collection) { img.backdrop[src=uri!(r_item_assets(id, AssetRole::Backdrop, Some(2048)))]; } .page.node { @if !matches!(node.kind, NodeKind::Collection) { div.bigposter { img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(2048)))]; } } .title { h1 { @node.title } @if node.media.is_some() { a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" }} } .details { @Props { node } h3 { @node.tagline } @if let Some(description) = &node.description { p { @for line in description.lines() { @line br; } } } } @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) { @if matches!(node.kind, NodeKind::Collection) { @if let Some(parent) = &node.path.last().cloned() { a.dirup[href=uri!(r_library_node(parent))] { "Go up" } } } @NodeFilterSortForm { f: filter } } @match node.kind { NodeKind::Collection | NodeKind::Channel => { ul.children {@for (id, node) in children.iter() { li { @NodeCard { id, node } } }} } NodeKind::Series => { ol { @for (id, c) in children.iter() { li { a[href=uri!(r_library_node(id))] { @c.title } } }} } _ => {} } } } Props<'a>(node: &'a NodePublic) { .props { @if let Some(m) = &node.media { p { @format_duration(m.duration) } p { @m.resolution_name() } } @for (kind, value) in &node.ratings { p { @match kind { Rating::YoutubeLikes => { @format_count(*value as usize) " Likes" } Rating::YoutubeViews => { @format_count(*value as usize) " Views" } Rating::YoutubeFollowers => { @format_count(*value as usize) " Subscribers" } Rating::RottenTomatoes => { @value " Tomatoes" } Rating::Metacritic => { "Metacritic Score: " @value } Rating::Imdb => { "IMDb Rating: " @value } } } } } } } pub fn format_duration(mut d: f64) -> String { let mut s = String::new(); for (unit, k) in [("h", 60. * 60.), ("m", 60.), ("s", 1.)] { let mut h = 0; while d > k { d -= k; h += 1; } if h > 0 { s += &format!("{h}{unit}") } } s } trait MediaInfoExt { fn resolution_name(&self) -> &'static str; } impl MediaInfoExt for MediaInfo { fn resolution_name(&self) -> &'static str { let mut maxw = 0; for t in &self.tracks { match &t.kind { SourceTrackKind::Video { width, .. } => maxw = maxw.max(*width), _ => (), } } match maxw { 7680.. => "8K", 3840.. => "4K", 2560.. => "WQHD", 1920.. => "Full HD", 1280.. => "HD 720p", 640.. => "NTSC", _ => "Unkown", } } } fn format_count(n: impl Into) -> String { let n: usize = n.into(); if n >= 1_000_000 { format!("{:.1}M", n as f32 / 1_000_000.) } else if n >= 1_000 { format!("{:.1}k", n as f32 / 1_000.) } else { format!("{n}") } }