Skip to main content

haki_dl/
manifest.rs

1//! Manifest, stream, playlist, segment, and encryption models.
2
3use std::cmp::Ordering;
4
5/// Media type associated with a stream or artifact.
6#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
7#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
8pub enum MediaType {
9    /// Audio stream.
10    Audio,
11    /// Video stream.
12    Video,
13    /// Subtitle stream.
14    Subtitles,
15    /// Closed captions stream.
16    ClosedCaptions,
17}
18
19/// Source extractor family.
20#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ExtractorType {
23    /// MPEG-DASH manifest.
24    MpegDash,
25    /// HLS manifest.
26    Hls,
27    /// Direct HTTP live TS input.
28    HttpLive,
29    /// Smooth Streaming manifest.
30    Mss,
31}
32
33/// Yes/no choice flag used by manifest metadata.
34#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum Choice {
37    /// Explicit no.
38    No,
39    /// Explicit yes.
40    Yes,
41}
42
43/// DASH role value.
44#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum RoleType {
47    Subtitle,
48    Main,
49    Alternate,
50    Supplementary,
51    Commentary,
52    Dub,
53    Description,
54    Sign,
55    Metadata,
56    ForcedSubtitle,
57    /// Numeric role value without a named variant.
58    Numeric(i32),
59}
60
61impl RoleType {
62    /// Converts an integer role value into a named role when one exists.
63    pub const fn from_number(value: i32) -> Self {
64        match value {
65            0 => Self::Subtitle,
66            1 => Self::Main,
67            2 => Self::Alternate,
68            3 => Self::Supplementary,
69            4 => Self::Commentary,
70            5 => Self::Dub,
71            6 => Self::Description,
72            7 => Self::Sign,
73            8 => Self::Metadata,
74            9 => Self::ForcedSubtitle,
75            other => Self::Numeric(other),
76        }
77    }
78
79    /// Parses a role enum token by name or integer value.
80    pub fn parse_enum_token(value: &str) -> Option<Self> {
81        let trimmed = value.trim();
82        parse_role_name(trimmed).or_else(|| trimmed.parse::<i32>().ok().map(Self::from_number))
83    }
84}
85
86fn parse_role_name(value: &str) -> Option<RoleType> {
87    match value.to_ascii_lowercase().as_str() {
88        "subtitle" => Some(RoleType::Subtitle),
89        "main" => Some(RoleType::Main),
90        "alternate" => Some(RoleType::Alternate),
91        "supplementary" => Some(RoleType::Supplementary),
92        "commentary" => Some(RoleType::Commentary),
93        "dub" => Some(RoleType::Dub),
94        "description" => Some(RoleType::Description),
95        "sign" => Some(RoleType::Sign),
96        "metadata" => Some(RoleType::Metadata),
97        "forcedsubtitle" => Some(RoleType::ForcedSubtitle),
98        _ => None,
99    }
100}
101
102/// Segment encryption method.
103#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
104#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
105pub enum EncryptionMethod {
106    /// No encryption.
107    #[default]
108    None,
109    Aes128,
110    Aes128Ecb,
111    SampleAes,
112    SampleAesCtr,
113    Cenc,
114    Chacha20,
115    Unknown,
116}
117
118impl EncryptionMethod {
119    /// Parses the method grammar used by manifests.
120    pub fn parse(value: Option<&str>) -> Self {
121        match value.map(normalize_token).as_deref() {
122            Some("none") => Self::None,
123            Some("aes128") => Self::Aes128,
124            Some("aes128ecb") => Self::Aes128Ecb,
125            Some("sampleaes") => Self::SampleAes,
126            Some("sampleaesctr") => Self::SampleAesCtr,
127            Some("cenc") => Self::Cenc,
128            Some("chacha20") => Self::Chacha20,
129            Some(_) => Self::Unknown,
130            None => Self::Unknown,
131        }
132    }
133}
134
135/// Origin of key material.
136#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
137#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
138pub enum KeySource {
139    /// No key material.
140    #[default]
141    None,
142    /// Inline manifest or CLI material.
143    Inline,
144    /// Key URI from a manifest.
145    Uri,
146    /// Key file from CLI/API.
147    File,
148    /// Key text file lookup.
149    KeyTextFile,
150    /// API-provided custom material.
151    Custom,
152}
153
154/// Encryption metadata attached to init or media segments.
155#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
156#[derive(Clone, Debug, Default, Eq, PartialEq)]
157pub struct EncryptionInfo {
158    /// Encryption method.
159    pub method: EncryptionMethod,
160    /// Content key bytes when already known.
161    pub key: Option<Vec<u8>>,
162    /// Initialization vector bytes when known.
163    pub iv: Option<Vec<u8>>,
164    /// Key identifier bytes when known.
165    pub kid: Option<Vec<u8>>,
166    /// Protection scheme such as cenc/cbcs when known.
167    pub scheme: Option<String>,
168    /// Raw protection data needed by downstream decryptors.
169    pub protection_data: Option<Vec<u8>>,
170    /// Key material source.
171    pub source: KeySource,
172}
173
174impl EncryptionInfo {
175    /// Returns true when the segment uses encryption.
176    pub fn is_encrypted(&self) -> bool {
177        self.method != EncryptionMethod::None
178    }
179}
180
181/// Inclusive byte range over a segment resource.
182#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub struct ByteRange {
185    /// First byte position.
186    pub start: i64,
187    /// Expected byte length.
188    pub length: i64,
189}
190
191impl ByteRange {
192    /// Returns the inclusive final byte position.
193    pub fn end(&self) -> Option<i64> {
194        Some(self.start.wrapping_add(self.length).wrapping_sub(1))
195    }
196}
197
198/// One media segment or initialization segment.
199#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
200#[derive(Clone, Debug)]
201pub struct MediaSegment {
202    /// Segment index.
203    pub index: i64,
204    /// Segment duration in seconds.
205    pub duration: f64,
206    /// Optional title.
207    pub title: Option<String>,
208    /// Program date-time as source text until a time crate is introduced.
209    pub program_date_time: Option<String>,
210    /// Byte-range start.
211    pub start_range: Option<i64>,
212    /// Expected byte length.
213    pub expected_length: Option<i64>,
214    /// Encryption metadata.
215    pub encryption: EncryptionInfo,
216    /// Segment URL.
217    pub url: String,
218    /// DASH name generated from a template variable.
219    pub name_from_var: Option<String>,
220}
221
222impl Default for MediaSegment {
223    fn default() -> Self {
224        Self {
225            index: 0,
226            duration: 0.0,
227            title: None,
228            program_date_time: None,
229            start_range: None,
230            expected_length: None,
231            encryption: EncryptionInfo::default(),
232            url: String::new(),
233            name_from_var: None,
234        }
235    }
236}
237
238impl MediaSegment {
239    /// Returns the inclusive byte-range end when start and length are known.
240    pub fn stop_range(&self) -> Option<i64> {
241        match (self.start_range, self.expected_length) {
242            (Some(start), Some(length)) => Some(start.wrapping_add(length).wrapping_sub(1)),
243            _ => None,
244        }
245    }
246
247    /// Returns true when the segment is encrypted.
248    pub fn is_encrypted(&self) -> bool {
249        self.encryption.is_encrypted()
250    }
251}
252
253impl PartialEq for MediaSegment {
254    fn eq(&self, other: &Self) -> bool {
255        self.index == other.index
256            && (self.duration - other.duration).abs() < 0.001
257            && self.title == other.title
258            && self.start_range == other.start_range
259            && self.stop_range() == other.stop_range()
260            && self.expected_length == other.expected_length
261            && self.url == other.url
262    }
263}
264
265/// Segment group separated by discontinuities.
266#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
267#[derive(Clone, Debug, Default, PartialEq)]
268pub struct MediaPart {
269    /// Segments in this part.
270    pub media_segments: Vec<MediaSegment>,
271}
272
273impl MediaPart {
274    /// Sum of segment durations in seconds.
275    pub fn total_duration(&self) -> f64 {
276        self.media_segments
277            .iter()
278            .map(|segment| segment.duration)
279            .sum()
280    }
281}
282
283/// Playlist attached to a stream.
284#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
285#[derive(Clone, Debug, PartialEq)]
286pub struct Playlist {
287    /// Playlist URL.
288    pub url: String,
289    /// Whether the playlist is live.
290    pub is_live: bool,
291    /// Live refresh interval in milliseconds.
292    pub refresh_interval_ms: f64,
293    /// Target duration in seconds.
294    pub target_duration: Option<f64>,
295    /// Initialization segment.
296    pub media_init: Option<MediaSegment>,
297    /// Discontinuity-separated media parts.
298    pub media_parts: Vec<MediaPart>,
299}
300
301impl Default for Playlist {
302    fn default() -> Self {
303        Self {
304            url: String::new(),
305            is_live: false,
306            refresh_interval_ms: 15_000.0,
307            target_duration: None,
308            media_init: None,
309            media_parts: Vec::new(),
310        }
311    }
312}
313
314impl Playlist {
315    /// Sum of all media segment durations in seconds.
316    pub fn total_duration(&self) -> f64 {
317        self.media_parts.iter().map(MediaPart::total_duration).sum()
318    }
319
320    /// Total number of media segments.
321    pub fn segments_count(&self) -> usize {
322        self.media_parts
323            .iter()
324            .map(|part| part.media_segments.len())
325            .sum()
326    }
327}
328
329/// Smooth Streaming metadata.
330#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
331#[derive(Clone, Debug, Default, Eq, PartialEq)]
332pub struct MssData {
333    pub four_cc: String,
334    pub codec_private_data: String,
335    pub stream_type: String,
336    pub timescale: i32,
337    pub sampling_rate: i32,
338    pub channels: i32,
339    pub bits_per_sample: i32,
340    pub nal_unit_length_field: i32,
341    pub duration: i64,
342    pub is_protection: bool,
343    pub protection_system_id: String,
344    pub protection_data: String,
345}
346
347/// Stream metadata used by selectors and progress events.
348#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
349#[derive(Clone, Debug, Default, PartialEq)]
350pub struct Stream {
351    /// Stable stream identifier.
352    pub id: String,
353    /// Stream media type when known.
354    pub media_type: Option<MediaType>,
355    /// Group identifier.
356    pub group_id: Option<String>,
357    /// Language tag when known.
358    pub language: Option<String>,
359    /// Human-readable stream name when known.
360    pub name: Option<String>,
361    /// Default flag when known.
362    pub default: Option<Choice>,
363    /// Forced subtitle/audio flag when known.
364    pub forced: Option<Choice>,
365    /// Duration skipped by selection/range logic.
366    pub skipped_duration: Option<f64>,
367    /// Smooth Streaming metadata.
368    pub mss_data: Option<MssData>,
369    /// Bandwidth in bits per second when known.
370    pub bandwidth: Option<i64>,
371    /// Codec string when known.
372    pub codecs: Option<String>,
373    /// Resolution label when known.
374    pub resolution: Option<String>,
375    /// Frame rate when known.
376    pub frame_rate: Option<f64>,
377    /// Channel label when known.
378    pub channels: Option<String>,
379    /// Stream extension hint when known.
380    pub extension: Option<String>,
381    /// DASH role when known.
382    pub role: Option<RoleType>,
383    /// Video range or color range metadata.
384    pub video_range: Option<String>,
385    /// HLS/DASH characteristics metadata.
386    pub characteristics: Option<String>,
387    /// Publish time as source text until a time crate is introduced.
388    pub publish_time: Option<String>,
389    /// Associated external audio group ID.
390    pub audio_id: Option<String>,
391    /// Associated external video group ID.
392    pub video_id: Option<String>,
393    /// Associated external subtitle group ID.
394    pub subtitle_id: Option<String>,
395    /// DASH period ID.
396    pub period_id: Option<String>,
397    /// Current URL.
398    pub url: String,
399    /// Original URL before URL processing.
400    pub original_url: String,
401    /// Expanded playlist.
402    pub playlist: Option<Playlist>,
403}
404
405impl Stream {
406    /// Number of media segments in the playlist.
407    pub fn segments_count(&self) -> usize {
408        self.playlist.as_ref().map_or(0, Playlist::segments_count)
409    }
410
411    /// Sum of media segment durations in seconds.
412    pub fn total_duration(&self) -> Option<f64> {
413        self.playlist.as_ref().map(Playlist::total_duration)
414    }
415}
416
417/// Parsed manifest model.
418#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
419#[derive(Clone, Debug, Default, PartialEq)]
420pub struct Manifest {
421    /// Extractor family that produced this manifest.
422    pub extractor_type: Option<ExtractorType>,
423    /// Source URL or path.
424    pub source: Option<String>,
425    /// Streams exposed by the manifest.
426    pub streams: Vec<Stream>,
427    /// Non-fatal parser warnings.
428    pub warnings: Vec<String>,
429    /// Whether any selected playlist should be treated as live.
430    pub is_live: bool,
431}
432
433/// Sorts streams using compatibility ordering.
434pub fn sort_streams_compatible(streams: &mut [Stream]) {
435    streams.sort_by(compare_streams_compatible);
436}
437
438/// Compares streams using media type, bandwidth descending, then audio channel order descending.
439pub fn compare_streams_compatible(left: &Stream, right: &Stream) -> Ordering {
440    media_rank(left.media_type)
441        .cmp(&media_rank(right.media_type))
442        .then_with(|| compare_option_i64_desc(left.bandwidth, right.bandwidth))
443        .then_with(|| audio_channel_order(right).cmp(&audio_channel_order(left)))
444}
445
446/// Parses the audio channel ordering key used by selection.
447pub fn audio_channel_order(stream: &Stream) -> i32 {
448    let Some(channels) = &stream.channels else {
449        return 0;
450    };
451    let first = channels.split('/').next().unwrap_or_default();
452    first.parse::<i32>().unwrap_or_default()
453}
454
455fn media_rank(media_type: Option<MediaType>) -> i32 {
456    match media_type {
457        None => -1,
458        Some(MediaType::Audio) => 0,
459        Some(MediaType::Video) => 1,
460        Some(MediaType::Subtitles) => 2,
461        Some(MediaType::ClosedCaptions) => 3,
462    }
463}
464
465fn compare_option_i64_desc(left: Option<i64>, right: Option<i64>) -> Ordering {
466    match (left, right) {
467        (Some(left), Some(right)) => right.cmp(&left),
468        (Some(_), None) => Ordering::Less,
469        (None, Some(_)) => Ordering::Greater,
470        (None, None) => Ordering::Equal,
471    }
472}
473
474fn normalize_token(value: &str) -> String {
475    value
476        .chars()
477        .filter(|ch| *ch != '-' && *ch != '_' && *ch != ' ')
478        .flat_map(char::to_lowercase)
479        .collect()
480}
481
482/// API stream selection model.
483#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
484#[derive(Clone, Debug, Default, Eq, PartialEq)]
485pub enum StreamSelector {
486    /// Preserve CLI-interactive behavior where available.
487    #[default]
488    Interactive,
489    /// Auto-select the best basic stream, audio languages, and subtitles.
490    Auto,
491    /// Select subtitles only.
492    SubtitlesOnly,
493    /// Select explicit stream identifiers.
494    ExplicitIds(Vec<String>),
495}