Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1464,9 +1464,29 @@ fn select_muxed_format(formats: &[Format], want: Option<u32>) -> Option<StreamSe
}
}

/// Default combo selection for a fresh resolve: "Best match" (index
/// 0) when the preference is Best, else the listed height closest to
/// the preferred one (ties go taller). The combo lists Best first,
/// then the options in order — so the preference preselects a row
/// instead of masquerading as Best match. Pure for tests.
pub fn default_quality_index(formats: &[VideoFormatOption], quality: &str) -> usize {
let want = match quality_height(quality) {
None => return 0,
Some(h) => h,
};
formats
.iter()
.enumerate()
.map(|(i, opt)| (i + 1, opt.height))
.min_by_key(|(_, h)| (h.abs_diff(want), std::cmp::Reverse(*h)))
.map(|(i, _)| i)
.unwrap_or(0)
}

/// Stored quality value to a height cap: `None` (Best) takes the
/// tallest variant available.
fn quality_height(value: &str) -> Option<u32> {
/// tallest variant available. Unknown values fall back to 1080p (same
/// fallback as the combo mapping and the extractor selector).
pub(crate) fn quality_height(value: &str) -> Option<u32> {
match value {
"best" => None,
"2160p" => Some(2160),
Expand Down
39 changes: 39 additions & 0 deletions src/video_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2556,3 +2556,42 @@ fn resolve_hls_input_prefers_binary_dump() {
assert_eq!(input.referer.as_deref(), Some("https://www.tiktok.com/"));
let _ = std::fs::remove_dir_all(&dir);
}

// ── default combo selection ──────────────────────────────────────────

fn test_options() -> Vec<VideoFormatOption> {
[480u32, 720, 1080]
.iter()
.map(|h| VideoFormatOption {
id: format!("v{h}"),
label: format!("{h}p"),
height: *h,
})
.collect()
}

#[test]
fn default_quality_index_preselects() {
let opts = test_options();
// Best (and empty listings) stay on "Best match".
assert_eq!(default_quality_index(&opts, "best"), 0);
assert_eq!(default_quality_index(&[], "720p"), 0);
// Otherwise the closest listed height wins (combo index 1-based).
assert_eq!(default_quality_index(&opts, "1080p"), 3);
assert_eq!(default_quality_index(&opts, "720p"), 2);
assert_eq!(default_quality_index(&opts, "480p"), 1);
// Between buckets the nearer height wins, ties go taller.
assert_eq!(default_quality_index(&opts, "2160p"), 3);
// Unknown stored values degrade like the extractor selector (1080p).
assert_eq!(default_quality_index(&opts, "mystery"), 3);
// Exact ties (odd extractor heights equidistant from the cap) go taller.
let odd = [600u32, 840]
.iter()
.map(|h| VideoFormatOption {
id: format!("v{h}"),
label: format!("{h}p"),
height: *h,
})
.collect::<Vec<_>>();
assert_eq!(default_quality_index(&odd, "720p"), 2);
}
21 changes: 14 additions & 7 deletions src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1677,7 +1677,12 @@ pub fn show_add_dialog(manager: Rc<DownloadManager>) {
.quality
.set_model(Some(&gtk4::StringList::new(&refs)));
*formats_b.borrow_mut() = ids;
step_b.quality.set_selected(0);
step_b
.quality
.set_selected(crate::video::default_quality_index(
&v.formats,
&settings_b.video_quality(),
) as u32);
*info_b.borrow_mut() = Some(v);
show_video_ready(&step_b);
set_lookup_add(&lookup_add_b, true);
Expand Down Expand Up @@ -2058,15 +2063,17 @@ pub fn show_add_dialog(manager: Rc<DownloadManager>) {
} else {
Some(typed.as_str())
};
// The fallback quality follows the picked format's
// height, so a dropped pin still degrades to the
// chosen height; "Best match" (index 0) keeps the
// live global preference. The combo lists Best
// match first, then the info formats in order.
// "Best match" (index 0) means best available,
// not the global preference: the preference
// already preselected a row at resolve time.
// Exact picks carry their height as the
// fallback, so a dropped pin still degrades to
// the chosen height. The combo lists Best match
// first, then the info formats in order.
let selected = step2.quality.selected() as usize;
let format_id = formats.borrow().get(selected).cloned().flatten();
let quality = match selected {
0 => m.settings().video_quality(),
0 => "best".to_string(),
i => v
.formats
.get(i - 1)
Expand Down
Loading