Skip to main content

suno_core/config/
effective.rs

1//! The resolved output types produced by [`Config::resolve`].
2
3use std::collections::BTreeMap;
4
5use crate::naming::CharacterSet;
6use crate::vocab::{AudioFormat, StemFormat, VideoCoverRetention, WebpEncodeSettings};
7
8use super::shape::{AreasConfig, Settings};
9
10/// CLI flag overrides passed to [`Config::resolve`](crate::config::Config::resolve). `None` means the flag
11/// was not provided.
12///
13/// Only `token` is carried top-level (the one field with a global `--token`
14/// flag); the rest nest in [`Settings`]. There is no `--token-command` flag, so
15/// `settings.token_command` is never populated from the CLI.
16#[derive(Debug, Default)]
17pub struct FlagOverrides {
18    pub token: Option<String>,
19    pub settings: Settings,
20}
21
22/// Resolved effective settings for one account/source combination.
23#[derive(Debug, Clone, PartialEq)]
24pub struct EffectiveSettings {
25    /// A direct token from `--token` or `SUNO_*_TOKEN`.
26    pub token: Option<String>,
27    /// A stored token from `[accounts.<label>].token`.
28    pub stored_token: Option<String>,
29    /// A command to run for the token when no direct token was supplied.
30    pub token_command: Option<String>,
31    /// The optional configured account id assertion (see [`AccountConfig`](crate::config::AccountConfig)).
32    pub account_id: Option<String>,
33    pub format: AudioFormat,
34    pub concurrency: u32,
35    pub retries: u32,
36    pub min_newest: u32,
37    pub animated_covers: bool,
38    /// Keep the raw album `cover.mp4` (`video_cover_url` verbatim, no transcode).
39    /// Driven by [`VideoCoverRetention::keeps_mp4`]; independent of `video_mp4`.
40    pub raw_animated_cover: bool,
41    pub video_cover_retention: VideoCoverRetention,
42    pub animated_cover_webp: WebpEncodeSettings,
43    pub details_sidecar: bool,
44    pub lyrics_sidecar: bool,
45    pub lrc_sidecar: bool,
46    pub video_mp4: bool,
47    pub download_stems: bool,
48    pub stem_format: StemFormat,
49    pub naming_template: String,
50    pub character_set: CharacterSet,
51    /// The per-account `[areas]` selection table, if configured.
52    pub areas: Option<AreasConfig>,
53    /// Manual album-name overrides, keyed by lineage root id, resolved from the
54    /// account's `[accounts.<label>.albums]` table. Deterministically ordered
55    /// (a [`BTreeMap`]) and pre-trimmed of empty values by [`Config::resolve`](crate::config::Config::resolve).
56    pub album_overrides: BTreeMap<String, String>,
57    /// Lead-track flags from `[accounts.<label>].lead_tracks`: clip ids (or
58    /// unique prefixes) each promoted to track 1 of their lineage album.
59    /// Trimmed, de-duplicated, and deterministically ordered by
60    /// [`Config::resolve`](crate::config::Config::resolve).
61    pub lead_tracks: Vec<String>,
62    /// Whether a lone-track album is numbered (defaults to `true`). `false`
63    /// leaves singletons unnumbered.
64    pub number_singletons: bool,
65}
66
67impl EffectiveSettings {
68    /// Returns `true` when these settings require ffmpeg to be on `PATH`.
69    ///
70    /// Lossless output (FLAC or ALAC) transcodes from the WAV render and an
71    /// animated WebP cover transcodes MP4→WebP, so either needs ffmpeg. A raw
72    /// MP4 alone, or MP3/WAV with no animated covers, does not.
73    pub fn requires_ffmpeg(&self) -> bool {
74        matches!(self.format, AudioFormat::Flac | AudioFormat::Alac) || self.animated_covers
75    }
76}
77
78/// Whether an explicit `--animated-covers` request was silently overridden by a
79/// `video_cover_retention` that keeps no animated WebP cover.
80///
81/// `--animated-covers` maps to `flag == Some(true)` (it is never `Some(false)`);
82/// when `video_cover_retention` is unset the flag value survives resolution, so a
83/// resolved `effective_animated == false` alongside `Some(true)` can only mean a
84/// `neither`/`mp4` retention dropped it. Pure, so the rule is unit-tested beside
85/// the resolver rather than in the CLI; the CLI only decides whether to print the
86/// note. The documented precedence is unchanged: this reports the override, it
87/// does not reverse it.
88pub fn animated_covers_flag_overridden(flag: Option<bool>, effective_animated: bool) -> bool {
89    flag == Some(true) && !effective_animated
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::config::Config;
96    use crate::config::fixtures::{no_env, no_flags};
97
98    #[test]
99    fn audio_format_display_roundtrip() {
100        for fmt in [
101            AudioFormat::Mp3,
102            AudioFormat::Flac,
103            AudioFormat::Wav,
104            AudioFormat::Alac,
105        ] {
106            let s = fmt.to_string();
107            assert_eq!(s.parse::<AudioFormat>().unwrap(), fmt);
108        }
109    }
110
111    #[test]
112    fn audio_format_ext() {
113        assert_eq!(AudioFormat::Mp3.ext(), "mp3");
114        assert_eq!(AudioFormat::Flac.ext(), "flac");
115        assert_eq!(AudioFormat::Wav.ext(), "wav");
116        assert_eq!(AudioFormat::Alac.ext(), "m4a");
117    }
118
119    fn base_settings(format: AudioFormat) -> EffectiveSettings {
120        let toml = "[accounts.a]\n";
121        let cfg = Config::from_toml(toml).unwrap();
122        let mut eff = cfg.resolve("a", None, &no_env(), &no_flags()).unwrap();
123        eff.format = format;
124        eff
125    }
126
127    #[test]
128    fn requires_ffmpeg_truth_table() {
129        // ffmpeg is needed for lossless output (FLAC/ALAC transcode from WAV) or
130        // an animated WebP cover (MP4->WebP). A raw MP4 cover alone does not need
131        // it, so raw_animated_cover never changes the answer.
132        for (label, format, animated, raw, want) in [
133            ("flac, no covers", AudioFormat::Flac, false, false, true),
134            ("flac, animated", AudioFormat::Flac, true, false, true),
135            ("alac, no covers", AudioFormat::Alac, false, false, true),
136            ("mp3, no covers", AudioFormat::Mp3, false, false, false),
137            ("mp3, animated webp", AudioFormat::Mp3, true, false, true),
138            (
139                "mp3, both webp and raw mp4",
140                AudioFormat::Mp3,
141                true,
142                true,
143                true,
144            ),
145            ("mp3, raw mp4 only", AudioFormat::Mp3, false, true, false),
146        ] {
147            let mut eff = base_settings(format);
148            eff.animated_covers = animated;
149            eff.raw_animated_cover = raw;
150            assert_eq!(eff.requires_ffmpeg(), want, "{label}");
151        }
152    }
153
154    #[test]
155    fn animated_covers_flag_override_truth_table() {
156        // `--animated-covers` maps to Some(true) and is reported as overridden
157        // only when the resolved effective value is false (a neither/mp4
158        // retention dropped it). An absent flag is never an override.
159        for (label, flag, effective, want) in [
160            ("neither/mp4 drops the flag", Some(true), false, true),
161            ("webp/both honours the flag", Some(true), true, false),
162            ("absent flag, effective false", None, false, false),
163            ("absent flag, effective true", None, true, false),
164        ] {
165            assert_eq!(
166                animated_covers_flag_overridden(flag, effective),
167                want,
168                "{label}"
169            );
170        }
171    }
172}