1use std::collections::{BTreeSet, HashMap};
6use std::str::FromStr;
7
8use crate::error::{Error, Result};
9use crate::naming::CharacterSet;
10use crate::vocab::{AudioFormat, VideoCoverRetention, WebpEncodeSettings};
11
12use super::effective::{EffectiveSettings, FlagOverrides};
13use super::label_to_env;
14use super::shape::Config;
15
16impl Config {
17 pub fn resolve(
23 &self,
24 account: &str,
25 source: Option<&str>,
26 env: &HashMap<String, String>,
27 flags: &FlagOverrides,
28 ) -> Result<EffectiveSettings> {
29 let acc = self
30 .accounts
31 .get(account)
32 .ok_or_else(|| Error::Config(format!("account '{account}' not found")))?;
33
34 let src = source.and_then(|s| acc.sources.get(s));
35 let label_env = label_to_env(account);
36
37 let env_val = |suffix: &str| -> Option<&str> {
39 env.get(&format!("SUNO_{label_env}_{suffix}"))
40 .or_else(|| env.get(&format!("SUNO_{suffix}")))
41 .map(String::as_str)
42 };
43
44 let format = resolve_enum(
45 Layers {
46 flag: flags.settings.format,
47 env: env_val("FORMAT"),
48 src: src.and_then(|s| s.settings.format),
49 acc: acc.settings.format,
50 defaults: self.defaults.settings.format,
51 name: "FORMAT",
52 },
53 None,
54 )?
55 .unwrap_or(AudioFormat::Flac);
56
57 let concurrency = resolve_parsed(
58 Layers {
59 flag: flags.settings.concurrency,
60 env: env_val("CONCURRENCY"),
61 src: src.and_then(|s| s.settings.concurrency),
62 acc: acc.settings.concurrency,
63 defaults: self.defaults.settings.concurrency,
64 name: "CONCURRENCY",
65 },
66 4,
67 )?;
68
69 let retries = resolve_parsed(
70 Layers {
71 flag: flags.settings.retries,
72 env: env_val("RETRIES"),
73 src: src.and_then(|s| s.settings.retries),
74 acc: acc.settings.retries,
75 defaults: self.defaults.settings.retries,
76 name: "RETRIES",
77 },
78 3,
79 )?;
80
81 let min_newest = resolve_parsed(
82 Layers {
83 flag: flags.settings.min_newest,
84 env: env_val("MIN_NEWEST"),
85 src: src.and_then(|s| s.settings.min_newest),
86 acc: acc.settings.min_newest,
87 defaults: self.defaults.settings.min_newest,
88 name: "MIN_NEWEST",
89 },
90 1,
91 )?;
92
93 let animated_covers = resolve_parsed(
94 Layers {
95 flag: flags.settings.animated_covers,
96 env: env_val("ANIMATED_COVERS"),
97 src: src.and_then(|s| s.settings.animated_covers),
98 acc: acc.settings.animated_covers,
99 defaults: self.defaults.settings.animated_covers,
100 name: "ANIMATED_COVERS",
101 },
102 false,
103 )?;
104
105 let details_sidecar = resolve_parsed(
106 Layers {
107 flag: flags.settings.details_sidecar,
108 env: env_val("DETAILS_SIDECAR"),
109 src: src.and_then(|s| s.settings.details_sidecar),
110 acc: acc.settings.details_sidecar,
111 defaults: self.defaults.settings.details_sidecar,
112 name: "DETAILS_SIDECAR",
113 },
114 false,
115 )?;
116
117 let lyrics_sidecar = resolve_parsed(
118 Layers {
119 flag: flags.settings.lyrics_sidecar,
120 env: env_val("LYRICS_SIDECAR"),
121 src: src.and_then(|s| s.settings.lyrics_sidecar),
122 acc: acc.settings.lyrics_sidecar,
123 defaults: self.defaults.settings.lyrics_sidecar,
124 name: "LYRICS_SIDECAR",
125 },
126 false,
127 )?;
128
129 let lrc_sidecar = resolve_parsed(
130 Layers {
131 flag: flags.settings.lrc_sidecar,
132 env: env_val("LRC_SIDECAR"),
133 src: src.and_then(|s| s.settings.lrc_sidecar),
134 acc: acc.settings.lrc_sidecar,
135 defaults: self.defaults.settings.lrc_sidecar,
136 name: "LRC_SIDECAR",
137 },
138 false,
139 )?;
140
141 let video_mp4 = resolve_parsed(
142 Layers {
143 flag: flags.settings.video_mp4,
144 env: env_val("VIDEO_MP4"),
145 src: src.and_then(|s| s.settings.video_mp4),
146 acc: acc.settings.video_mp4,
147 defaults: self.defaults.settings.video_mp4,
148 name: "VIDEO_MP4",
149 },
150 false,
151 )?;
152
153 let download_stems = resolve_parsed(
154 Layers {
155 flag: flags.settings.download_stems,
156 env: env_val("DOWNLOAD_STEMS"),
157 src: src.and_then(|s| s.settings.download_stems),
158 acc: acc.settings.download_stems,
159 defaults: self.defaults.settings.download_stems,
160 name: "DOWNLOAD_STEMS",
161 },
162 false,
163 )?;
164
165 let stem_format = resolve_enum(
166 Layers {
167 flag: flags.settings.stem_format,
168 env: env_val("STEM_FORMAT"),
169 src: src.and_then(|s| s.settings.stem_format),
170 acc: acc.settings.stem_format,
171 defaults: self.defaults.settings.stem_format,
172 name: "STEM_FORMAT",
173 },
174 None,
175 )?
176 .unwrap_or_default();
177
178 let video_cover_retention = resolve_enum(
179 Layers {
180 flag: flags.settings.video_cover_retention,
181 env: env_val("VIDEO_COVER_RETENTION"),
182 src: src.and_then(|s| s.settings.video_cover_retention),
183 acc: acc.settings.video_cover_retention,
184 defaults: self.defaults.settings.video_cover_retention,
185 name: "VIDEO_COVER_RETENTION",
186 },
187 None,
188 )?;
189 let (animated_covers, raw_animated_cover) = match video_cover_retention {
194 Some(retention) => (retention.keeps_webp(), retention.keeps_mp4()),
195 None => (animated_covers, false),
196 };
197
198 let defaults_webp = WebpEncodeSettings::default();
199 let animated_cover_quality = resolve_u8_ranged(
200 Layers {
201 flag: flags.settings.animated_cover_quality,
202 env: env_val("ANIMATED_COVER_QUALITY"),
203 src: src.and_then(|s| s.settings.animated_cover_quality),
204 acc: acc.settings.animated_cover_quality,
205 defaults: self.defaults.settings.animated_cover_quality,
206 name: "ANIMATED_COVER_QUALITY",
207 },
208 defaults_webp.quality,
209 0..=100,
210 )?;
211 let animated_cover_max_fps = resolve_parsed(
212 Layers {
213 flag: flags.settings.animated_cover_max_fps,
214 env: env_val("ANIMATED_COVER_MAX_FPS"),
215 src: src.and_then(|s| s.settings.animated_cover_max_fps),
216 acc: acc.settings.animated_cover_max_fps,
217 defaults: self.defaults.settings.animated_cover_max_fps,
218 name: "ANIMATED_COVER_MAX_FPS",
219 },
220 defaults_webp.max_fps,
221 )?;
222 let animated_cover_max_width = resolve_parsed_opt(
223 Layers {
224 flag: flags.settings.animated_cover_max_width,
225 env: env_val("ANIMATED_COVER_MAX_WIDTH"),
226 src: src.and_then(|s| s.settings.animated_cover_max_width),
227 acc: acc.settings.animated_cover_max_width,
228 defaults: self.defaults.settings.animated_cover_max_width,
229 name: "ANIMATED_COVER_MAX_WIDTH",
230 },
231 defaults_webp.max_width,
232 )?;
233 let animated_cover_compression_level = resolve_u8_ranged(
234 Layers {
235 flag: flags.settings.animated_cover_compression_level,
236 env: env_val("ANIMATED_COVER_COMPRESSION_LEVEL"),
237 src: src.and_then(|s| s.settings.animated_cover_compression_level),
238 acc: acc.settings.animated_cover_compression_level,
239 defaults: self.defaults.settings.animated_cover_compression_level,
240 name: "ANIMATED_COVER_COMPRESSION_LEVEL",
241 },
242 defaults_webp.compression_level,
243 0..=4,
244 )?;
245 let animated_cover_lossless = resolve_parsed(
246 Layers {
247 flag: flags.settings.animated_cover_lossless,
248 env: env_val("ANIMATED_COVER_LOSSLESS"),
249 src: src.and_then(|s| s.settings.animated_cover_lossless),
250 acc: acc.settings.animated_cover_lossless,
251 defaults: self.defaults.settings.animated_cover_lossless,
252 name: "ANIMATED_COVER_LOSSLESS",
253 },
254 defaults_webp.lossless,
255 )?;
256
257 let naming_template = resolve_owned(
258 flags.settings.naming_template.clone(),
259 env_val("NAMING_TEMPLATE"),
260 src.and_then(|s| s.settings.naming_template.clone()),
261 acc.settings.naming_template.clone(),
262 self.defaults.settings.naming_template.clone(),
263 )
264 .unwrap_or_else(|| crate::naming::DEFAULT_TEMPLATE.to_owned());
265
266 let character_set = resolve_enum(
267 Layers {
268 flag: flags.settings.character_set,
269 env: env_val("CHARACTER_SET"),
270 src: src.and_then(|s| s.settings.character_set),
271 acc: acc.settings.character_set,
272 defaults: self.defaults.settings.character_set,
273 name: "CHARACTER_SET",
274 },
275 None,
276 )?
277 .unwrap_or(CharacterSet::Unicode);
278
279 let number_singletons = resolve_parsed(
280 Layers {
281 flag: flags.settings.number_singletons,
282 env: env_val("NUMBER_SINGLETONS"),
283 src: src.and_then(|s| s.settings.number_singletons),
284 acc: acc.settings.number_singletons,
285 defaults: self.defaults.settings.number_singletons,
286 name: "NUMBER_SINGLETONS",
287 },
288 true,
289 )?;
290
291 let token = flags
292 .token
293 .clone()
294 .or_else(|| env.get(&format!("SUNO_{label_env}_TOKEN")).cloned())
295 .or_else(|| env.get("SUNO_TOKEN").cloned());
296
297 let token_command = resolve_owned(
298 None,
299 env_val("TOKEN_COMMAND"),
300 src.and_then(|s| s.settings.token_command.clone()),
301 acc.settings.token_command.clone(),
302 self.defaults.settings.token_command.clone(),
303 );
304
305 Ok(EffectiveSettings {
306 token,
307 stored_token: acc.token.clone(),
308 token_command,
309 account_id: acc.account_id.clone(),
310 format,
311 concurrency,
312 retries,
313 min_newest,
314 animated_covers,
315 raw_animated_cover,
316 video_cover_retention: match (animated_covers, raw_animated_cover) {
317 (false, false) => VideoCoverRetention::Neither,
318 (true, false) => VideoCoverRetention::Webp,
319 (false, true) => VideoCoverRetention::Mp4,
320 (true, true) => VideoCoverRetention::Both,
321 },
322 animated_cover_webp: WebpEncodeSettings {
323 quality: animated_cover_quality,
324 max_fps: animated_cover_max_fps,
325 max_width: animated_cover_max_width,
326 lossless: animated_cover_lossless,
327 compression_level: animated_cover_compression_level,
328 },
329 details_sidecar,
330 lyrics_sidecar,
331 lrc_sidecar,
332 video_mp4,
333 download_stems,
334 stem_format,
335 naming_template,
336 character_set,
337 areas: acc.areas.clone(),
338 album_overrides: acc
339 .albums
340 .iter()
341 .filter(|(_, name)| !name.trim().is_empty())
342 .map(|(root_id, name)| (root_id.clone(), name.trim().to_owned()))
343 .collect(),
344 lead_tracks: acc
345 .lead_tracks
346 .iter()
347 .map(|entry| entry.trim())
348 .filter(|entry| !entry.is_empty())
349 .map(str::to_owned)
350 .collect::<BTreeSet<String>>()
351 .into_iter()
352 .collect(),
353 number_singletons,
354 })
355 }
356}
357
358struct Layers<'a, T> {
369 flag: Option<T>,
370 env: Option<&'a str>,
371 src: Option<T>,
372 acc: Option<T>,
373 defaults: Option<T>,
374 name: &'a str,
375}
376
377fn resolve_parsed<T>(layers: Layers<'_, T>, compiled: T) -> Result<T>
378where
379 T: FromStr + Copy,
380{
381 Ok(resolve_parsed_opt(layers, Some(compiled))?.unwrap_or(compiled))
382}
383
384fn resolve_parsed_opt<T>(layers: Layers<'_, T>, compiled: Option<T>) -> Result<Option<T>>
389where
390 T: FromStr + Copy,
391{
392 let Layers {
393 flag,
394 env,
395 src,
396 acc,
397 defaults,
398 name,
399 } = layers;
400 if let Some(v) = flag {
401 return Ok(Some(v));
402 }
403 if let Some(s) = env {
404 return s
405 .parse()
406 .map(Some)
407 .map_err(|_| Error::Config(format!("invalid {name}: '{s}'")));
408 }
409 Ok(src.or(acc).or(defaults).or(compiled))
410}
411
412fn resolve_u8_ranged(
413 layers: Layers<'_, u8>,
414 compiled: u8,
415 range: std::ops::RangeInclusive<u8>,
416) -> Result<u8> {
417 let Layers {
418 flag,
419 env,
420 src,
421 acc,
422 defaults,
423 name,
424 } = layers;
425 let value = if let Some(v) = flag {
426 v
427 } else if let Some(s) = env {
428 s.parse()
429 .map_err(|_| Error::Config(format!("invalid {name}: '{s}' (expected integer)")))?
430 } else {
431 src.or(acc).or(defaults).unwrap_or(compiled)
432 };
433 if range.contains(&value) {
434 Ok(value)
435 } else {
436 Err(Error::Config(format!(
437 "invalid {name}: '{value}' (expected {}..={})",
438 range.start(),
439 range.end()
440 )))
441 }
442}
443
444fn resolve_enum<T>(layers: Layers<'_, T>, compiled: Option<T>) -> Result<Option<T>>
445where
446 T: FromStr<Err = Error> + Copy,
447{
448 let Layers {
449 flag,
450 env,
451 src,
452 acc,
453 defaults,
454 name,
455 } = layers;
456 if let Some(v) = flag {
457 return Ok(Some(v));
458 }
459 if let Some(s) = env {
460 return s
461 .parse()
462 .map(Some)
463 .map_err(|err| Error::Config(format!("invalid {name}: '{s}' ({err})")));
464 }
465 Ok(src.or(acc).or(defaults).or(compiled))
466}
467
468fn resolve_owned(
474 flag: Option<String>,
475 env_str: Option<&str>,
476 src: Option<String>,
477 acc: Option<String>,
478 defaults: Option<String>,
479) -> Option<String> {
480 flag.or_else(|| env_str.map(str::to_owned))
481 .or(src)
482 .or(acc)
483 .or(defaults)
484}
485
486#[cfg(test)]
487mod tests;