Skip to main content

jellytau_lib/utils/
conversions.rs

1//! Unit conversion and formatting utilities
2//!
3//! This module provides centralized conversion functions for:
4//! - Jellyfin tick-to-seconds conversions
5//! - Volume normalization (0-1 vs 0-100 ranges)
6//! - Time formatting for UI display
7//!
8//! These utilities eliminate magic numbers and duplicate conversion logic
9//! across the codebase.
10
11/// Number of Jellyfin ticks per second (10 million)
12///
13/// Jellyfin uses "ticks" for time values where 10,000,000 ticks = 1 second.
14/// This follows the .NET TimeSpan.Ticks convention.
15pub const TICKS_PER_SECOND: i64 = 10_000_000;
16
17/// Convert seconds to Jellyfin ticks
18///
19/// # Arguments
20/// * `seconds` - Time in seconds (e.g., 90.5 for 1 minute 30.5 seconds)
21///
22/// # Returns
23/// Time in Jellyfin ticks (will be rounded down to nearest tick)
24///
25/// # Example
26/// ```
27/// # use jellytau_lib::utils::conversions::*;
28/// let ticks = seconds_to_ticks(1.5); // 15,000,000 ticks
29/// ```
30#[inline]
31pub fn seconds_to_ticks(seconds: f64) -> i64 {
32    (seconds * TICKS_PER_SECOND as f64) as i64
33}
34
35/// Convert Jellyfin ticks to seconds
36///
37/// # Arguments
38/// * `ticks` - Time in Jellyfin ticks
39///
40/// # Returns
41/// Time in seconds as floating point
42///
43/// # Example
44/// ```
45/// # use jellytau_lib::utils::conversions::*;
46/// let seconds = ticks_to_seconds(15_000_000); // 1.5 seconds
47/// ```
48#[inline]
49pub fn ticks_to_seconds(ticks: i64) -> f64 {
50    ticks as f64 / TICKS_PER_SECOND as f64
51}
52
53/// Convert percentage volume (0-100) to normalized (0.0-1.0)
54///
55/// Used when receiving volume from Jellyfin remote sessions or UI controls.
56/// Values outside the 0-100 range are clamped.
57///
58/// # Arguments
59/// * `percent` - Volume as percentage (0 to 100)
60///
61/// # Returns
62/// Normalized volume (0.0 to 1.0)
63///
64/// # Example
65/// ```
66/// # use jellytau_lib::utils::conversions::*;
67/// let normalized = percent_to_volume(75.0); // 0.75
68/// ```
69#[inline]
70pub fn percent_to_volume(percent: f64) -> f64 {
71    percent.clamp(0.0, 100.0) / 100.0
72}
73
74/// Convert normalized volume (0.0-1.0) to percentage (0-100)
75///
76/// Used when sending volume to Jellyfin remote sessions, MPV, or ExoPlayer.
77/// Values outside the 0.0-1.0 range are clamped.
78///
79/// # Arguments
80/// * `volume` - Normalized volume (0.0 to 1.0)
81///
82/// # Returns
83/// Volume as percentage (0 to 100), floored to integer value
84///
85/// # Example
86/// ```
87/// # use jellytau_lib::utils::conversions::*;
88/// let percent = volume_to_percent(0.75); // 75.0
89/// ```
90#[inline]
91#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
92pub fn volume_to_percent(volume: f64) -> f64 {
93    (volume.clamp(0.0, 1.0) * 100.0).floor()
94}
95
96/// Format time in seconds to MM:SS display string
97///
98/// # Arguments
99/// * `seconds` - Time in seconds
100///
101/// # Returns
102/// Formatted string like "3:45" or "12:09"
103///
104/// # Example
105/// ```
106/// # use jellytau_lib::utils::conversions::*;
107/// let formatted = format_time(225.0); // "3:45"
108/// ```
109pub fn format_time(seconds: f64) -> String {
110    let mins = (seconds / 60.0).floor() as i64;
111    let secs = (seconds % 60.0).floor() as i64;
112    format!("{}:{:02}", mins, secs)
113}
114
115/// Format time in seconds to HH:MM:SS or MM:SS display string
116///
117/// Automatically chooses format based on duration:
118/// - Less than 1 hour: Returns MM:SS format
119/// - 1 hour or more: Returns HH:MM:SS format
120///
121/// # Arguments
122/// * `seconds` - Time in seconds
123///
124/// # Returns
125/// Formatted string like "1:23:45" or "3:45"
126///
127/// # Example
128/// ```
129/// # use jellytau_lib::utils::conversions::*;
130/// let short = format_time_long(225.0);  // "3:45"
131/// let long = format_time_long(5025.0);  // "1:23:45"
132/// ```
133pub fn format_time_long(seconds: f64) -> String {
134    let hours = (seconds / 3600.0).floor() as i64;
135    let mins = ((seconds % 3600.0) / 60.0).floor() as i64;
136    let secs = (seconds % 60.0).floor() as i64;
137
138    if hours > 0 {
139        format!("{}:{:02}:{:02}", hours, mins, secs)
140    } else {
141        format!("{}:{:02}", mins, secs)
142    }
143}
144
145/// Calculate progress percentage from position and duration
146///
147/// # Arguments
148/// * `position` - Current position in seconds
149/// * `duration` - Total duration in seconds
150///
151/// # Returns
152/// Progress as percentage (0.0 to 100.0), or 0.0 if duration is invalid
153///
154/// # Example
155/// ```
156/// # use jellytau_lib::utils::conversions::*;
157/// let progress = calculate_progress(45.0, 180.0); // 25.0
158/// ```
159pub fn calculate_progress(position: f64, duration: f64) -> f64 {
160    if duration <= 0.0 {
161        return 0.0;
162    }
163    ((position / duration) * 100.0).clamp(0.0, 100.0)
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_tick_conversion() {
172        assert_eq!(seconds_to_ticks(1.0), 10_000_000);
173        assert_eq!(seconds_to_ticks(0.5), 5_000_000);
174        assert_eq!(ticks_to_seconds(10_000_000), 1.0);
175        assert_eq!(ticks_to_seconds(5_000_000), 0.5);
176    }
177
178    #[test]
179    fn test_volume_conversion() {
180        assert_eq!(percent_to_volume(100.0), 1.0);
181        assert_eq!(percent_to_volume(50.0), 0.5);
182        assert_eq!(percent_to_volume(0.0), 0.0);
183        assert_eq!(volume_to_percent(1.0), 100.0);
184        assert_eq!(volume_to_percent(0.5), 50.0);
185        assert_eq!(volume_to_percent(0.0), 0.0);
186    }
187
188    #[test]
189    fn test_volume_clamping() {
190        assert_eq!(percent_to_volume(150.0), 1.0);
191        assert_eq!(percent_to_volume(-10.0), 0.0);
192        assert_eq!(volume_to_percent(1.5), 100.0);
193        assert_eq!(volume_to_percent(-0.5), 0.0);
194    }
195
196    #[test]
197    fn test_time_formatting() {
198        assert_eq!(format_time(0.0), "0:00");
199        assert_eq!(format_time(59.0), "0:59");
200        assert_eq!(format_time(60.0), "1:00");
201        assert_eq!(format_time(125.0), "2:05");
202        assert_eq!(format_time(3661.0), "61:01");
203    }
204
205    #[test]
206    fn test_time_formatting_long() {
207        assert_eq!(format_time_long(0.0), "0:00");
208        assert_eq!(format_time_long(59.0), "0:59");
209        assert_eq!(format_time_long(3599.0), "59:59");
210        assert_eq!(format_time_long(3600.0), "1:00:00");
211        assert_eq!(format_time_long(3661.0), "1:01:01");
212        assert_eq!(format_time_long(7384.0), "2:03:04");
213    }
214
215    #[test]
216    fn test_progress_calculation() {
217        assert_eq!(calculate_progress(0.0, 100.0), 0.0);
218        assert_eq!(calculate_progress(50.0, 100.0), 50.0);
219        assert_eq!(calculate_progress(100.0, 100.0), 100.0);
220        assert_eq!(calculate_progress(25.0, 0.0), 0.0); // Invalid duration
221        assert_eq!(calculate_progress(150.0, 100.0), 100.0); // Clamped
222    }
223}