/* 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) 2026 metamuffin */ use jellycommon::{ jellyobject::{Object, Tag}, *, }; use jellyui_locale::tr; use std::{borrow::Cow, fmt::Write}; pub fn format_duration(d: f64) -> String { format_duration_mode("en", d, false) } pub fn format_duration_long(lang: &str, d: f64) -> String { format_duration_mode(lang, d, true) } fn format_duration_mode(lang: &str, mut d: f64, long_units: bool) -> String { let mut s = String::new(); let sign = if d > 0. { "" } else { "-" }; d = d.abs(); for (short, long, long_pl, k) in [ ("d", "time.day", "time.days", 60. * 60. * 24.), ("h", "time.hour", "time.hours", 60. * 60.), ("m", "time.minute", "time.minutes", 60.), ("s", "time.second", "time.seconds", 1.), ] { let h = (d / k).floor(); d -= h * k; if h > 0. { if long_units { let long = tr(lang, if h != 1. { long_pl } else { long }); let and = format!(" {} ", tr(lang, "time.and_join")); // TODO breaks if seconds is zero write!( s, "{}{h} {long}{}", if k != 1. { "" } else { &and }, if k > 60. { ", " } else { "" }, ) .unwrap(); } else { write!(s, "{h}{short} ").unwrap(); } } } format!("{sign}{}", s.trim()) } #[test] fn test_duration_short() { assert_eq!(format_duration(61.).as_str(), "1m 1s"); assert_eq!(format_duration(3661.).as_str(), "1h 1m 1s"); } #[test] fn test_duration_long() { assert_eq!( format_duration_long("en", 61.).as_str(), "1 minute and 1 second" ); assert_eq!( format_duration_long("en", 121.).as_str(), "2 minutes and 1 second" ); assert_eq!( format_duration_long("en", 3661.).as_str(), "1 hour, 1 minute and 1 second" ); } pub fn format_size(size: u64) -> String { humansize::format_size(size, humansize::DECIMAL) } pub fn format_kind(lang: &str, kind: Tag) -> Cow<'static, str> { tr( lang, match kind { KIND_MOVIE => "kind.movie", KIND_VIDEO => "kind.video", KIND_MUSIC => "kind.music", KIND_SHORTFORMVIDEO => "kind.short_form_video", KIND_COLLECTION => "kind.collection", KIND_CHANNEL => "kind.channel", KIND_SHOW => "kind.show", KIND_SERIES => "kind.series", KIND_SEASON => "kind.season", KIND_EPISODE => "kind.episode", _ => "kind.unknown", }, ) } pub fn node_resolution_name(node: &Object) -> &'static str { let mut maxdim = 0; for t in node.iter(NO_TRACK) { if let Some(width) = t.get(TR_PIXEL_WIDTH) { maxdim = maxdim.max(width) } if let Some(height) = t.get(TR_PIXEL_HEIGHT) { maxdim = maxdim.max(height) } } match maxdim { 30720.. => "32K", 15360.. => "16K", 7680.. => "8K UHD", 5120.. => "5K UHD", 3840.. => "4K UHD", 2560.. => "QHD 1440p", 1920.. => "FHD 1080p", 1280.. => "HD 720p", 854.. => "SD 480p", _ => "Unkown", } } pub 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}") } } pub fn format_chapter(c: &Object) -> (String, String) { ( format!( "{}-{}", c.get(CH_START).map(format_duration).unwrap_or_default(), c.get(CH_END).map(format_duration).unwrap_or_default(), ), c.get(CH_NAME).unwrap_or_default().to_string(), ) }