From 00a70862564b3f9bb892ce31e63cebe0ff308363 Mon Sep 17 00:00:00 2001 From: anhtahaylove Date: Wed, 1 Jul 2026 22:01:44 +0700 Subject: [PATCH] Resolve Firefox cookie profiles before yt-dlp uses them Firefox profiles.ini separates the user-facing Name from the Path that yt-dlp needs. The previous patch fixed the Settings selector, but downloads started before opening Settings could still pass the legacy display name. The profile parser now lives in a shared Rust utility so browser detection and yt-dlp cookie args use the same value, including absolute paths from IsRelative=0 entries. Constraint: Firefox profiles can use Name=default-release with Path=Profiles/. Constraint: yt-dlp accepts a Firefox profile path for profiles outside the default root. Rejected: Frontend-only localStorage migration | download, metadata, channel, and polling flows can run before Settings opens. Confidence: high Scope-risk: narrow Directive: Keep display_name user-facing; only folder_name/cookie value should be passed to yt-dlp. Tested: bun run biome check --write . Tested: bun run tsc -b Tested: cargo test firefox_profile --lib Tested: cargo check Not-tested: Manual Firefox cookie download with an IsRelative=0 profile. --- src-tauri/src/commands/dependencies.rs | 86 +----- src-tauri/src/services/ytdlp.rs | 9 +- src-tauri/src/utils/firefox_profiles.rs | 275 ++++++++++++++++++ src-tauri/src/utils/mod.rs | 2 + .../settings/sections/NetworkSection.tsx | 22 +- 5 files changed, 312 insertions(+), 82 deletions(-) create mode 100644 src-tauri/src/utils/firefox_profiles.rs diff --git a/src-tauri/src/commands/dependencies.rs b/src-tauri/src/commands/dependencies.rs index ee5ed37b..53e0889a 100644 --- a/src-tauri/src/commands/dependencies.rs +++ b/src-tauri/src/commands/dependencies.rs @@ -12,7 +12,10 @@ use crate::types::{ BackendError, DenoStatus, DependencySource, FfmpegStatus, GalleryDlStatus, YtdlpAllVersions, YtdlpChannel, YtdlpChannelUpdateInfo, YtdlpVersionInfo, }; -use crate::utils::{extract_deno_zip, extract_tar_gz, extract_tar_xz, extract_zip, CommandExt}; +use crate::utils::{ + extract_deno_zip, extract_tar_gz, extract_tar_xz, extract_zip, firefox_profiles_from_ini, + CommandExt, +}; use futures_util::StreamExt; use serde::{Deserialize, Serialize}; use std::process::Stdio; @@ -1166,84 +1169,11 @@ pub async fn get_browser_profiles(browser: String) -> Result } fn get_firefox_profiles(content: &str) -> Vec { - #[derive(Default)] - struct FirefoxProfileEntry { - name: Option, - path: Option, - is_default: bool, - index: usize, - } - - let mut entries = Vec::new(); - let mut install_default_paths = Vec::new(); - let mut current_profile: Option = None; - let mut in_install_section = false; - let mut next_index = 0; - - for raw_line in content.lines() { - let line = raw_line.trim(); - - if line.starts_with('[') && line.ends_with(']') { - if let Some(entry) = current_profile.take() { - if entry.name.as_deref().is_some_and(|name| !name.is_empty()) { - entries.push(entry); - } - } - - in_install_section = line.starts_with("[Install"); - current_profile = if line.starts_with("[Profile") { - let entry = FirefoxProfileEntry { - index: next_index, - ..Default::default() - }; - next_index += 1; - Some(entry) - } else { - None - }; - continue; - } - - if let Some(entry) = current_profile.as_mut() { - if let Some(name) = line.strip_prefix("Name=") { - entry.name = Some(name.to_string()); - } else if let Some(path) = line.strip_prefix("Path=") { - entry.path = Some(path.to_string()); - } else if line == "Default=1" { - entry.is_default = true; - } - } else if in_install_section { - if let Some(path) = line.strip_prefix("Default=") { - if !path.is_empty() { - install_default_paths.push(path.to_string()); - } - } - } - } - - if let Some(entry) = current_profile { - if entry.name.as_deref().is_some_and(|name| !name.is_empty()) { - entries.push(entry); - } - } - - entries.sort_by_key(|entry| { - let is_install_default = entry.path.as_ref().is_some_and(|path| { - install_default_paths - .iter() - .any(|default_path| default_path == path) - }); - - (!is_install_default, !entry.is_default, entry.index) - }); - - entries + firefox_profiles_from_ini(content) .into_iter() - .filter_map(|entry| { - entry.name.map(|name| BrowserProfile { - folder_name: name.clone(), - display_name: name, - }) + .map(|profile| BrowserProfile { + folder_name: profile.folder_name, + display_name: profile.display_name, }) .collect() } diff --git a/src-tauri/src/services/ytdlp.rs b/src-tauri/src/services/ytdlp.rs index eae88c3d..e7bbf2ad 100644 --- a/src-tauri/src/services/ytdlp.rs +++ b/src-tauri/src/services/ytdlp.rs @@ -2,7 +2,9 @@ use crate::types::{ BackendError, DependencySource, YtdlpAllVersions, YtdlpChannel, YtdlpChannelInfo, YtdlpVersionInfo, }; -use crate::utils::{find_system_binary, unix_system_binary_dirs, CommandExt}; +use crate::utils::{ + find_system_binary, resolve_firefox_profile_for_cookies, unix_system_binary_dirs, CommandExt, +}; use std::path::PathBuf; use std::process::Stdio; use tauri::{AppHandle, Manager}; @@ -907,6 +909,11 @@ pub fn build_cookie_args( let mut cookie_arg = browser.to_string(); if let Some(profile) = cookie_browser_profile { if !profile.is_empty() { + let profile = if browser.eq_ignore_ascii_case("firefox") { + resolve_firefox_profile_for_cookies(profile) + } else { + profile.to_string() + }; cookie_arg = format!("{}:{}", browser, profile); } } diff --git a/src-tauri/src/utils/firefox_profiles.rs b/src-tauri/src/utils/firefox_profiles.rs new file mode 100644 index 00000000..ef9d3886 --- /dev/null +++ b/src-tauri/src/utils/firefox_profiles.rs @@ -0,0 +1,275 @@ +use std::path::PathBuf; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FirefoxProfile { + pub folder_name: String, + pub display_name: String, +} + +#[derive(Default)] +struct FirefoxProfileEntry { + name: Option, + path: Option, + is_relative: Option, + is_default: bool, + index: usize, +} + +fn firefox_profile_folder_name(path: &str) -> Option { + path.replace('\\', "/") + .rsplit('/') + .find(|segment| !segment.is_empty()) + .map(str::to_string) +} + +fn firefox_profile_cookie_value(path: &str, is_relative: Option) -> Option { + let path = path.trim(); + if path.is_empty() { + return None; + } + + if is_relative == Some(false) { + return Some(path.to_string()); + } + + firefox_profile_folder_name(path) +} + +pub fn firefox_profiles_from_ini(content: &str) -> Vec { + let mut entries = Vec::new(); + let mut install_default_paths = Vec::new(); + let mut current_profile: Option = None; + let mut in_install_section = false; + let mut next_index = 0; + + for raw_line in content.lines() { + let line = raw_line.trim(); + + if line.starts_with('[') && line.ends_with(']') { + if let Some(entry) = current_profile.take() { + if entry.name.as_deref().is_some_and(|name| !name.is_empty()) { + entries.push(entry); + } + } + + in_install_section = line.starts_with("[Install"); + current_profile = if line.starts_with("[Profile") { + let entry = FirefoxProfileEntry { + index: next_index, + ..Default::default() + }; + next_index += 1; + Some(entry) + } else { + None + }; + continue; + } + + if let Some(entry) = current_profile.as_mut() { + if let Some(name) = line.strip_prefix("Name=") { + entry.name = Some(name.to_string()); + } else if let Some(path) = line.strip_prefix("Path=") { + entry.path = Some(path.to_string()); + } else if let Some(is_relative) = line.strip_prefix("IsRelative=") { + entry.is_relative = match is_relative { + "0" => Some(false), + "1" => Some(true), + _ => None, + }; + } else if line == "Default=1" { + entry.is_default = true; + } + } else if in_install_section { + if let Some(path) = line.strip_prefix("Default=") { + if !path.is_empty() { + install_default_paths.push(path.to_string()); + } + } + } + } + + if let Some(entry) = current_profile { + if entry.name.as_deref().is_some_and(|name| !name.is_empty()) { + entries.push(entry); + } + } + + entries.sort_by_key(|entry| { + let is_install_default = entry.path.as_ref().is_some_and(|path| { + install_default_paths + .iter() + .any(|default_path| default_path == path) + }); + + (!is_install_default, !entry.is_default, entry.index) + }); + + entries + .into_iter() + .filter_map(|entry| { + let display_name = entry.name?; + let folder_name = entry + .path + .as_deref() + .and_then(|path| firefox_profile_cookie_value(path, entry.is_relative))?; + Some(FirefoxProfile { + folder_name, + display_name, + }) + }) + .collect() +} + +pub fn resolve_firefox_profile_from_ini(content: &str, selected_profile: &str) -> Option { + let selected_profile = selected_profile.trim(); + if selected_profile.is_empty() { + return None; + } + + firefox_profiles_from_ini(content) + .into_iter() + .find(|profile| { + profile.folder_name == selected_profile || profile.display_name == selected_profile + }) + .map(|profile| profile.folder_name) +} + +pub fn firefox_profiles_ini_path() -> Option { + #[cfg(target_os = "macos")] + { + let home = std::env::var("HOME").ok()?; + return Some( + PathBuf::from(home) + .join("Library") + .join("Application Support") + .join("Firefox") + .join("profiles.ini"), + ); + } + + #[cfg(target_os = "windows")] + { + let app_data = std::env::var("APPDATA").ok()?; + return Some( + PathBuf::from(app_data) + .join("Mozilla") + .join("Firefox") + .join("profiles.ini"), + ); + } + + #[cfg(target_os = "linux")] + { + let home = std::env::var("HOME").ok()?; + return Some( + PathBuf::from(home) + .join(".mozilla") + .join("firefox") + .join("profiles.ini"), + ); + } + + #[allow(unreachable_code)] + None +} + +pub fn resolve_firefox_profile_for_cookies(selected_profile: &str) -> String { + let Some(profiles_ini) = firefox_profiles_ini_path() else { + return selected_profile.to_string(); + }; + + std::fs::read_to_string(profiles_ini) + .ok() + .and_then(|content| resolve_firefox_profile_from_ini(&content, selected_profile)) + .unwrap_or_else(|| selected_profile.to_string()) +} + +#[cfg(test)] +mod tests { + use super::{ + firefox_profile_cookie_value, firefox_profile_folder_name, firefox_profiles_from_ini, + resolve_firefox_profile_from_ini, + }; + + #[test] + fn firefox_relative_profile_uses_the_real_directory_name() { + assert_eq!( + firefox_profile_folder_name("Profiles/i879pxds.default-release"), + Some("i879pxds.default-release".to_string()) + ); + assert_eq!( + firefox_profile_folder_name(r"Profiles\i879pxds.default-release"), + Some("i879pxds.default-release".to_string()) + ); + } + + #[test] + fn firefox_absolute_profile_path_is_preserved_for_ytdlp() { + assert_eq!( + firefox_profile_cookie_value( + r"C:\Users\Me\AppData\Roaming\Mozilla\Firefox\Profiles\external.default", + Some(false), + ), + Some( + r"C:\Users\Me\AppData\Roaming\Mozilla\Firefox\Profiles\external.default" + .to_string() + ) + ); + } + + #[test] + fn firefox_profiles_ini_separates_display_name_from_cookie_value() { + let profiles = firefox_profiles_from_ini( + r#" +[Profile0] +Name=default-release +IsRelative=1 +Path=Profiles/i879pxds.default-release +Default=1 +"#, + ); + + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0].display_name, "default-release"); + assert_eq!(profiles[0].folder_name, "i879pxds.default-release"); + } + + #[test] + fn firefox_profiles_ini_preserves_absolute_paths() { + let profiles = firefox_profiles_from_ini( + r#" +[Profile0] +Name=external +IsRelative=0 +Path=C:\Firefox\Profiles\external.default +"#, + ); + + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0].display_name, "external"); + assert_eq!( + profiles[0].folder_name, + r"C:\Firefox\Profiles\external.default" + ); + } + + #[test] + fn firefox_profile_resolution_migrates_legacy_display_names() { + let content = r#" +[Profile0] +Name=default-release +IsRelative=1 +Path=Profiles/i879pxds.default-release +"#; + + assert_eq!( + resolve_firefox_profile_from_ini(content, "default-release"), + Some("i879pxds.default-release".to_string()) + ); + assert_eq!( + resolve_firefox_profile_from_ini(content, "i879pxds.default-release"), + Some("i879pxds.default-release".to_string()) + ); + } +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 53eb4148..82812688 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,6 +1,7 @@ mod command; mod extract; mod filename; +mod firefox_profiles; mod format; mod path; mod progress; @@ -9,6 +10,7 @@ mod security; pub use command::*; pub use extract::*; pub use filename::*; +pub use firefox_profiles::*; pub use format::*; pub use path::*; pub use progress::*; diff --git a/src/components/settings/sections/NetworkSection.tsx b/src/components/settings/sections/NetworkSection.tsx index 2019866d..6fbb5048 100644 --- a/src/components/settings/sections/NetworkSection.tsx +++ b/src/components/settings/sections/NetworkSection.tsx @@ -69,9 +69,25 @@ export function NetworkSection({ highlightId }: NetworkSectionProps) { }); setBrowserProfiles(profiles); - // Auto-select first profile if none selected - if (profiles.length > 0 && !cookieSettings.browserProfile) { - updateCookieSettings({ browserProfile: profiles[0].folder_name }); + if (profiles.length > 0) { + const selectedProfile = cookieSettings.browserProfile; + const exactProfile = profiles.find((profile) => profile.folder_name === selectedProfile); + const legacyFirefoxProfile = + cookieSettings.browser === 'firefox' + ? profiles.find((profile) => profile.display_name === selectedProfile) + : undefined; + + if (!selectedProfile) { + setUseCustomProfile(false); + updateCookieSettings({ browserProfile: profiles[0].folder_name }); + } else if (exactProfile) { + setUseCustomProfile(false); + } else if (legacyFirefoxProfile) { + setUseCustomProfile(false); + updateCookieSettings({ browserProfile: legacyFirefoxProfile.folder_name }); + } else { + setUseCustomProfile(true); + } } } catch (error) { console.error('Failed to load profiles:', error);