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`]. `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`]).
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`].
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`].
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#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::config::Config;
82    use crate::config::fixtures::{no_env, no_flags};
83
84    #[test]
85    fn audio_format_display_roundtrip() {
86        for fmt in [
87            AudioFormat::Mp3,
88            AudioFormat::Flac,
89            AudioFormat::Wav,
90            AudioFormat::Alac,
91        ] {
92            let s = fmt.to_string();
93            assert_eq!(s.parse::<AudioFormat>().unwrap(), fmt);
94        }
95    }
96
97    #[test]
98    fn audio_format_ext() {
99        assert_eq!(AudioFormat::Mp3.ext(), "mp3");
100        assert_eq!(AudioFormat::Flac.ext(), "flac");
101        assert_eq!(AudioFormat::Wav.ext(), "wav");
102        assert_eq!(AudioFormat::Alac.ext(), "m4a");
103    }
104
105    fn base_settings(format: AudioFormat) -> EffectiveSettings {
106        let toml = "[accounts.a]\n";
107        let cfg = Config::from_toml(toml).unwrap();
108        let mut eff = cfg.resolve("a", None, &no_env(), &no_flags()).unwrap();
109        eff.format = format;
110        eff
111    }
112
113    #[test]
114    fn requires_ffmpeg_flac_always_needs_it() {
115        let mut eff = base_settings(AudioFormat::Flac);
116        eff.animated_covers = false;
117        assert!(eff.requires_ffmpeg());
118        eff.animated_covers = true;
119        assert!(eff.requires_ffmpeg());
120    }
121
122    #[test]
123    fn requires_ffmpeg_alac_always_needs_it() {
124        let mut eff = base_settings(AudioFormat::Alac);
125        eff.animated_covers = false;
126        assert!(eff.requires_ffmpeg(), "alac transcodes, so needs ffmpeg");
127    }
128
129    #[test]
130    fn requires_ffmpeg_mp3_needs_it_only_for_animated_webp() {
131        let mut eff = base_settings(AudioFormat::Mp3);
132        assert!(!eff.requires_ffmpeg(), "mp3 + no covers = no ffmpeg");
133        eff.animated_covers = true;
134        assert!(eff.requires_ffmpeg(), "mp3 + animated webp = needs ffmpeg");
135        eff.raw_animated_cover = true;
136        assert!(
137            eff.requires_ffmpeg(),
138            "mp3 + both (webp + raw mp4) = needs ffmpeg"
139        );
140        eff.animated_covers = false;
141        assert!(!eff.requires_ffmpeg(), "mp3 + raw mp4 only = no ffmpeg");
142    }
143}