1use crate::types::{StreamFormat, StreamInfo};
19use std::fmt;
20use std::str::FromStr;
21
22pub const AUDIO_ITAG: u32 = 140;
24
25#[derive(Debug, Clone, Copy)]
27pub struct VideoItags {
28 pub h264: u32,
30 pub vp9: u32,
32 pub av1: u32,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum Quality {
53 AudioOnly,
55 P144,
57 P240,
59 P360,
61 P480,
63 P720,
65 P720F60,
67 P1080,
69 P1080F60,
71 P1440,
73 P1440F60,
75 P2160,
77 P2160F60,
79 Best,
81}
82
83impl Quality {
84 #[must_use]
86 pub fn label(&self) -> &'static str {
87 match self {
88 Quality::AudioOnly => "audio_only",
89 Quality::P144 => "144p",
90 Quality::P240 => "240p",
91 Quality::P360 => "360p",
92 Quality::P480 => "480p",
93 Quality::P720 => "720p",
94 Quality::P720F60 => "720p60",
95 Quality::P1080 => "1080p",
96 Quality::P1080F60 => "1080p60",
97 Quality::P1440 => "1440p",
98 Quality::P1440F60 => "1440p60",
99 Quality::P2160 => "2160p",
100 Quality::P2160F60 => "2160p60",
101 Quality::Best => "best",
102 }
103 }
104
105 #[must_use]
109 pub fn itags(&self) -> Option<VideoItags> {
110 match self {
111 Quality::AudioOnly | Quality::Best => None,
112 Quality::P144 => Some(VideoItags {
113 h264: 160,
114 vp9: 278,
115 av1: 394,
116 }),
117 Quality::P240 => Some(VideoItags {
118 h264: 133,
119 vp9: 242,
120 av1: 395,
121 }),
122 Quality::P360 => Some(VideoItags {
123 h264: 134,
124 vp9: 243,
125 av1: 396,
126 }),
127 Quality::P480 => Some(VideoItags {
128 h264: 135,
129 vp9: 244,
130 av1: 397,
131 }),
132 Quality::P720 => Some(VideoItags {
133 h264: 136,
134 vp9: 247,
135 av1: 398,
136 }),
137 Quality::P720F60 => Some(VideoItags {
138 h264: 298,
139 vp9: 302,
140 av1: 398,
141 }),
142 Quality::P1080 => Some(VideoItags {
143 h264: 137,
144 vp9: 248,
145 av1: 399,
146 }),
147 Quality::P1080F60 => Some(VideoItags {
148 h264: 299,
149 vp9: 303,
150 av1: 399,
151 }),
152 Quality::P1440 => Some(VideoItags {
153 h264: 264,
154 vp9: 271,
155 av1: 400,
156 }),
157 Quality::P1440F60 => Some(VideoItags {
158 h264: 304,
159 vp9: 308,
160 av1: 400,
161 }),
162 Quality::P2160 => Some(VideoItags {
163 h264: 266,
164 vp9: 313,
165 av1: 401,
166 }),
167 Quality::P2160F60 => Some(VideoItags {
168 h264: 305,
169 vp9: 315,
170 av1: 401,
171 }),
172 }
173 }
174
175 #[must_use]
177 pub fn all() -> &'static [Quality] {
178 &[
179 Quality::AudioOnly,
180 Quality::P144,
181 Quality::P240,
182 Quality::P360,
183 Quality::P480,
184 Quality::P720,
185 Quality::P720F60,
186 Quality::P1080,
187 Quality::P1080F60,
188 Quality::P1440,
189 Quality::P1440F60,
190 Quality::P2160,
191 Quality::P2160F60,
192 ]
193 }
194}
195
196impl FromStr for Quality {
197 type Err = String;
198
199 fn from_str(s: &str) -> Result<Self, Self::Err> {
200 match s.to_lowercase().as_str() {
201 "audio_only" | "audio" => Ok(Quality::AudioOnly),
202 "144p" | "144" => Ok(Quality::P144),
203 "240p" | "240" => Ok(Quality::P240),
204 "360p" | "360" => Ok(Quality::P360),
205 "480p" | "480" => Ok(Quality::P480),
206 "720p" | "720" => Ok(Quality::P720),
207 "720p60" => Ok(Quality::P720F60),
208 "1080p" | "1080" => Ok(Quality::P1080),
209 "1080p60" => Ok(Quality::P1080F60),
210 "1440p" | "1440" => Ok(Quality::P1440),
211 "1440p60" => Ok(Quality::P1440F60),
212 "2160p" | "2160" | "4k" => Ok(Quality::P2160),
213 "2160p60" | "4k60" => Ok(Quality::P2160F60),
214 "best" => Ok(Quality::Best),
215 _ => Err(format!("Unknown quality: {}", s)),
216 }
217 }
218}
219
220impl fmt::Display for Quality {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 write!(f, "{}", self.label())
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
231pub enum CodecPreference {
232 #[default]
234 H264,
235 VP9,
237 AV1,
239}
240
241impl FromStr for CodecPreference {
242 type Err = String;
243
244 fn from_str(s: &str) -> Result<Self, Self::Err> {
245 match s.to_lowercase().as_str() {
246 "h264" | "avc" => Ok(CodecPreference::H264),
247 "vp9" => Ok(CodecPreference::VP9),
248 "av1" => Ok(CodecPreference::AV1),
249 _ => Err(format!("Unknown codec preference: {}", s)),
250 }
251 }
252}
253
254impl fmt::Display for CodecPreference {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 match self {
257 CodecPreference::H264 => write!(f, "h264"),
258 CodecPreference::VP9 => write!(f, "vp9"),
259 CodecPreference::AV1 => write!(f, "av1"),
260 }
261 }
262}
263
264#[derive(Debug, Clone)]
284pub struct QualitySelector {
285 qualities: Vec<Quality>,
286 codec: CodecPreference,
287}
288
289impl QualitySelector {
290 #[must_use]
296 pub fn new(quality: Quality) -> Self {
297 Self {
298 qualities: vec![quality],
299 codec: CodecPreference::default(),
300 }
301 }
302
303 #[must_use]
307 pub fn fallback(mut self, quality: Quality) -> Self {
308 self.qualities.push(quality);
309 self
310 }
311
312 #[must_use]
316 pub fn codec(mut self, codec: CodecPreference) -> Self {
317 self.codec = codec;
318 self
319 }
320
321 #[must_use]
325 pub fn select(&self, stream_info: &StreamInfo) -> Option<StreamFormat> {
326 if self.qualities.first() == Some(&Quality::AudioOnly) {
328 return stream_info.audio.clone();
329 }
330
331 let qualities: Vec<Quality> = self
333 .qualities
334 .iter()
335 .flat_map(|q| {
336 if *q == Quality::Best {
337 Quality::all().iter().rev().copied().collect::<Vec<_>>()
338 } else {
339 vec![*q]
340 }
341 })
342 .collect();
343
344 for quality in qualities {
345 if quality == Quality::AudioOnly {
346 continue;
347 }
348
349 let label = quality.label();
350
351 let codec_order = match self.codec {
353 CodecPreference::AV1 => ["AV1", "VP9", "h264"],
354 CodecPreference::VP9 => ["VP9", "h264", "AV1"],
355 CodecPreference::H264 => ["h264", "VP9", "AV1"],
356 };
357
358 for codec in codec_order {
359 let key = format!("{} ({})", label, codec);
360 if let Some(format) = stream_info.video.get(&key) {
361 return Some(format.clone());
362 }
363 }
364 }
365
366 None
367 }
368
369 #[must_use]
371 pub fn codec_preference(&self) -> CodecPreference {
372 self.codec
373 }
374}
375
376impl Default for QualitySelector {
377 fn default() -> Self {
378 Self::new(Quality::Best)
379 }
380}
381
382#[must_use]
395pub fn parse_quality_selection(selection: &str) -> Vec<Quality> {
396 selection
397 .split('/')
398 .filter_map(|s| s.trim().parse().ok())
399 .collect()
400}
401
402#[must_use]
406pub fn get_itag_for_quality(
407 quality: Quality,
408 codec_pref: CodecPreference,
409 available_itags: &std::collections::HashMap<u32, String>,
410) -> Option<(u32, String)> {
411 let itags = quality.itags()?;
412
413 let order = match codec_pref {
414 CodecPreference::AV1 => [itags.av1, itags.vp9, itags.h264],
415 CodecPreference::VP9 => [itags.vp9, itags.h264, itags.av1],
416 CodecPreference::H264 => [itags.h264, itags.vp9, itags.av1],
417 };
418
419 for itag in order {
420 if let Some(url) = available_itags.get(&itag) {
421 return Some((itag, url.clone()));
422 }
423 }
424
425 None
426}