1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct AppSettings {
6 pub schema_version: u32,
7 pub appearance: AppearanceSettings,
8 pub download: DownloadSettings,
9 pub advanced: AdvancedSettings,
10 #[serde(default)]
11 pub telegram: TelegramSettings,
12 #[serde(default)]
13 pub proxy: ProxySettings,
14 #[serde(default)]
15 pub onboarding_completed: bool,
16 #[serde(default)]
17 pub start_with_windows: bool,
18 #[serde(default)]
19 pub portable_mode: bool,
20 #[serde(default)]
21 pub legal_acknowledged: bool,
22 #[serde(default)]
23 pub last_download_options: LastDownloadOptions,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct LastDownloadOptions {
28 #[serde(default)]
29 pub mode: Option<String>,
30 #[serde(default)]
31 pub quality: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct AppearanceSettings {
36 pub theme: String,
37 pub language: String,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct DownloadSettings {
42 pub default_output_dir: PathBuf,
43 pub always_ask_path: bool,
44 pub video_quality: String,
45 pub skip_existing: bool,
46 pub download_attachments: bool,
47 pub download_descriptions: bool,
48 #[serde(default = "default_true")]
49 pub embed_metadata: bool,
50 #[serde(default = "default_true")]
51 pub embed_thumbnail: bool,
52 #[serde(default)]
53 pub clipboard_detection: bool,
54 #[serde(default = "default_filename_template")]
55 pub filename_template: String,
56 #[serde(default)]
57 pub organize_by_platform: bool,
58 #[serde(default)]
59 pub download_subtitles: bool,
60 #[serde(default)]
61 pub include_auto_subtitles: bool,
62 #[serde(default)]
63 pub translate_metadata: bool,
64 #[serde(default)]
65 pub youtube_sponsorblock: bool,
66 #[serde(default)]
67 pub split_by_chapters: bool,
68 #[serde(default)]
69 pub hotkey_enabled: bool,
70 #[serde(default = "default_hotkey_binding")]
71 pub hotkey_binding: String,
72 #[serde(default)]
73 pub extra_ytdlp_flags: Vec<String>,
74 #[serde(default = "default_true")]
75 pub copy_to_clipboard_on_hotkey: bool,
76 #[serde(default)]
77 pub cookie_file: String,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct AdvancedSettings {
82 pub max_concurrent_segments: u32,
83 pub max_retries: u32,
84 #[serde(default = "default_max_concurrent_downloads")]
85 pub max_concurrent_downloads: u32,
86 #[serde(default = "default_concurrent_fragments")]
87 pub concurrent_fragments: u32,
88 #[serde(default = "default_stagger_delay_ms")]
89 pub stagger_delay_ms: u64,
90 #[serde(default = "default_torrent_listen_port")]
91 pub torrent_listen_port: u16,
92 #[serde(default)]
93 pub cookies_from_browser: String,
94 #[serde(default)]
95 pub twitter_manual_cookie: String,
96}
97
98fn default_concurrent_fragments() -> u32 {
99 8
100}
101
102fn default_max_concurrent_downloads() -> u32 {
103 2
104}
105
106fn default_stagger_delay_ms() -> u64 {
107 150
108}
109
110fn default_torrent_listen_port() -> u16 {
111 6881
112}
113
114fn default_true() -> bool {
115 true
116}
117
118pub fn default_filename_template() -> String {
119 "%(title).200s [%(id)s].%(ext)s".into()
120}
121
122fn default_hotkey_binding() -> String {
123 "CmdOrCtrl+Shift+D".into()
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct TelegramSettings {
128 pub concurrent_downloads: u32,
129 pub fix_file_extensions: bool,
130}
131
132impl Default for TelegramSettings {
133 fn default() -> Self {
134 Self {
135 concurrent_downloads: 3,
136 fix_file_extensions: true,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, Default)]
142pub struct ProxySettings {
143 #[serde(default)]
144 pub enabled: bool,
145 #[serde(default = "default_proxy_type")]
146 pub proxy_type: String,
147 #[serde(default)]
148 pub host: String,
149 #[serde(default = "default_proxy_port")]
150 pub port: u16,
151 #[serde(default)]
152 pub username: String,
153 #[serde(default)]
154 pub password: String,
155}
156
157fn default_proxy_type() -> String {
158 "http".into()
159}
160
161fn default_proxy_port() -> u16 {
162 8080
163}
164
165impl Default for AppSettings {
166 fn default() -> Self {
167 Self {
168 schema_version: 1,
169 appearance: AppearanceSettings {
170 theme: "system".into(),
171 language: "en".into(),
172 },
173 download: DownloadSettings {
174 default_output_dir: dirs::download_dir().unwrap_or_else(|| PathBuf::from(".")),
175 always_ask_path: true,
176 video_quality: "720p".into(),
177 skip_existing: true,
178 download_attachments: true,
179 download_descriptions: true,
180 embed_metadata: true,
181 embed_thumbnail: true,
182 clipboard_detection: false,
183 filename_template: default_filename_template(),
184 organize_by_platform: false,
185 download_subtitles: false,
186 include_auto_subtitles: false,
187 translate_metadata: false,
188 youtube_sponsorblock: false,
189 split_by_chapters: false,
190 hotkey_enabled: false,
191 hotkey_binding: default_hotkey_binding(),
192 extra_ytdlp_flags: Vec::new(),
193 copy_to_clipboard_on_hotkey: true,
194 cookie_file: String::new(),
195 },
196 advanced: AdvancedSettings {
197 max_concurrent_segments: 20,
198 max_retries: 3,
199 max_concurrent_downloads: 2,
200 concurrent_fragments: 8,
201 stagger_delay_ms: 150,
202 torrent_listen_port: 6881,
203 cookies_from_browser: String::new(),
204 twitter_manual_cookie: String::new(),
205 },
206 telegram: TelegramSettings::default(),
207 proxy: ProxySettings::default(),
208 onboarding_completed: false,
209 start_with_windows: false,
210 portable_mode: false,
211 legal_acknowledged: false,
212 last_download_options: LastDownloadOptions::default(),
213 }
214 }
215}
216
217impl AppSettings {
218 pub fn load_from_disk() -> Self {
219 let data_dir = match crate::core::paths::app_data_dir() {
220 Some(d) => d,
221 None => return Self::default(),
222 };
223 let store_path = data_dir.join("settings.json");
224 let content = match std::fs::read_to_string(&store_path) {
225 Ok(c) => c,
226 Err(_) => return Self::default(),
227 };
228 let json: serde_json::Value = match serde_json::from_str(&content) {
229 Ok(v) => v,
230 Err(_) => return Self::default(),
231 };
232 match json.get("app_settings") {
233 Some(val) => serde_json::from_value::<Self>(val.clone()).unwrap_or_default(),
234 None => Self::default(),
235 }
236 }
237
238 pub fn save_to_disk(&self) -> anyhow::Result<()> {
239 let data_dir = crate::core::paths::app_data_dir()
240 .ok_or_else(|| anyhow::anyhow!("Could not find app data dir"))?;
241 let store_path = data_dir.join("settings.json");
242
243 let mut json = if store_path.exists() {
244 let content = std::fs::read_to_string(&store_path)?;
245 serde_json::from_str::<serde_json::Value>(&content).unwrap_or(serde_json::json!({}))
246 } else {
247 serde_json::json!({})
248 };
249
250 if let Some(obj) = json.as_object_mut() {
251 obj.insert("app_settings".to_string(), serde_json::to_value(self)?);
252 }
253
254 let serialized = serde_json::to_string_pretty(&json)?;
255 std::fs::write(store_path, serialized)?;
256 Ok(())
257 }
258}