use crispy_iptv_types::Resolution;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MediaInfo {
pub video: Option<VideoInfo>,
pub audio: Option<AudioInfo>,
pub format_name: Option<String>,
pub duration_secs: Option<f64>,
pub overall_bitrate: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoInfo {
pub codec: String,
pub width: u32,
pub height: u32,
pub fps: f64,
pub bitrate: Option<u64>,
pub resolution: Resolution,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioInfo {
pub codec: String,
pub bitrate: Option<u64>,
pub channels: Option<u32>,
pub sample_rate: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HlsVariant {
pub url: String,
pub bandwidth: u64,
pub average_bandwidth: Option<u64>,
pub width: Option<u32>,
pub height: Option<u32>,
pub codecs: Option<String>,
}
impl HlsVariant {
pub fn quality_score(&self) -> (u8, u64, u64, u64) {
let pixels = match (self.width, self.height) {
(Some(w), Some(h)) if w > 0 && h > 0 => u64::from(w) * u64::from(h),
_ => 0,
};
let has_res = u8::from(pixels > 0);
(
has_res,
pixels,
self.average_bandwidth.unwrap_or(0),
self.bandwidth,
)
}
}
pub fn classify_resolution(width: u32, height: u32) -> Resolution {
if width >= 3840 && height >= 2160 {
Resolution::UHD
} else if width >= 1920 && height >= 1080 {
Resolution::FHD
} else if width >= 1280 && height >= 720 {
Resolution::HD
} else if width > 0 && height > 0 {
Resolution::SD
} else {
Resolution::Unknown
}
}
pub fn height_to_label(height: u32) -> &'static str {
if height == 0 {
"Unknown"
} else if height >= 4320 {
"8K"
} else if height >= 2160 {
"4K"
} else if height >= 1080 {
"1080p"
} else if height >= 720 {
"720p"
} else {
"SD"
}
}
pub fn parse_frame_rate(fps_str: &str) -> Option<f64> {
let trimmed = fps_str.trim();
if trimmed.is_empty() {
return None;
}
if let Some((num_str, den_str)) = trimmed.split_once('/') {
let num: f64 = num_str.trim().parse().ok()?;
let den: f64 = den_str.trim().parse().ok()?;
if den > 0.0 {
let fps = num / den;
if fps > 0.0 {
return Some(fps);
}
}
None
} else {
let fps: f64 = trimmed.parse().ok()?;
if fps > 0.0 { Some(fps) } else { None }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_resolution_from_dimensions() {
assert_eq!(classify_resolution(3840, 2160), Resolution::UHD);
assert_eq!(classify_resolution(1920, 1080), Resolution::FHD);
assert_eq!(classify_resolution(1280, 720), Resolution::HD);
assert_eq!(classify_resolution(720, 576), Resolution::SD);
assert_eq!(classify_resolution(640, 480), Resolution::SD);
assert_eq!(classify_resolution(0, 0), Resolution::Unknown);
}
#[test]
fn height_to_label_covers_all_tiers() {
assert_eq!(height_to_label(0), "Unknown");
assert_eq!(height_to_label(480), "SD");
assert_eq!(height_to_label(720), "720p");
assert_eq!(height_to_label(1080), "1080p");
assert_eq!(height_to_label(2160), "4K");
assert_eq!(height_to_label(4320), "8K");
}
#[test]
fn parse_fractional_frame_rate() {
let fps = parse_frame_rate("30000/1001").unwrap();
assert!((fps - 29.97).abs() < 0.01);
}
#[test]
fn parse_integer_frame_rate() {
let fps = parse_frame_rate("25").unwrap();
assert!((fps - 25.0).abs() < f64::EPSILON);
}
#[test]
fn parse_frame_rate_zero_denominator() {
assert!(parse_frame_rate("30/0").is_none());
}
#[test]
fn parse_frame_rate_empty() {
assert!(parse_frame_rate("").is_none());
}
#[test]
fn parse_frame_rate_negative() {
assert!(parse_frame_rate("-25").is_none());
}
#[test]
fn hls_variant_quality_score_ordering() {
let low = HlsVariant {
url: "low.m3u8".into(),
bandwidth: 500_000,
average_bandwidth: None,
width: Some(640),
height: Some(360),
codecs: None,
};
let high = HlsVariant {
url: "high.m3u8".into(),
bandwidth: 5_000_000,
average_bandwidth: Some(4_500_000),
width: Some(1920),
height: Some(1080),
codecs: None,
};
assert!(high.quality_score() > low.quality_score());
}
#[test]
fn hls_variant_no_resolution_scores_lower() {
let with_res = HlsVariant {
url: "a.m3u8".into(),
bandwidth: 1_000_000,
average_bandwidth: None,
width: Some(1280),
height: Some(720),
codecs: None,
};
let without_res = HlsVariant {
url: "b.m3u8".into(),
bandwidth: 10_000_000,
average_bandwidth: None,
width: None,
height: None,
codecs: None,
};
assert!(with_res.quality_score() > without_res.quality_score());
}
}