Skip to main content

haki_dl/
config.rs

1//! Request options and compatibility configuration.
2
3use std::collections::BTreeMap;
4use std::path::PathBuf;
5use std::time::Duration;
6
7use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
8
9use crate::error::{Error, Result};
10use crate::manifest::RoleType;
11use crate::mux::{MuxFormat, MuxImport};
12
13/// Compatibility mode applied during request planning.
14#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15pub enum CompatibilityProfile {
16    /// Preserve CLI-visible compatibility behavior.
17    #[default]
18    CliCompatible,
19    /// Use safer API defaults when explicitly selected by callers.
20    ApiSafe,
21}
22
23/// Console verbosity.
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
25pub enum LogLevel {
26    Debug,
27    #[default]
28    Info,
29    Warn,
30    Error,
31    Off,
32}
33
34/// Decryption backend requested by the user.
35#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
36pub enum DecryptionEngine {
37    #[default]
38    Mp4forge,
39    Mp4decrypt,
40    ShakaPackager,
41    Ffmpeg,
42}
43
44/// Subtitle text output format.
45#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
46pub enum SubtitleFormat {
47    /// SubRip output.
48    #[default]
49    Srt,
50    /// WebVTT output.
51    Vtt,
52}
53
54/// Language for user-facing CLI text.
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum UiLanguage {
57    /// English CLI output.
58    EnUs,
59}
60
61/// HLS encryption method override.
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub enum HlsMethod {
64    None,
65    Aes128,
66    Aes128Ecb,
67    Cenc,
68    SampleAes,
69    SampleAesCtr,
70    Chacha20,
71    Unknown,
72}
73
74/// Final mux backend.
75#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76pub enum MuxerKind {
77    /// External ffmpeg process.
78    #[default]
79    Ffmpeg,
80    /// External mkvmerge process.
81    Mkvmerge,
82    /// Optional in-process MP4 backend.
83    Mp4forge,
84}
85
86/// Segment or time clipping range supplied through `--custom-range`.
87#[derive(Clone, Debug, PartialEq)]
88pub enum CustomRange {
89    Segment {
90        input: String,
91        start_index: i64,
92        end_index: i64,
93    },
94    Time {
95        input: String,
96        start_seconds: f64,
97        end_seconds: f64,
98    },
99}
100
101/// A delayed start timestamp supplied as `yyyyMMddHHmmss`.
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct TaskStartAt {
104    raw: String,
105}
106
107impl TaskStartAt {
108    /// Creates a delayed-start value from the raw `yyyyMMddHHmmss` form.
109    pub fn new(raw: String) -> Self {
110        Self { raw }
111    }
112
113    /// Returns the raw `yyyyMMddHHmmss` form.
114    pub fn as_str(&self) -> &str {
115        &self.raw
116    }
117
118    /// Calculates how long remains until this timestamp using the current local clock.
119    pub fn duration_until_now(&self) -> Result<Option<Duration>> {
120        self.duration_until_datetime(current_local_datetime())
121    }
122
123    /// Calculates how long remains until this timestamp using another raw timestamp.
124    pub fn duration_until_raw(&self, now_raw: &str) -> Result<Option<Duration>> {
125        self.duration_until_datetime(parse_task_start_datetime(now_raw)?)
126    }
127
128    fn duration_until_datetime(&self, now: PrimitiveDateTime) -> Result<Option<Duration>> {
129        let target = parse_task_start_datetime(&self.raw)?;
130        if target <= now {
131            return Ok(None);
132        }
133        Ok(Some((target - now).unsigned_abs()))
134    }
135}
136
137fn current_local_datetime() -> PrimitiveDateTime {
138    let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
139    PrimitiveDateTime::new(now.date(), now.time())
140}
141
142fn parse_task_start_datetime(value: &str) -> Result<PrimitiveDateTime> {
143    if value.len() != 14 || !value.chars().all(|ch| ch.is_ascii_digit()) {
144        return Err(Error::config("task start time must use yyyyMMddHHmmss"));
145    }
146    let year = parse_task_start_part(value, 0, 4)?;
147    let month = parse_task_start_part(value, 4, 6)?;
148    let day = parse_task_start_part(value, 6, 8)?;
149    let hour = parse_task_start_part(value, 8, 10)?;
150    let minute = parse_task_start_part(value, 10, 12)?;
151    let second = parse_task_start_part(value, 12, 14)?;
152    let month = Month::try_from(
153        u8::try_from(month)
154            .map_err(|_| Error::config("task start time contains an out-of-range month"))?,
155    )
156    .map_err(|_| Error::config("task start time contains an out-of-range month"))?;
157    let date = Date::from_calendar_date(
158        i32::try_from(year)
159            .map_err(|_| Error::config("task start time contains an out-of-range year"))?,
160        month,
161        u8::try_from(day)
162            .map_err(|_| Error::config("task start time contains an out-of-range day"))?,
163    )
164    .map_err(|_| Error::config("task start time contains an out-of-range date"))?;
165    let time = Time::from_hms(
166        u8::try_from(hour)
167            .map_err(|_| Error::config("task start time contains an out-of-range hour"))?,
168        u8::try_from(minute)
169            .map_err(|_| Error::config("task start time contains an out-of-range minute"))?,
170        u8::try_from(second)
171            .map_err(|_| Error::config("task start time contains an out-of-range second"))?,
172    )
173    .map_err(|_| Error::config("task start time contains an out-of-range time"))?;
174    Ok(PrimitiveDateTime::new(date, time))
175}
176
177fn parse_task_start_part(value: &str, start: usize, end: usize) -> Result<u32> {
178    value
179        .get(start..end)
180        .ok_or_else(|| Error::config("task start time field is invalid"))?
181        .parse::<u32>()
182        .map_err(|_| Error::config("task start time field is invalid"))
183}
184
185/// Canonicalized custom content key.
186#[derive(Clone, Debug, Eq, PartialEq)]
187pub enum CustomKey {
188    Track { track_id: u32, key_hex: String },
189    Kid { kid_hex: String, key_hex: String },
190    Key { key_hex: String },
191}
192
193/// Stream selection/drop filter. Pattern fields intentionally stay as strings
194/// so API callers can build values before a regex engine is chosen.
195#[derive(Clone, Debug, Default, PartialEq)]
196pub struct StreamFilter {
197    pub for_choice: String,
198    pub id: Option<String>,
199    pub language: Option<String>,
200    pub name: Option<String>,
201    pub codecs: Option<String>,
202    pub resolution: Option<String>,
203    pub frame_rate: Option<String>,
204    pub channels: Option<String>,
205    pub range: Option<String>,
206    pub url: Option<String>,
207    pub segment_count_min: Option<i64>,
208    pub segment_count_max: Option<i64>,
209    pub playlist_duration_min: Option<f64>,
210    pub playlist_duration_max: Option<f64>,
211    pub bandwidth_min: Option<i64>,
212    pub bandwidth_max: Option<i64>,
213    pub role: Option<RoleType>,
214}
215
216/// Options for final mux-after-done behavior.
217#[derive(Clone, Debug, Eq, PartialEq)]
218pub struct MuxAfterDoneOptions {
219    /// Requested output format.
220    pub format: MuxFormat,
221    /// Requested mux backend.
222    pub muxer: MuxerKind,
223    /// Optional process-backed fallback used only when an explicit mp4forge mux fails.
224    ///
225    /// Currently only [`MuxerKind::Ffmpeg`] is accepted because mp4forge
226    /// mux-after-done is restricted to MP4 output.
227    pub fallback_muxer: Option<MuxerKind>,
228    /// Optional backend binary path for process-backed muxers.
229    pub bin_path: Option<PathBuf>,
230    /// Keep intermediate files after mux succeeds.
231    pub keep: bool,
232    /// Exclude subtitle artifacts from the final mux.
233    pub skip_sub: bool,
234}
235
236impl Default for MuxAfterDoneOptions {
237    fn default() -> Self {
238        Self {
239            format: MuxFormat::Mp4,
240            muxer: MuxerKind::Ffmpeg,
241            fallback_muxer: None,
242            bin_path: None,
243            keep: false,
244            skip_sub: false,
245        }
246    }
247}
248
249/// Typed download options using canonical snake_case option names.
250#[derive(Clone, Debug)]
251pub struct DownloadOptions {
252    /// Active compatibility profile.
253    pub compatibility_profile: CompatibilityProfile,
254    pub log_level: LogLevel,
255    /// Temporary directory root.
256    pub tmp_dir: Option<PathBuf>,
257    /// Save directory.
258    pub save_dir: Option<PathBuf>,
259    /// Save name without extension.
260    pub save_name: Option<String>,
261    /// Save pattern.
262    pub save_pattern: Option<String>,
263    pub log_file_path: Option<PathBuf>,
264    pub ui_language: Option<UiLanguage>,
265    pub urlprocessor_args: Option<String>,
266    /// Base URL override.
267    pub base_url: Option<String>,
268    pub headers: BTreeMap<String, String>,
269    pub custom_proxy: Option<String>,
270    pub use_system_proxy: bool,
271    /// Allow invalid TLS certificates for compatibility with problematic sources.
272    pub allow_insecure_tls: bool,
273    pub append_url_params: bool,
274    pub custom_range: Option<CustomRange>,
275    pub keys: Vec<CustomKey>,
276    pub key_text_file: Option<PathBuf>,
277    pub decryption_engine: DecryptionEngine,
278    pub decryption_binary_path: Option<PathBuf>,
279    pub mp4_real_time_decryption: bool,
280    pub use_shaka_packager: bool,
281    pub custom_hls_method: Option<HlsMethod>,
282    pub custom_hls_key: Option<Vec<u8>>,
283    pub custom_hls_iv: Option<Vec<u8>>,
284    pub allow_hls_multi_ext_map: bool,
285    /// Maximum concurrent segment workers per stream.
286    pub thread_count: i32,
287    /// Segment retry count.
288    pub download_retry_count: i32,
289    /// HTTP request timeout.
290    pub http_request_timeout: Duration,
291    pub max_speed: Option<u64>,
292    pub auto_select: bool,
293    /// Download selected streams concurrently.
294    pub concurrent_download: bool,
295    /// Select only subtitles.
296    pub sub_only: bool,
297    pub select_video: Vec<StreamFilter>,
298    pub select_audio: Vec<StreamFilter>,
299    pub select_subtitle: Vec<StreamFilter>,
300    pub drop_video: Vec<StreamFilter>,
301    pub drop_audio: Vec<StreamFilter>,
302    pub drop_subtitle: Vec<StreamFilter>,
303    pub ad_keywords: Vec<String>,
304    /// Skip post-download merge.
305    pub skip_merge: bool,
306    /// Skip download after metadata and selection planning.
307    pub skip_download: bool,
308    /// Check segment count.
309    pub check_segments_count: bool,
310    /// Use binary concatenation for per-stream merge.
311    pub binary_merge: bool,
312    /// Use ffmpeg concat demuxer for ffmpeg merge.
313    pub use_ffmpeg_concat_demuxer: bool,
314    /// Delete temporary files after success.
315    pub del_after_done: bool,
316    /// Omit date metadata where supported.
317    pub no_date_info: bool,
318    /// Disable log file output.
319    pub no_log: bool,
320    /// Write metadata JSON sidecars.
321    pub write_meta_json: bool,
322    /// Repair or extract subtitles when possible.
323    pub auto_subtitle_fix: bool,
324    /// Subtitle output format.
325    pub sub_format: SubtitleFormat,
326    /// Optional ffmpeg binary path.
327    pub ffmpeg_binary_path: Option<PathBuf>,
328    /// Optional mkvmerge binary path.
329    pub mkvmerge_binary_path: Option<PathBuf>,
330    /// Optional final mux-after-done options.
331    pub mux_after_done: Option<MuxAfterDoneOptions>,
332    pub mux_imports: Vec<MuxImport>,
333    /// Live streams should be handled as VOD when possible.
334    pub live_perform_as_vod: bool,
335    /// Merge live output as segments arrive.
336    pub live_real_time_merge: bool,
337    /// Keep live segments.
338    pub live_keep_segments: bool,
339    /// Pipe live media into a mux process where supported.
340    pub live_pipe_mux: bool,
341    pub live_record_limit: Option<Duration>,
342    pub live_wait_time: Option<i32>,
343    /// Number of live segments to take on startup.
344    pub live_take_count: i32,
345    pub live_fix_vtt_by_audio: bool,
346    pub task_start_at: Option<TaskStartAt>,
347    pub force_ansi_console: bool,
348    pub no_ansi_color: bool,
349    pub disable_update_check: bool,
350}
351
352impl Default for DownloadOptions {
353    fn default() -> Self {
354        Self {
355            compatibility_profile: CompatibilityProfile::CliCompatible,
356            log_level: LogLevel::Info,
357            tmp_dir: None,
358            save_dir: None,
359            save_name: None,
360            save_pattern: None,
361            log_file_path: None,
362            ui_language: None,
363            urlprocessor_args: None,
364            base_url: None,
365            headers: BTreeMap::new(),
366            custom_proxy: None,
367            use_system_proxy: true,
368            allow_insecure_tls: false,
369            append_url_params: false,
370            custom_range: None,
371            keys: Vec::new(),
372            key_text_file: None,
373            decryption_engine: DecryptionEngine::Mp4forge,
374            decryption_binary_path: None,
375            mp4_real_time_decryption: false,
376            use_shaka_packager: false,
377            custom_hls_method: None,
378            custom_hls_key: None,
379            custom_hls_iv: None,
380            allow_hls_multi_ext_map: false,
381            thread_count: default_thread_count(),
382            download_retry_count: 3,
383            http_request_timeout: Duration::from_secs(100),
384            max_speed: None,
385            auto_select: false,
386            concurrent_download: false,
387            sub_only: false,
388            select_video: Vec::new(),
389            select_audio: Vec::new(),
390            select_subtitle: Vec::new(),
391            drop_video: Vec::new(),
392            drop_audio: Vec::new(),
393            drop_subtitle: Vec::new(),
394            ad_keywords: Vec::new(),
395            skip_merge: false,
396            skip_download: false,
397            check_segments_count: true,
398            binary_merge: false,
399            use_ffmpeg_concat_demuxer: false,
400            del_after_done: true,
401            no_date_info: false,
402            no_log: false,
403            write_meta_json: true,
404            auto_subtitle_fix: true,
405            sub_format: SubtitleFormat::Srt,
406            ffmpeg_binary_path: None,
407            mkvmerge_binary_path: None,
408            mux_after_done: None,
409            mux_imports: Vec::new(),
410            live_perform_as_vod: false,
411            live_real_time_merge: false,
412            live_keep_segments: true,
413            live_pipe_mux: false,
414            live_record_limit: None,
415            live_wait_time: None,
416            live_take_count: 16,
417            live_fix_vtt_by_audio: false,
418            task_start_at: None,
419            force_ansi_console: false,
420            no_ansi_color: false,
421            disable_update_check: false,
422        }
423    }
424}
425
426const fn fallback_thread_count() -> i32 {
427    1
428}
429
430fn default_thread_count() -> i32 {
431    match std::thread::available_parallelism() {
432        Ok(count) => i32::try_from(count.get()).unwrap_or(i32::MAX),
433        Err(_) => fallback_thread_count(),
434    }
435}