Skip to content

Commit c5bf9c9

Browse files
committed
feat: add custom cookies.txt file support for Instagram and other sites
- Add cookiesFilePath preference for custom Netscape format cookies file - Cookies file takes priority over browser cookies - Add file picker UI in Settings > Advanced > Cookies - Apply cookies to media info fetching, playlist detection, and downloads - Fix filename collisions by including video ID in default template - Improve Instagram thumbnail detection from thumbnails array - Add translations for TR, EN, DE
1 parent 6e5e708 commit c5bf9c9

File tree

21 files changed

+614
-66
lines changed

21 files changed

+614
-66
lines changed

src-tauri/src/commands/folder.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ pub async fn pick_folder(app: tauri::AppHandle) -> Result<Option<String>, String
2323
}
2424
}
2525

26+
/// Open native file picker dialog for cookies.txt file
27+
#[tauri::command]
28+
pub async fn pick_cookies_file(app: tauri::AppHandle) -> Result<Option<String>, String> {
29+
let file = app
30+
.dialog()
31+
.file()
32+
.set_title("Select Cookies File")
33+
.add_filter("Cookies File", &["txt"])
34+
.blocking_pick_file();
35+
36+
match file {
37+
Some(path) => Ok(Some(path.to_string())),
38+
None => Ok(None), // User cancelled
39+
}
40+
}
41+
2642
/// Check if a folder is accessible (exists and writable)
2743
#[tauri::command]
2844
pub fn check_folder_accessible(path: String) -> Result<bool, String> {

src-tauri/src/commands/media_info.rs

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,45 @@ use std::process::Stdio;
99

1010
use crate::models::{DownloadError, MediaInfo};
1111
use crate::utils::create_hidden_async_command;
12+
use tauri_plugin_store::StoreExt;
1213

1314
/// Fetches media information from a URL using yt-dlp
1415
///
1516
/// Runs `yt-dlp --simulate -J <url>` to get JSON metadata without downloading.
17+
/// Uses cookies from preferences if available (for Instagram, etc.)
1618
///
1719
/// **Validates: Requirements 12.3**
1820
#[tauri::command]
19-
pub async fn fetch_media_info(url: String) -> Result<MediaInfo, String> {
21+
pub async fn fetch_media_info(url: String, app: tauri::AppHandle) -> Result<MediaInfo, String> {
2022
if url.trim().is_empty() {
2123
return Err(DownloadError::InvalidUrl("URL cannot be empty".to_string()).to_string());
2224
}
2325

26+
// Get cookies settings from preferences
27+
let (cookies_file_path, cookies_from_browser) = get_cookies_from_preferences(&app);
28+
29+
// Build command arguments
30+
let mut args = vec!["--simulate", "-J", "--no-playlist"];
31+
32+
// Add cookies arguments if available
33+
let cookies_file_arg: String;
34+
let cookies_browser_arg: String;
35+
36+
if let Some(ref file_path) = cookies_file_path {
37+
cookies_file_arg = file_path.clone();
38+
args.push("--cookies");
39+
args.push(&cookies_file_arg);
40+
} else if let Some(ref browser) = cookies_from_browser {
41+
cookies_browser_arg = browser.clone();
42+
args.push("--cookies-from-browser");
43+
args.push(&cookies_browser_arg);
44+
}
45+
46+
args.push(&url);
47+
2448
// Run yt-dlp with --simulate -J to get JSON metadata (hidden window)
2549
let output = create_hidden_async_command("yt-dlp")
26-
.args(["--simulate", "-J", "--no-playlist", &url])
50+
.args(&args)
2751
.stdout(Stdio::piped())
2852
.stderr(Stdio::piped())
2953
.output()
@@ -55,9 +79,27 @@ fn parse_media_info_json(json_str: &str) -> Result<MediaInfo, String> {
5579
.unwrap_or("Unknown Title")
5680
.to_string();
5781

82+
// Try multiple sources for thumbnail:
83+
// 1. Direct thumbnail field
84+
// 2. thumbnails array (last item is usually highest quality)
85+
// 3. For Instagram: display_url field
5886
let thumbnail = json["thumbnail"]
5987
.as_str()
60-
.map(|s| s.to_string());
88+
.map(|s| s.to_string())
89+
.or_else(|| {
90+
// Try thumbnails array - get the last (highest quality) one
91+
json["thumbnails"]
92+
.as_array()
93+
.and_then(|arr| {
94+
arr.iter()
95+
.rev()
96+
.find_map(|t| t["url"].as_str().map(|s| s.to_string()))
97+
})
98+
})
99+
.or_else(|| {
100+
// Instagram specific: display_url
101+
json["display_url"].as_str().map(|s| s.to_string())
102+
});
61103

62104
let duration = json["duration"].as_f64();
63105

@@ -90,6 +132,33 @@ fn parse_media_info_json(json_str: &str) -> Result<MediaInfo, String> {
90132
})
91133
}
92134

135+
/// Gets cookies settings from preferences store
136+
fn get_cookies_from_preferences(app: &tauri::AppHandle) -> (Option<String>, Option<String>) {
137+
let store = match app.store("preferences.json") {
138+
Ok(s) => s,
139+
Err(_) => return (None, None),
140+
};
141+
142+
let prefs = match store.get("preferences") {
143+
Some(v) => v,
144+
None => return (None, None),
145+
};
146+
147+
let cookies_file_path = prefs
148+
.get("cookiesFilePath")
149+
.and_then(|v| v.as_str())
150+
.filter(|s| !s.is_empty())
151+
.map(|s| s.to_string());
152+
153+
let cookies_from_browser = prefs
154+
.get("cookiesFromBrowser")
155+
.and_then(|v| v.as_str())
156+
.filter(|s| !s.is_empty())
157+
.map(|s| s.to_string());
158+
159+
(cookies_file_path, cookies_from_browser)
160+
}
161+
93162
/// Parses yt-dlp stderr to extract a user-friendly error message
94163
fn parse_ytdlp_error(stderr: &str) -> String {
95164
let stderr_lower = stderr.to_lowercase();

src-tauri/src/commands/playlist.rs

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,37 @@
44
55
use serde::{Deserialize, Serialize};
66
use std::process::Stdio;
7+
use tauri_plugin_store::StoreExt;
78

89
use crate::utils::create_hidden_async_command;
910

11+
/// Gets cookies settings from preferences store
12+
fn get_cookies_from_preferences(app: &tauri::AppHandle) -> (Option<String>, Option<String>) {
13+
let store = match app.store("preferences.json") {
14+
Ok(s) => s,
15+
Err(_) => return (None, None),
16+
};
17+
18+
let prefs = match store.get("preferences") {
19+
Some(v) => v,
20+
None => return (None, None),
21+
};
22+
23+
let cookies_file_path = prefs
24+
.get("cookiesFilePath")
25+
.and_then(|v| v.as_str())
26+
.filter(|s| !s.is_empty())
27+
.map(|s| s.to_string());
28+
29+
let cookies_from_browser = prefs
30+
.get("cookiesFromBrowser")
31+
.and_then(|v| v.as_str())
32+
.filter(|s| !s.is_empty())
33+
.map(|s| s.to_string());
34+
35+
(cookies_file_path, cookies_from_browser)
36+
}
37+
1038
/// A single video entry in a playlist
1139
#[derive(Debug, Clone, Serialize, Deserialize)]
1240
#[serde(rename_all = "camelCase")]
@@ -49,7 +77,7 @@ pub struct PlaylistInfo {
4977

5078
/// Check if a URL is a playlist
5179
#[tauri::command]
52-
pub async fn check_is_playlist(url: String) -> Result<bool, String> {
80+
pub async fn check_is_playlist(url: String, app: tauri::AppHandle) -> Result<bool, String> {
5381
if url.trim().is_empty() {
5482
return Ok(false);
5583
}
@@ -67,9 +95,30 @@ pub async fn check_is_playlist(url: String) -> Result<bool, String> {
6795
}
6896
}
6997

98+
// Get cookies settings from preferences
99+
let (cookies_file_path, cookies_from_browser) = get_cookies_from_preferences(&app);
100+
101+
// Build command arguments
102+
let mut args = vec!["--flat-playlist", "-J", "--no-download"];
103+
104+
let cookies_file_arg: String;
105+
let cookies_browser_arg: String;
106+
107+
if let Some(ref file_path) = cookies_file_path {
108+
cookies_file_arg = file_path.clone();
109+
args.push("--cookies");
110+
args.push(&cookies_file_arg);
111+
} else if let Some(ref browser) = cookies_from_browser {
112+
cookies_browser_arg = browser.clone();
113+
args.push("--cookies-from-browser");
114+
args.push(&cookies_browser_arg);
115+
}
116+
117+
args.push(&url);
118+
70119
// For other sites, use yt-dlp to check
71120
let output = create_hidden_async_command("yt-dlp")
72-
.args(["--flat-playlist", "-J", "--no-download", &url])
121+
.args(&args)
73122
.stdout(Stdio::piped())
74123
.stderr(Stdio::piped())
75124
.output()
@@ -90,19 +139,35 @@ pub async fn check_is_playlist(url: String) -> Result<bool, String> {
90139

91140
/// Fetch playlist information and video list
92141
#[tauri::command]
93-
pub async fn fetch_playlist_info(url: String) -> Result<PlaylistInfo, String> {
142+
pub async fn fetch_playlist_info(url: String, app: tauri::AppHandle) -> Result<PlaylistInfo, String> {
94143
if url.trim().is_empty() {
95144
return Err("URL cannot be empty".to_string());
96145
}
97146

147+
// Get cookies settings from preferences
148+
let (cookies_file_path, cookies_from_browser) = get_cookies_from_preferences(&app);
149+
150+
// Build command arguments
151+
let mut args = vec!["--flat-playlist", "-J", "--no-download"];
152+
153+
let cookies_file_arg: String;
154+
let cookies_browser_arg: String;
155+
156+
if let Some(ref file_path) = cookies_file_path {
157+
cookies_file_arg = file_path.clone();
158+
args.push("--cookies");
159+
args.push(&cookies_file_arg);
160+
} else if let Some(ref browser) = cookies_from_browser {
161+
cookies_browser_arg = browser.clone();
162+
args.push("--cookies-from-browser");
163+
args.push(&cookies_browser_arg);
164+
}
165+
166+
args.push(&url);
167+
98168
// Use --flat-playlist to get playlist info without downloading video details
99169
let output = create_hidden_async_command("yt-dlp")
100-
.args([
101-
"--flat-playlist",
102-
"-J",
103-
"--no-download",
104-
&url,
105-
])
170+
.args(&args)
106171
.stdout(Stdio::piped())
107172
.stderr(Stdio::piped())
108173
.output()

src-tauri/src/commands/preferences.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub struct Preferences {
3636
/// Custom filename template (e.g., "{title} - {uploader} [{quality}]")
3737
#[serde(default)]
3838
pub filename_template: Option<String>,
39+
/// Path to custom cookies.txt file (Netscape format)
40+
#[serde(default)]
41+
pub cookies_file_path: Option<String>,
3942
}
4043

4144
fn default_true() -> bool {
@@ -55,6 +58,7 @@ impl Default for Preferences {
5558
proxy_enabled: false,
5659
proxy_url: None,
5760
filename_template: None,
61+
cookies_file_path: None,
5862
}
5963
}
6064
}
@@ -147,6 +151,7 @@ mod tests {
147151
proxy_enabled: false,
148152
proxy_url: None,
149153
filename_template: Some("{title} - {uploader}".to_string()),
154+
cookies_file_path: None,
150155
};
151156

152157
let json = serde_json::to_string(&prefs).unwrap();

0 commit comments

Comments
 (0)