use async_trait::async_trait;
use url::Url;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Quality {
Video { height: u32, fps: Option<f64> },
Audio { bitrate_kbps: Option<u32> },
Subtitles { lang: String, automatic: bool },
Unknown { note: Option<String> },
}
impl std::fmt::Display for Quality {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Quality::Video { height, fps } => {
match fps.filter(|v| *v >= 50.0) {
Some(v) => write!(f, "{height}p{}", v.round() as u32),
None => write!(f, "{height}p"),
}
}
Quality::Audio { bitrate_kbps } => match bitrate_kbps {
Some(k) => write!(f, "audio {k}k"),
None => f.write_str("audio"),
},
Quality::Subtitles { lang, automatic } => {
if *automatic {
write!(f, "auto-subtitles ({lang})")
} else {
write!(f, "subtitles ({lang})")
}
}
Quality::Unknown { note } => match note {
Some(n) => f.write_str(n),
None => f.write_str("unknown"),
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MediaFormat {
pub id: String,
pub ext: String,
pub height: Option<u32>,
pub fps: Option<f64>,
pub tbr: Option<f64>,
pub size: Option<u64>,
pub size_is_approx: bool,
pub has_video: bool,
pub has_audio: bool,
pub note: Option<String>,
}
impl MediaFormat {
pub fn is_complete(&self) -> bool {
self.has_video && self.has_audio
}
pub fn is_audio_only(&self) -> bool {
self.has_audio && !self.has_video
}
pub fn quality(&self) -> Quality {
if let Some(height) = self.height {
Quality::Video {
height,
fps: self.fps,
}
} else if self.is_audio_only() {
Quality::Audio {
bitrate_kbps: self.tbr.map(|t| t.round() as u32),
}
} else {
Quality::Unknown {
note: self.note.clone().or_else(|| Some(self.id.clone())),
}
}
}
pub fn quality_label(&self) -> String {
self.quality().to_string()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SubtitleTrack {
pub lang: String,
pub ext: String,
pub automatic: bool,
}
pub const SUBTITLE_ID_PREFIXES: [&str; 2] = ["subs:", "autosubs:"];
fn is_language_tag(lang: &str) -> bool {
!lang.is_empty()
&& lang.len() <= 32
&& lang
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
pub fn subtitle_format_id(lang: &str, automatic: bool) -> Option<String> {
if !is_language_tag(lang) {
return None;
}
let prefix = if automatic { "autosubs" } else { "subs" };
Some(format!("{prefix}:{lang}"))
}
pub fn parse_subtitle_format_id(id: &str) -> Option<(&str, bool)> {
let (lang, automatic) = if let Some(lang) = id.strip_prefix("autosubs:") {
(lang, true)
} else {
(id.strip_prefix("subs:")?, false)
};
is_language_tag(lang).then_some((lang, automatic))
}
#[derive(Debug, Clone, PartialEq)]
pub struct QualityTier {
pub quality: Quality,
pub format_id: String,
pub ext: String,
pub size: Option<u64>,
pub size_is_approx: bool,
pub needs_merge: bool,
pub available: bool,
}
#[derive(Debug, Clone)]
pub struct FormatOffer {
pub source_url: Url,
pub title: String,
pub formats: Vec<MediaFormat>,
pub default_id: Option<String>,
pub default_ext: Option<String>,
pub subtitles: Vec<SubtitleTrack>,
pub can_merge: bool,
}
impl FormatOffer {
pub fn selectable(&self) -> Vec<&MediaFormat> {
let mut out: Vec<&MediaFormat> = self
.formats
.iter()
.filter(|f| {
if self.can_merge {
true
} else {
f.is_complete()
}
})
.filter(|f| f.has_video || f.has_audio)
.collect();
out.sort_by(|a, b| {
b.has_video
.cmp(&a.has_video)
.then(b.height.unwrap_or(0).cmp(&a.height.unwrap_or(0)))
.then(
b.tbr
.unwrap_or(0.0)
.partial_cmp(&a.tbr.unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal),
)
});
out
}
fn ranked(&self) -> Vec<&MediaFormat> {
let mut out: Vec<&MediaFormat> = self
.formats
.iter()
.filter(|f| f.has_video || f.has_audio)
.collect();
out.sort_by(|a, b| {
b.has_video
.cmp(&a.has_video)
.then(b.height.unwrap_or(0).cmp(&a.height.unwrap_or(0)))
.then(
b.tbr
.unwrap_or(0.0)
.partial_cmp(&a.tbr.unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal),
)
});
out
}
pub fn quality_tiers(&self) -> Vec<QualityTier> {
let best_audio = self.best_audio_only();
let mut seen_heights: Vec<u32> = Vec::new();
let mut tiers: Vec<QualityTier> = Vec::new();
for f in self.ranked() {
if !f.has_video {
continue;
}
let Some(height) = f.height else { continue };
if seen_heights.contains(&height) {
continue;
}
seen_heights.push(height);
let tier = if f.is_complete() {
QualityTier {
quality: f.quality(),
ext: self.ext_for_parts(&f.id, &[f]),
format_id: f.id.clone(),
size: f.size,
size_is_approx: f.size_is_approx,
needs_merge: false,
available: true,
}
} else if let Some(audio) = best_audio {
let id = merged_id(&f.id, &audio.id);
QualityTier {
quality: f.quality(),
ext: self.ext_for_parts(&id, &[f, audio]),
format_id: id,
size: f.size.zip(audio.size).map(|(v, a)| v.saturating_add(a)),
size_is_approx: f.size_is_approx || audio.size_is_approx,
needs_merge: true,
available: self.can_merge,
}
} else {
continue;
};
tiers.push(tier);
}
if let Some(audio) = best_audio {
tiers.push(QualityTier {
quality: audio.quality(),
ext: self.ext_for_parts(&audio.id, &[audio]),
format_id: audio.id.clone(),
size: audio.size,
size_is_approx: audio.size_is_approx,
needs_merge: false,
available: true,
});
}
let mut subs: Vec<&SubtitleTrack> = self.subtitles.iter().collect();
subs.sort_by_key(|t| (t.automatic, t.lang.clone()));
for track in subs {
let Some(format_id) = subtitle_format_id(&track.lang, track.automatic) else {
continue;
};
tiers.push(QualityTier {
quality: Quality::Subtitles {
lang: track.lang.clone(),
automatic: track.automatic,
},
ext: track.ext.clone(),
format_id,
size: None,
size_is_approx: false,
needs_merge: false,
available: true,
});
}
tiers
}
fn ext_for_parts(&self, format_id: &str, parts: &[&MediaFormat]) -> String {
if self.default_id.as_deref() == Some(format_id)
&& let Some(ext) = &self.default_ext
{
return ext.clone();
}
let exts: Vec<&str> = parts.iter().map(|f| f.ext.as_str()).collect();
if exts.is_empty() {
return "bin".to_owned();
}
merged_ext(&exts)
}
pub fn best_audio_only(&self) -> Option<&MediaFormat> {
self.formats
.iter()
.filter(|f| f.is_audio_only())
.max_by(|a, b| {
a.tbr
.unwrap_or(0.0)
.partial_cmp(&b.tbr.unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn find(&self, id: &str) -> Option<&MediaFormat> {
self.formats.iter().find(|f| f.id == id)
}
}
pub fn merged_id(video_id: &str, audio_id: &str) -> String {
format!("{video_id}+{audio_id}")
}
pub fn split_id(id: &str) -> Vec<&str> {
id.split('+').collect()
}
pub fn merged_ext(exts: &[&str]) -> String {
let mp4_family = ["mp4", "m4a", "m4v", "mov"];
let webm_family = ["webm", "weba"];
if exts.iter().all(|e| mp4_family.contains(e)) {
"mp4".to_owned()
} else if exts.iter().all(|e| webm_family.contains(e)) {
"webm".to_owned()
} else {
"mkv".to_owned()
}
}
#[async_trait]
pub trait FormatSelector: Send + Sync {
async fn select(&self, offer: &FormatOffer) -> Option<String>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct DefaultFormatSelector;
#[async_trait]
impl FormatSelector for DefaultFormatSelector {
async fn select(&self, offer: &FormatOffer) -> Option<String> {
offer
.default_id
.clone()
.or_else(|| offer.selectable().first().map(|f| f.id.clone()))
}
}
#[derive(Debug, Clone)]
pub struct FixedFormatSelector(pub String);
#[async_trait]
impl FormatSelector for FixedFormatSelector {
async fn select(&self, _offer: &FormatOffer) -> Option<String> {
Some(self.0.clone())
}
}
#[derive(Debug, Clone, Copy)]
pub struct MaxHeightFormatSelector {
pub max_height: u32,
}
#[async_trait]
impl FormatSelector for MaxHeightFormatSelector {
async fn select(&self, offer: &FormatOffer) -> Option<String> {
let best_audio = offer.best_audio_only();
for f in offer.selectable() {
if !f.has_video {
continue;
}
if f.height.is_some_and(|h| h > self.max_height) {
continue;
}
if f.is_complete() {
return Some(f.id.clone());
}
if let Some(audio) = best_audio {
return Some(merged_id(&f.id, &audio.id));
}
}
DefaultFormatSelector.select(offer).await
}
}
#[cfg(test)]
mod tests {
use super::*;
fn video(id: &str, height: u32, ext: &str) -> MediaFormat {
MediaFormat {
id: id.to_owned(),
ext: ext.to_owned(),
height: Some(height),
fps: Some(30.0),
tbr: Some(height as f64 * 2.0),
size: Some(1000),
size_is_approx: false,
has_video: true,
has_audio: false,
note: None,
}
}
fn audio(id: &str, tbr: f64, ext: &str) -> MediaFormat {
MediaFormat {
id: id.to_owned(),
ext: ext.to_owned(),
height: None,
fps: None,
tbr: Some(tbr),
size: Some(100),
size_is_approx: false,
has_video: false,
has_audio: true,
note: None,
}
}
fn complete(id: &str, height: u32) -> MediaFormat {
MediaFormat {
has_audio: true,
..video(id, height, "mp4")
}
}
fn offer(formats: Vec<MediaFormat>, can_merge: bool) -> FormatOffer {
FormatOffer {
source_url: Url::parse("https://example.test/watch").unwrap(),
title: "T".to_owned(),
formats,
default_id: None,
default_ext: None,
subtitles: Vec::new(),
can_merge,
}
}
#[test]
fn tiers_needing_a_muxer_are_listed_but_marked_unavailable() {
let o = offer(
vec![
video("137", 1080, "mp4"),
audio("251", 128.0, "webm"),
complete("18", 360),
],
false,
);
let tiers = o.quality_tiers();
let by_label: Vec<(String, bool)> = tiers
.iter()
.map(|t| (t.quality.to_string(), t.available))
.collect();
assert_eq!(
by_label,
[
("1080p".to_owned(), false),
("360p".to_owned(), true),
("audio 128k".to_owned(), true)
]
);
let o = FormatOffer {
can_merge: true,
..o
};
assert!(o.quality_tiers().iter().all(|t| t.available));
}
#[test]
fn without_a_muxer_only_self_contained_formats_are_offered() {
let o = offer(
vec![
video("137", 1080, "mp4"),
audio("251", 128.0, "webm"),
complete("18", 360),
],
false,
);
let ids: Vec<&str> = o.selectable().iter().map(|f| f.id.as_str()).collect();
assert_eq!(ids, ["18"], "video-only and audio-only need a muxer");
}
#[test]
fn with_a_muxer_video_tiers_pair_with_the_best_audio() {
let o = offer(
vec![
video("137", 1080, "mp4"),
video("136", 720, "mp4"),
audio("251", 128.0, "webm"),
audio("250", 64.0, "webm"),
],
true,
);
let tiers = o.quality_tiers();
assert_eq!(tiers[0].quality.to_string(), "1080p");
assert_eq!(tiers[0].format_id, "137+251");
assert!(tiers[0].needs_merge);
assert_eq!(tiers[1].format_id, "136+251");
assert_eq!(tiers.last().unwrap().format_id, "251");
assert!(!tiers.last().unwrap().needs_merge);
}
#[test]
fn quality_tiers_collapse_duplicate_heights() {
let o = offer(
vec![
video("137", 1080, "mp4"),
video("248", 1080, "webm"),
video("136", 720, "mp4"),
audio("251", 128.0, "webm"),
],
true,
);
let tiers = o.quality_tiers();
let heights: Vec<String> = tiers.iter().map(|t| t.quality.to_string()).collect();
assert_eq!(heights, ["1080p", "720p", "audio 128k"]);
}
#[test]
fn tiers_report_the_container_they_produce() {
let o = offer(
vec![
video("137", 1080, "mp4"),
audio("251", 128.0, "webm"),
complete("18", 360),
],
true,
);
let tiers = o.quality_tiers();
assert_eq!(tiers[0].ext, "mkv");
assert_eq!(tiers[1].ext, "mp4");
assert_eq!(tiers.last().unwrap().ext, "webm");
}
#[test]
fn transcripts_are_offered_last_and_authored_ones_come_first() {
let mut o = offer(vec![complete("18", 360)], true);
o.subtitles = vec![
SubtitleTrack {
lang: "es".to_owned(),
ext: "vtt".to_owned(),
automatic: true,
},
SubtitleTrack {
lang: "en".to_owned(),
ext: "srt".to_owned(),
automatic: false,
},
];
let tiers = o.quality_tiers();
assert_eq!(
tiers[0].quality,
Quality::Video {
height: 360,
fps: Some(30.0)
}
);
assert_eq!(tiers[1].quality.to_string(), "subtitles (en)");
assert_eq!(tiers[1].ext, "srt");
assert_eq!(tiers[2].quality.to_string(), "auto-subtitles (es)");
assert!(tiers[1].size.is_none() && tiers[2].size.is_none());
}
#[test]
fn a_subtitle_choice_round_trips_through_its_id() {
let id = subtitle_format_id("pt-BR", true).unwrap();
assert_eq!(parse_subtitle_format_id(&id), Some(("pt-BR", true)));
assert_eq!(
parse_subtitle_format_id(&subtitle_format_id("en", false).unwrap()),
Some(("en", false))
);
assert_eq!(parse_subtitle_format_id("137+251"), None);
assert_eq!(parse_subtitle_format_id("18"), None);
assert_eq!(subtitle_format_id("en:US", false), None);
assert_eq!(subtitle_format_id("", false), None);
assert_eq!(parse_subtitle_format_id("subs:"), None);
assert_eq!(parse_subtitle_format_id("subs:../etc/passwd"), None);
}
#[test]
fn merged_container_follows_the_source_families() {
assert_eq!(merged_ext(&["mp4", "m4a"]), "mp4");
assert_eq!(merged_ext(&["webm", "webm"]), "webm");
assert_eq!(merged_ext(&["mp4", "webm"]), "mkv");
assert_eq!(merged_ext(&["mp4"]), "mp4");
}
#[tokio::test]
async fn default_selector_prefers_the_resolved_default() {
let mut o = offer(vec![video("137", 1080, "mp4"), complete("18", 360)], true);
o.default_id = Some("137+251".to_owned());
assert_eq!(
DefaultFormatSelector.select(&o).await,
Some("137+251".to_owned())
);
}
#[tokio::test]
async fn default_selector_falls_back_when_nothing_was_resolved() {
let o = offer(vec![complete("18", 360), video("137", 1080, "mp4")], false);
assert_eq!(
DefaultFormatSelector.select(&o).await,
Some("18".to_owned())
);
}
#[tokio::test]
async fn max_height_selector_caps_quality() {
let o = offer(
vec![
video("137", 1080, "mp4"),
video("136", 720, "mp4"),
audio("251", 128.0, "webm"),
],
true,
);
let selector = MaxHeightFormatSelector { max_height: 720 };
assert_eq!(selector.select(&o).await, Some("136+251".to_owned()));
}
#[test]
fn split_id_handles_compound_and_plain_ids() {
assert_eq!(split_id("137+251"), ["137", "251"]);
assert_eq!(split_id("18"), ["18"]);
}
}