Ajout de la source Radio France avec : - Implémentation du trait MusicSource pour l'intégration UPnP - Routes HTTP API REST pour l'accès aux stations et flux AAC - Proxy streaming avec tracking des connexions - Génération dynamique de l'arborescence UPnP - Cache multi-niveaux (stations, métadonnées, covers) - Support des ~70 stations (standalone, groupes, radios ICI) Fichiers créés : - pmoradiofrance/src/source.rs (implémentation MusicSource) - pmoradiofrance/src/server_ext.rs (routes HTTP et proxy streaming) - pmoradiofrance/assets/radiofrance-logo.webp (logo placeholder) Fichiers modifiés : - pmoradiofrance/src/lib.rs (re-exports et modules) - pmoradiofrance/Cargo.toml (dépendances server) - Cargo.toml (workspace) - pmomediaserver/Cargo.toml (feature radiofrance) - PMOMusic/Cargo.toml (feature radiofrance) - PMOMusic/src/main.rs (enregistrement automatique) Tests et validation : compilation OK, pattern respecté, feature-gating cohérent
217 lines
6.8 KiB
Rust
217 lines
6.8 KiB
Rust
//! Time formatting and parsing utilities for music renderers.
|
|
//!
|
|
//! This module provides utilities to convert between different time representations:
|
|
//! - HH:MM:SS format (UPnP standard)
|
|
//! - Seconds (u32/u64)
|
|
//! - Milliseconds
|
|
//!
|
|
//! All functions are designed to be robust and provide clear error messages.
|
|
|
|
use crate::errors::ControlPointError;
|
|
|
|
/// Formats a duration in seconds as HH:MM:SS.
|
|
///
|
|
/// # Examples
|
|
/// ```
|
|
/// # use pmocontrol::music_renderer::time_utils::format_hhmmss;
|
|
/// assert_eq!(format_hhmmss(0), "00:00:00");
|
|
/// assert_eq!(format_hhmmss(61), "00:01:01");
|
|
/// assert_eq!(format_hhmmss(3661), "01:01:01");
|
|
/// ```
|
|
pub fn format_hhmmss(seconds: u64) -> String {
|
|
let hours = seconds / 3600;
|
|
let minutes = (seconds % 3600) / 60;
|
|
let secs = seconds % 60;
|
|
format!("{:02}:{:02}:{:02}", hours, minutes, secs)
|
|
}
|
|
|
|
/// Formats a duration in seconds as HH:MM:SS (u32 variant).
|
|
///
|
|
/// Convenience wrapper for u32 values.
|
|
pub fn format_hhmmss_u32(seconds: u32) -> String {
|
|
format_hhmmss(seconds as u64)
|
|
}
|
|
|
|
/// Formats a duration in seconds as HH:MM:SS (f64 variant).
|
|
///
|
|
/// Used by Chromecast which returns floating point durations.
|
|
/// Rounds to nearest second.
|
|
pub fn format_hhmmss_f64(seconds: f64) -> String {
|
|
format_hhmmss(seconds.round() as u64)
|
|
}
|
|
|
|
/// Parses a time string in HH:MM:SS, MM:SS, or SS format to seconds.
|
|
///
|
|
/// This is the most flexible parser, supporting multiple formats:
|
|
/// - "HH:MM:SS" → hours * 3600 + minutes * 60 + seconds
|
|
/// - "MM:SS" → minutes * 60 + seconds
|
|
/// - "SS" → seconds
|
|
///
|
|
/// # Examples
|
|
/// ```
|
|
/// # use pmocontrol::music_renderer::time_utils::parse_time_flexible;
|
|
/// assert_eq!(parse_time_flexible("01:02:03").unwrap(), 3723);
|
|
/// assert_eq!(parse_time_flexible("02:03").unwrap(), 123);
|
|
/// assert_eq!(parse_time_flexible("42").unwrap(), 42);
|
|
/// ```
|
|
///
|
|
/// # Errors
|
|
/// Returns an error if:
|
|
/// - The input has more than 3 parts
|
|
/// - Any part is not a valid u32
|
|
pub fn parse_time_flexible(input: &str) -> Result<u32, ControlPointError> {
|
|
let parts: Vec<&str> = input.split(':').collect();
|
|
|
|
if parts.is_empty() || parts.len() > 3 {
|
|
return Err(ControlPointError::InvalidTimeFormat(format!(
|
|
"Invalid time format '{}': expected HH:MM:SS, MM:SS, or SS",
|
|
input
|
|
)));
|
|
}
|
|
|
|
let mut total = 0u32;
|
|
for part in parts {
|
|
let value = part.parse::<u32>().map_err(|_| {
|
|
ControlPointError::InvalidTimeFormat(format!(
|
|
"Invalid numeric value '{}' in time string '{}'",
|
|
part, input
|
|
))
|
|
})?;
|
|
total = total * 60 + value;
|
|
}
|
|
|
|
Ok(total)
|
|
}
|
|
|
|
/// Parses a strict HH:MM:SS format to seconds.
|
|
///
|
|
/// This parser requires exactly 3 components separated by colons,
|
|
/// and validates that minutes and seconds are < 60.
|
|
///
|
|
/// # Examples
|
|
/// ```
|
|
/// # use pmocontrol::music_renderer::time_utils::parse_hhmmss_strict;
|
|
/// assert_eq!(parse_hhmmss_strict("01:02:03").unwrap(), 3723);
|
|
/// assert!(parse_hhmmss_strict("02:03").is_err()); // requires HH:MM:SS
|
|
/// assert!(parse_hhmmss_strict("00:61:00").is_err()); // minutes > 59
|
|
/// ```
|
|
///
|
|
/// # Errors
|
|
/// Returns an error if:
|
|
/// - The format is not exactly HH:MM:SS
|
|
/// - Minutes or seconds are >= 60
|
|
/// - Any component is not a valid u64
|
|
pub fn parse_hhmmss_strict(input: &str) -> Result<u64, ControlPointError> {
|
|
let parts: Vec<&str> = input.split(':').collect();
|
|
|
|
if parts.len() != 3 {
|
|
return Err(ControlPointError::InvalidTimeFormat(format!(
|
|
"Invalid time format '{}': expected exactly HH:MM:SS",
|
|
input
|
|
)));
|
|
}
|
|
|
|
let hours: u64 = parts[0].parse().map_err(|_| {
|
|
ControlPointError::InvalidTimeFormat(format!("Invalid hour component in '{}'", input))
|
|
})?;
|
|
|
|
let minutes: u64 = parts[1].parse().map_err(|_| {
|
|
ControlPointError::InvalidTimeFormat(format!("Invalid minute component in '{}'", input))
|
|
})?;
|
|
|
|
let seconds: u64 = parts[2].parse().map_err(|_| {
|
|
ControlPointError::InvalidTimeFormat(format!("Invalid second component in '{}'", input))
|
|
})?;
|
|
|
|
if minutes >= 60 || seconds >= 60 {
|
|
return Err(ControlPointError::InvalidTimeFormat(format!(
|
|
"Invalid time '{}': minutes and seconds must be < 60",
|
|
input
|
|
)));
|
|
}
|
|
|
|
Ok(hours * 3600 + minutes * 60 + seconds)
|
|
}
|
|
|
|
/// Converts milliseconds to seconds (rounding down).
|
|
#[inline]
|
|
pub fn ms_to_seconds(milliseconds: u64) -> u64 {
|
|
milliseconds / 1000
|
|
}
|
|
|
|
/// Converts seconds to milliseconds.
|
|
#[inline]
|
|
pub fn seconds_to_ms(seconds: u64) -> u64 {
|
|
seconds * 1000
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_format_hhmmss() {
|
|
assert_eq!(format_hhmmss(0), "00:00:00");
|
|
assert_eq!(format_hhmmss(1), "00:00:01");
|
|
assert_eq!(format_hhmmss(60), "00:01:00");
|
|
assert_eq!(format_hhmmss(61), "00:01:01");
|
|
assert_eq!(format_hhmmss(3600), "01:00:00");
|
|
assert_eq!(format_hhmmss(3661), "01:01:01");
|
|
assert_eq!(format_hhmmss(86399), "23:59:59");
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_hhmmss_f64() {
|
|
assert_eq!(format_hhmmss_f64(123.4), "00:02:03");
|
|
assert_eq!(format_hhmmss_f64(123.6), "00:02:04");
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_time_flexible() {
|
|
// HH:MM:SS format
|
|
assert_eq!(parse_time_flexible("01:02:03").unwrap(), 3723);
|
|
assert_eq!(parse_time_flexible("00:00:00").unwrap(), 0);
|
|
assert_eq!(parse_time_flexible("23:59:59").unwrap(), 86399);
|
|
|
|
// MM:SS format
|
|
assert_eq!(parse_time_flexible("02:03").unwrap(), 123);
|
|
assert_eq!(parse_time_flexible("00:00").unwrap(), 0);
|
|
|
|
// SS format
|
|
assert_eq!(parse_time_flexible("42").unwrap(), 42);
|
|
assert_eq!(parse_time_flexible("0").unwrap(), 0);
|
|
|
|
// Errors
|
|
assert!(parse_time_flexible("").is_err());
|
|
assert!(parse_time_flexible("1:2:3:4").is_err());
|
|
assert!(parse_time_flexible("abc").is_err());
|
|
assert!(parse_time_flexible("1:abc").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_hhmmss_strict() {
|
|
assert_eq!(parse_hhmmss_strict("01:02:03").unwrap(), 3723);
|
|
assert_eq!(parse_hhmmss_strict("00:00:00").unwrap(), 0);
|
|
assert_eq!(parse_hhmmss_strict("23:59:59").unwrap(), 86399);
|
|
|
|
// Errors - wrong format
|
|
assert!(parse_hhmmss_strict("02:03").is_err());
|
|
assert!(parse_hhmmss_strict("42").is_err());
|
|
|
|
// Errors - invalid values
|
|
assert!(parse_hhmmss_strict("00:60:00").is_err());
|
|
assert!(parse_hhmmss_strict("00:00:60").is_err());
|
|
assert!(parse_hhmmss_strict("abc:00:00").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_ms_conversions() {
|
|
assert_eq!(ms_to_seconds(1000), 1);
|
|
assert_eq!(ms_to_seconds(1500), 1); // rounds down
|
|
assert_eq!(ms_to_seconds(999), 0);
|
|
|
|
assert_eq!(seconds_to_ms(1), 1000);
|
|
assert_eq!(seconds_to_ms(0), 0);
|
|
}
|
|
}
|