use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::time::Duration;
use futures_util::lock::Mutex as AsyncMutex;
use futures_util::stream::{self, StreamExt};
use crate::backoff::{backoff_delay, retry_after};
use crate::client::SunoClient;
use crate::clock::Clock;
use crate::config::AudioFormat;
use crate::error::Error;
use crate::ffmpeg::{Ffmpeg, WebpEncodeSettings};
use crate::fs::Filesystem;
use crate::graph::{AlbumArt, PlaylistState};
use crate::http::{Http, HttpRequest};
use crate::lineage::LineageContext;
use crate::manifest::{ArtifactState, Manifest, ManifestEntry};
use crate::model::Clip;
use crate::reconcile::{Action, ArtifactKind, Desired, Plan, SourceMode, set_manifest_artifact};
use crate::tag::{TrackMetadata, tag_flac, tag_mp3};
type ClientLock<'a, C> = AsyncMutex<&'a mut SunoClient<C>>;
#[derive(Debug, Clone)]
pub struct ExecOptions {
pub max_retries: u32,
pub wav_poll_attempts: u32,
pub wav_poll_interval: Duration,
pub concurrency: u32,
}
impl Default for ExecOptions {
fn default() -> Self {
Self {
max_retries: 3,
wav_poll_attempts: 24,
wav_poll_interval: Duration::from_secs(5),
concurrency: 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RunStatus {
#[default]
Completed,
AuthAborted,
DiskFull,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Failure {
pub clip_id: String,
pub reason: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ExecOutcome {
pub downloaded: usize,
pub reformatted: usize,
pub retagged: usize,
pub renamed: usize,
pub deleted: usize,
pub skipped: usize,
pub artifacts_written: usize,
pub artifacts_deleted: usize,
pub failures: Vec<Failure>,
pub status: RunStatus,
}
impl ExecOutcome {
pub fn failed(&self) -> usize {
self.failures.len()
}
fn record(&mut self, effect: Effect) {
match effect {
Effect::Downloaded => self.downloaded += 1,
Effect::Reformatted => self.reformatted += 1,
Effect::Retagged => self.retagged += 1,
Effect::Renamed => self.renamed += 1,
Effect::Deleted => self.deleted += 1,
Effect::Skipped => self.skipped += 1,
Effect::ArtifactWritten => self.artifacts_written += 1,
Effect::ArtifactDeleted => self.artifacts_deleted += 1,
}
}
}
pub struct Ports<'a, H, F, G, C> {
pub client: &'a mut SunoClient<C>,
pub http: &'a H,
pub fs: &'a F,
pub ffmpeg: &'a G,
pub clock: &'a C,
}
pub async fn execute<H, F, G, C>(
plan: &Plan,
manifest: &mut Manifest,
albums: &mut BTreeMap<String, AlbumArt>,
playlists: &mut BTreeMap<String, PlaylistState>,
desired: &[Desired],
ports: Ports<'_, H, F, G, C>,
opts: &ExecOptions,
) -> ExecOutcome
where
H: Http,
F: Filesystem,
G: Ffmpeg,
C: Clock,
{
let Ports {
client,
http,
fs,
ffmpeg,
clock,
} = ports;
let by_id: HashMap<&str, &Desired> = desired.iter().map(|d| (d.clip.id.as_str(), d)).collect();
let by_path: HashMap<&str, &Desired> = desired.iter().map(|d| (d.path.as_str(), d)).collect();
let write_targets: BTreeSet<String> = plan
.actions
.iter()
.filter_map(|a| match a {
Action::Download { path, .. }
| Action::Reformat { path, .. }
| Action::WriteArtifact { path, .. } => Some(path.clone()),
Action::Rename { to, .. } => Some(to.clone()),
_ => None,
})
.collect();
let mut tracked_paths: HashMap<String, u32> = HashMap::new();
for (_, entry) in manifest.iter() {
for path in entry.artifact_paths() {
*tracked_paths.entry(path.to_owned()).or_default() += 1;
}
}
for art in albums.values() {
for state in [art.folder_jpg.as_ref(), art.folder_webp.as_ref()]
.into_iter()
.flatten()
{
*tracked_paths.entry(state.path.clone()).or_default() += 1;
}
}
for playlist in playlists.values() {
*tracked_paths.entry(playlist.path.clone()).or_default() += 1;
}
let ctx = Ctx {
http,
fs,
ffmpeg,
clock,
opts,
by_id: &by_id,
by_path: &by_path,
write_targets: &write_targets,
};
let mut outcome = ExecOutcome::default();
let client_lock = AsyncMutex::new(client);
let concurrency = opts.concurrency.max(1) as usize;
let ctx_ref = &ctx;
let client_lock_ref = &client_lock;
let mut renders = stream::iter(
plan.actions
.iter()
.filter(|action| is_audio_action(action))
.map(|action| async move { ctx_ref.prepare_audio(client_lock_ref, action).await }),
)
.buffered(concurrency);
for action in &plan.actions {
let result = if is_audio_action(action) {
match renders.next().await {
Some(Ok(rendered)) => ctx.commit_audio(manifest, rendered),
Some(Err(fail)) => Err(fail),
None => unreachable!("buffered yields one result per audio action"),
}
} else {
ctx.apply(action, manifest, albums, playlists, &mut tracked_paths)
.await
};
match result {
Ok(effect) => outcome.record(effect),
Err(fail) => {
let abort = abort_status(fail.class);
outcome.failures.push(Failure {
clip_id: fail.clip_id,
reason: fail.reason,
});
if let Some(status) = abort {
outcome.status = status;
break;
}
}
}
}
drop(renders);
let _ = fs.prune_empty_dirs("");
outcome
}
fn is_audio_action(action: &Action) -> bool {
matches!(action, Action::Download { .. } | Action::Reformat { .. })
}
struct RenderedAudio {
clip_id: String,
path: String,
format: AudioFormat,
from_path: Option<String>,
effect: Effect,
bytes: Vec<u8>,
}
enum Effect {
Downloaded,
Reformatted,
Retagged,
Renamed,
Deleted,
Skipped,
ArtifactWritten,
ArtifactDeleted,
}
#[derive(Debug, Clone, Copy)]
enum Class {
Auth,
Disk,
Transient,
Permanent,
}
struct Fail {
class: Class,
clip_id: String,
reason: String,
}
fn abort_status(class: Class) -> Option<RunStatus> {
match class {
Class::Auth => Some(RunStatus::AuthAborted),
Class::Disk => Some(RunStatus::DiskFull),
Class::Transient | Class::Permanent => None,
}
}
fn auth_fail(clip_id: impl Into<String>, reason: impl Into<String>) -> Fail {
Fail {
class: Class::Auth,
clip_id: clip_id.into(),
reason: reason.into(),
}
}
fn transient_fail(clip_id: impl Into<String>, reason: impl Into<String>) -> Fail {
Fail {
class: Class::Transient,
clip_id: clip_id.into(),
reason: reason.into(),
}
}
fn permanent_fail(clip_id: impl Into<String>, reason: impl Into<String>) -> Fail {
Fail {
class: Class::Permanent,
clip_id: clip_id.into(),
reason: reason.into(),
}
}
fn disk_fail(clip_id: impl Into<String>, reason: impl Into<String>) -> Fail {
Fail {
class: Class::Disk,
clip_id: clip_id.into(),
reason: reason.into(),
}
}
fn is_album_kind(kind: ArtifactKind) -> bool {
matches!(kind, ArtifactKind::FolderJpg | ArtifactKind::FolderWebp)
}
fn is_playlist_kind(kind: ArtifactKind) -> bool {
matches!(kind, ArtifactKind::Playlist)
}
fn is_per_clip_kind(kind: ArtifactKind) -> bool {
matches!(
kind,
ArtifactKind::CoverJpg
| ArtifactKind::CoverWebp
| ArtifactKind::DetailsTxt
| ArtifactKind::LyricsTxt
| ArtifactKind::Lrc
| ArtifactKind::VideoMp4
)
}
fn playlist_name_from_path(path: &str) -> String {
std::path::Path::new(path)
.file_stem()
.map(|stem| stem.to_string_lossy().into_owned())
.unwrap_or_default()
}
struct FetchError {
class: Class,
reason: String,
retry_after: Option<Duration>,
}
impl FetchError {
fn transient(reason: impl Into<String>, retry_after: Option<Duration>) -> Self {
Self {
class: Class::Transient,
reason: reason.into(),
retry_after,
}
}
fn permanent(reason: impl Into<String>) -> Self {
Self {
class: Class::Permanent,
reason: reason.into(),
retry_after: None,
}
}
fn attribute(self, clip_id: &str) -> Fail {
Fail {
class: self.class,
clip_id: clip_id.to_owned(),
reason: self.reason,
}
}
}
struct Ctx<'a, H, F, G, C> {
http: &'a H,
fs: &'a F,
ffmpeg: &'a G,
clock: &'a C,
opts: &'a ExecOptions,
by_id: &'a HashMap<&'a str, &'a Desired>,
by_path: &'a HashMap<&'a str, &'a Desired>,
write_targets: &'a BTreeSet<String>,
}
impl<H, F, G, C> Ctx<'_, H, F, G, C>
where
H: Http,
F: Filesystem,
G: Ffmpeg,
C: Clock,
{
async fn apply(
&self,
action: &Action,
manifest: &mut Manifest,
albums: &mut BTreeMap<String, AlbumArt>,
playlists: &mut BTreeMap<String, PlaylistState>,
tracked_paths: &mut HashMap<String, u32>,
) -> Result<Effect, Fail> {
match action {
Action::Download { .. } | Action::Reformat { .. } => {
unreachable!("audio actions are applied in the concurrent phase")
}
Action::Retag {
clip,
lineage,
path,
} => self.retag(manifest, clip, lineage, path).await,
Action::Rename { from, to } => self.rename(manifest, from, to),
Action::Delete { path, clip_id } => self.delete(manifest, path, clip_id),
Action::Skip { clip_id } => {
self.refresh_preserve(manifest, clip_id);
Ok(Effect::Skipped)
}
Action::WriteArtifact {
kind,
path,
source_url,
hash,
owner_id,
content,
} => {
self.write_artifact(
manifest,
albums,
playlists,
*kind,
path,
source_url,
hash,
owner_id,
content.as_deref(),
tracked_paths,
)
.await
}
Action::DeleteArtifact {
kind,
path,
owner_id,
} => self.delete_artifact(manifest, albums, playlists, *kind, path, owner_id),
}
}
async fn prepare_audio(
&self,
client_lock: &ClientLock<'_, C>,
action: &Action,
) -> Result<RenderedAudio, Fail> {
match action {
Action::Download {
clip,
lineage,
path,
format,
} => {
let bytes = self
.produce_audio(client_lock, clip, lineage, *format)
.await?;
Ok(RenderedAudio {
clip_id: clip.id.clone(),
path: path.clone(),
format: *format,
from_path: None,
effect: Effect::Downloaded,
bytes,
})
}
Action::Reformat {
clip,
path,
from_path,
from: _,
to,
} => {
let lineage = self
.by_id
.get(clip.id.as_str())
.map(|d| d.lineage.clone())
.unwrap_or_else(|| LineageContext::own_root(clip));
let bytes = self.produce_audio(client_lock, clip, &lineage, *to).await?;
Ok(RenderedAudio {
clip_id: clip.id.clone(),
path: path.clone(),
format: *to,
from_path: Some(from_path.clone()),
effect: Effect::Reformatted,
bytes,
})
}
_ => unreachable!("prepare_audio only handles audio actions"),
}
}
fn commit_audio(
&self,
manifest: &mut Manifest,
rendered: RenderedAudio,
) -> Result<Effect, Fail> {
let RenderedAudio {
clip_id,
path,
format,
from_path,
effect,
bytes,
} = rendered;
let size = self.write_verify(&clip_id, &path, &bytes)?;
if let Some(from) = from_path {
self.fs.remove(&from).map_err(|err| {
permanent_fail(&clip_id, format!("could not remove old file: {err}"))
})?;
}
manifest.insert(clip_id.clone(), self.entry(&clip_id, &path, format, size));
Ok(effect)
}
async fn retag(
&self,
manifest: &mut Manifest,
clip: &Clip,
lineage: &LineageContext,
path: &str,
) -> Result<Effect, Fail> {
let Some(format) = manifest.get(&clip.id).map(|entry| entry.format) else {
return Err(permanent_fail(
&clip.id,
"retag target missing from manifest",
));
};
if format == AudioFormat::Wav {
self.refresh_hashes(manifest, &clip.id, None);
return Ok(Effect::Retagged);
}
let meta = TrackMetadata::from_clip(clip, lineage);
let cover = self.fetch_cover(clip).await;
let existing = self
.fs
.read(path)
.map_err(|err| permanent_fail(&clip.id, format!("could not read for retag: {err}")))?;
let tagged = match format {
AudioFormat::Mp3 => tag_mp3(&existing, &meta, cover.as_deref()),
AudioFormat::Flac => tag_flac(&existing, &meta, cover.as_deref()),
AudioFormat::Wav => unreachable!("WAV handled above"),
}
.map_err(|err| permanent_fail(&clip.id, err.to_string()))?;
let size = self.write_verify(&clip.id, path, &tagged)?;
self.refresh_hashes(manifest, &clip.id, Some(size));
Ok(Effect::Retagged)
}
fn rename(&self, manifest: &mut Manifest, from: &str, to: &str) -> Result<Effect, Fail> {
let label = self
.by_path
.get(to)
.map(|d| d.clip.id.clone())
.unwrap_or_else(|| to.to_owned());
self.fs.rename(from, to).map_err(|err| {
if err.is_out_of_space() {
disk_fail(label, "disk full: no space left to rename")
} else {
permanent_fail(label, format!("rename failed: {err}"))
}
})?;
let clip_id = self.by_path.get(to).map(|d| d.clip.id.clone()).or_else(|| {
manifest
.entries
.iter()
.find(|(_, entry)| entry.path == from)
.map(|(id, _)| id.clone())
});
if let Some(id) = clip_id
&& let Some(entry) = manifest.entries.get_mut(&id)
{
entry.path = to.to_owned();
if let Some(d) = self.by_path.get(to) {
entry.preserve = preserve_for(d);
}
}
Ok(Effect::Renamed)
}
fn delete(&self, manifest: &mut Manifest, path: &str, clip_id: &str) -> Result<Effect, Fail> {
self.fs
.remove(path)
.map_err(|err| permanent_fail(clip_id, format!("delete failed: {err}")))?;
manifest.remove(clip_id);
Ok(Effect::Deleted)
}
#[allow(clippy::too_many_arguments)]
async fn write_artifact(
&self,
manifest: &mut Manifest,
albums: &mut BTreeMap<String, AlbumArt>,
playlists: &mut BTreeMap<String, PlaylistState>,
kind: ArtifactKind,
path: &str,
source_url: &str,
hash: &str,
owner_id: &str,
content: Option<&str>,
tracked_paths: &mut HashMap<String, u32>,
) -> Result<Effect, Fail> {
if is_per_clip_kind(kind) && manifest.get(owner_id).is_none() {
return Ok(Effect::Skipped);
}
let old_path = match kind {
ArtifactKind::CoverJpg => manifest
.get(owner_id)
.and_then(|e| e.cover_jpg.as_ref())
.map(|s| s.path.clone()),
ArtifactKind::CoverWebp => manifest
.get(owner_id)
.and_then(|e| e.cover_webp.as_ref())
.map(|s| s.path.clone()),
ArtifactKind::DetailsTxt => manifest
.get(owner_id)
.and_then(|e| e.details_txt.as_ref())
.map(|s| s.path.clone()),
ArtifactKind::LyricsTxt => manifest
.get(owner_id)
.and_then(|e| e.lyrics_txt.as_ref())
.map(|s| s.path.clone()),
ArtifactKind::Lrc => manifest
.get(owner_id)
.and_then(|e| e.lrc.as_ref())
.map(|s| s.path.clone()),
ArtifactKind::VideoMp4 => manifest
.get(owner_id)
.and_then(|e| e.video_mp4.as_ref())
.map(|s| s.path.clone()),
ArtifactKind::FolderJpg | ArtifactKind::FolderWebp => albums
.get(owner_id)
.and_then(|a| a.artifact(kind))
.map(|s| s.path.clone()),
ArtifactKind::Playlist => None,
};
let bytes = match content {
Some(text) => text.as_bytes().to_vec(),
None => self.artifact_bytes(kind, source_url, owner_id).await?,
};
self.write_verify(owner_id, path, &bytes)?;
if let Some(old) = old_path.as_deref()
&& !old.is_empty()
&& old != path
{
let still_referenced = tracked_paths
.get_mut(old)
.map(|count| {
*count = count.saturating_sub(1);
*count > 0
})
.unwrap_or(false);
if !still_referenced && !self.write_targets.contains(old) {
self.fs.remove(old).map_err(|err| {
permanent_fail(
owner_id,
format!("could not remove old sidecar {old}: {err}"),
)
})?;
}
}
if is_album_kind(kind) {
albums.entry(owner_id.to_owned()).or_default().set(
kind,
Some(ArtifactState {
path: path.to_owned(),
hash: hash.to_owned(),
}),
);
} else if is_playlist_kind(kind) {
playlists.insert(
owner_id.to_owned(),
PlaylistState {
name: playlist_name_from_path(path),
path: path.to_owned(),
hash: hash.to_owned(),
},
);
} else if let Some(entry) = manifest.entries.get_mut(owner_id) {
set_manifest_artifact(
entry,
kind,
Some(ArtifactState {
path: path.to_owned(),
hash: hash.to_owned(),
}),
);
}
Ok(Effect::ArtifactWritten)
}
async fn artifact_bytes(
&self,
kind: ArtifactKind,
source_url: &str,
owner_id: &str,
) -> Result<Vec<u8>, Fail> {
let source = self
.fetch_bytes(source_url)
.await
.map_err(|err| err.attribute(owner_id))?;
match kind {
ArtifactKind::CoverWebp | ArtifactKind::FolderWebp => self
.ffmpeg
.mp4_to_webp(&source, WebpEncodeSettings::default())
.await
.map_err(|err| {
if err.is_out_of_space() {
disk_fail(owner_id, "disk full: no space left to transcode")
} else {
permanent_fail(owner_id, format!("cover transcode failed: {err}"))
}
}),
ArtifactKind::DetailsTxt | ArtifactKind::LyricsTxt | ArtifactKind::Lrc => Err(
permanent_fail(owner_id, "text sidecar requires inline content"),
),
ArtifactKind::CoverJpg
| ArtifactKind::FolderJpg
| ArtifactKind::Playlist
| ArtifactKind::VideoMp4 => Ok(source),
}
}
fn delete_artifact(
&self,
manifest: &mut Manifest,
albums: &mut BTreeMap<String, AlbumArt>,
playlists: &mut BTreeMap<String, PlaylistState>,
kind: ArtifactKind,
path: &str,
owner_id: &str,
) -> Result<Effect, Fail> {
self.fs
.remove(path)
.map_err(|err| permanent_fail(owner_id, format!("artifact delete failed: {err}")))?;
if is_album_kind(kind) {
if let Some(art) = albums.get_mut(owner_id) {
art.set(kind, None);
if art.is_empty() {
albums.remove(owner_id);
}
}
} else if is_playlist_kind(kind) {
playlists.remove(owner_id);
} else if let Some(entry) = manifest.entries.get_mut(owner_id) {
set_manifest_artifact(entry, kind, None);
}
Ok(Effect::ArtifactDeleted)
}
async fn produce_audio(
&self,
client_lock: &ClientLock<'_, C>,
clip: &Clip,
lineage: &LineageContext,
format: AudioFormat,
) -> Result<Vec<u8>, Fail> {
let meta = TrackMetadata::from_clip(clip, lineage);
match format {
AudioFormat::Mp3 => {
let url = clip.mp3_url();
let audio = self
.fetch_bytes(&url)
.await
.map_err(|err| err.attribute(&clip.id))?;
let cover = self.fetch_cover(clip).await;
tag_mp3(&audio, &meta, cover.as_deref())
.map_err(|err| permanent_fail(&clip.id, err.to_string()))
}
AudioFormat::Flac => {
let wav = self.fetch_wav(client_lock, clip).await?;
let flac = self.ffmpeg.wav_to_flac(&wav).await.map_err(|err| {
if err.is_out_of_space() {
disk_fail(&clip.id, "disk full: no space left to transcode")
} else {
permanent_fail(&clip.id, format!("transcode failed: {err}"))
}
})?;
let cover = self.fetch_cover(clip).await;
tag_flac(&flac, &meta, cover.as_deref())
.map_err(|err| permanent_fail(&clip.id, err.to_string()))
}
AudioFormat::Wav => self.fetch_wav(client_lock, clip).await,
}
}
async fn fetch_wav(
&self,
client_lock: &ClientLock<'_, C>,
clip: &Clip,
) -> Result<Vec<u8>, Fail> {
let url = match self.resolve_wav_url(client_lock, &clip.id).await? {
Some(url) => url,
None => return Err(transient_fail(&clip.id, "WAV render was not ready")),
};
self.fetch_bytes(&url)
.await
.map_err(|err| err.attribute(&clip.id))
}
async fn resolve_wav_url(
&self,
client_lock: &ClientLock<'_, C>,
id: &str,
) -> Result<Option<String>, Fail> {
if let Some(url) = self.wav_url_retrying(client_lock, id).await? {
return Ok(Some(url));
}
self.request_wav_retrying(client_lock, id).await?;
for _ in 0..self.opts.wav_poll_attempts {
self.clock.sleep(self.opts.wav_poll_interval).await;
if let Some(url) = self.wav_url_retrying(client_lock, id).await? {
return Ok(Some(url));
}
}
Ok(None)
}
async fn wav_url_retrying(
&self,
client_lock: &ClientLock<'_, C>,
id: &str,
) -> Result<Option<String>, Fail> {
let mut attempt: u32 = 0;
loop {
let result = {
let mut client = client_lock.lock().await;
client.wav_url(self.http, id).await
};
match result {
Ok(url) => return Ok(url),
Err(err) => match self.retry_core(id, err, &mut attempt).await {
Some(fail) => return Err(fail),
None => continue,
},
}
}
}
async fn request_wav_retrying(
&self,
client_lock: &ClientLock<'_, C>,
id: &str,
) -> Result<(), Fail> {
let mut attempt: u32 = 0;
loop {
let result = {
let mut client = client_lock.lock().await;
client.request_wav(self.http, id).await
};
match result {
Ok(()) => return Ok(()),
Err(err) => match self.retry_core(id, err, &mut attempt).await {
Some(fail) => return Err(fail),
None => continue,
},
}
}
}
async fn retry_core(&self, id: &str, err: Error, attempt: &mut u32) -> Option<Fail> {
let fail = classify_core(id, err);
if matches!(fail.class, Class::Transient) && *attempt < self.opts.max_retries {
self.clock.sleep(backoff_delay(*attempt, None)).await;
*attempt += 1;
None
} else {
Some(fail)
}
}
async fn fetch_bytes(&self, url: &str) -> Result<Vec<u8>, FetchError> {
let mut attempt: u32 = 0;
loop {
let result = self.http.send(HttpRequest::get(url)).await;
match classify_response(result) {
Ok(body) => return Ok(body),
Err(err) => {
if matches!(err.class, Class::Transient) && attempt < self.opts.max_retries {
let delay = backoff_delay(attempt, err.retry_after);
self.clock.sleep(delay).await;
attempt += 1;
continue;
}
return Err(err);
}
}
}
}
async fn fetch_cover(&self, clip: &Clip) -> Option<Vec<u8>> {
for url in clip.cover_candidates() {
if let Ok(response) = self.http.send(HttpRequest::get(url)).await
&& (200..=299).contains(&response.status)
&& !response.body.is_empty()
{
return Some(response.body);
}
}
None
}
fn write_verify(&self, clip_id: &str, path: &str, bytes: &[u8]) -> Result<u64, Fail> {
self.fs.write_atomic(path, bytes).map_err(|err| {
if err.is_out_of_space() {
disk_fail(clip_id, format!("disk full: no space left to write {path}"))
} else {
permanent_fail(clip_id, format!("write failed: {err}"))
}
})?;
match self.fs.metadata(path) {
Some(stat) if stat.size == bytes.len() as u64 => Ok(stat.size),
Some(stat) => Err(permanent_fail(
clip_id,
format!("wrote {} bytes, expected {}", stat.size, bytes.len()),
)),
None => Ok(bytes.len() as u64),
}
}
fn entry(&self, clip_id: &str, path: &str, format: AudioFormat, size: u64) -> ManifestEntry {
match self.by_id.get(clip_id) {
Some(d) => manifest_entry(d, size),
None => ManifestEntry {
path: path.to_owned(),
format,
size,
..ManifestEntry::default()
},
}
}
fn refresh_hashes(&self, manifest: &mut Manifest, clip_id: &str, size: Option<u64>) {
let desired = self.by_id.get(clip_id).copied();
if let Some(entry) = manifest.entries.get_mut(clip_id) {
if let Some(d) = desired {
entry.meta_hash = d.meta_hash.clone();
entry.art_hash = d.art_hash.clone();
entry.preserve = preserve_for(d);
}
if let Some(size) = size {
entry.size = size;
}
}
}
fn refresh_preserve(&self, manifest: &mut Manifest, clip_id: &str) {
if let Some(d) = self.by_id.get(clip_id).copied()
&& let Some(entry) = manifest.entries.get_mut(clip_id)
{
entry.preserve = preserve_for(d);
}
}
}
fn manifest_entry(d: &Desired, size: u64) -> ManifestEntry {
ManifestEntry {
path: d.path.clone(),
format: d.format,
meta_hash: d.meta_hash.clone(),
art_hash: d.art_hash.clone(),
size,
preserve: preserve_for(d),
..Default::default()
}
}
fn preserve_for(d: &Desired) -> bool {
d.private || d.modes.contains(&SourceMode::Copy)
}
fn classify_response(
result: Result<crate::http::HttpResponse, crate::http::TransportError>,
) -> Result<Vec<u8>, FetchError> {
let response = match result {
Ok(response) => response,
Err(err) => {
return Err(FetchError::transient(
format!("transport error: {err}"),
None,
));
}
};
match response.status {
200..=299 => {
if let Some(expected) = content_length(&response) {
let actual = response.body.len() as u64;
if actual != expected {
return Err(FetchError::transient(
format!("truncated download: {actual} of {expected} bytes"),
None,
));
}
}
Ok(response.body)
}
401 | 403 => Err(FetchError::transient(
format!("download rejected: status {}", response.status),
None,
)),
408 => Err(FetchError::transient("request timed out", None)),
429 => Err(FetchError::transient(
"rate limited",
retry_after(&response),
)),
500..=599 => Err(FetchError::transient(
format!("server error {}", response.status),
None,
)),
status => Err(FetchError::permanent(format!(
"download failed: status {status}"
))),
}
}
fn classify_core(id: &str, err: Error) -> Fail {
let reason = err.to_string();
match err {
Error::Auth(_) => auth_fail(id, reason),
Error::RateLimited { .. } | Error::Connection(_) => transient_fail(id, reason),
Error::Api(_) | Error::NotFound(_) | Error::Tag(_) | Error::Config(_) => {
permanent_fail(id, reason)
}
}
}
fn content_length(response: &crate::http::HttpResponse) -> Option<u64> {
response.header("content-length")?.trim().parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ClerkAuth;
use crate::http::HttpResponse;
use crate::testutil::{MemFs, RecordingClock, Reply, ScriptedHttp, StubFfmpeg};
fn clip(id: &str) -> Clip {
Clip {
id: id.to_owned(),
title: "Song".to_owned(),
audio_url: format!("https://cdn1.suno.ai/{id}.mp3"),
..Default::default()
}
}
fn art_clip(id: &str) -> Clip {
Clip {
image_large_url: format!("https://art.suno.ai/{id}/large.jpg"),
image_url: format!("https://art.suno.ai/{id}/small.jpg"),
..clip(id)
}
}
fn ext(format: AudioFormat) -> &'static str {
match format {
AudioFormat::Mp3 => "mp3",
AudioFormat::Flac => "flac",
AudioFormat::Wav => "wav",
}
}
fn desired(clip: Clip, format: AudioFormat) -> Desired {
Desired {
path: format!("{}.{}", clip.id, ext(format)),
lineage: LineageContext::own_root(&clip),
clip,
format,
meta_hash: "m".to_owned(),
art_hash: "art".to_owned(),
modes: vec![SourceMode::Mirror],
trashed: false,
private: false,
artifacts: Vec::new(),
}
}
fn entry(path: &str, format: AudioFormat) -> ManifestEntry {
ManifestEntry {
path: path.to_owned(),
format,
meta_hash: "old".to_owned(),
art_hash: "old-art".to_owned(),
size: 8,
preserve: false,
..Default::default()
}
}
#[allow(clippy::too_many_arguments)]
fn run(
plan: &Plan,
manifest: &mut Manifest,
desired: &[Desired],
http: &ScriptedHttp,
fs: &MemFs,
ffmpeg: &StubFfmpeg,
clock: &RecordingClock,
opts: &ExecOptions,
) -> ExecOutcome {
let mut albums = BTreeMap::new();
run_with_albums(
plan,
manifest,
&mut albums,
desired,
http,
fs,
ffmpeg,
clock,
opts,
)
}
#[allow(clippy::too_many_arguments)]
fn run_with_albums(
plan: &Plan,
manifest: &mut Manifest,
albums: &mut BTreeMap<String, AlbumArt>,
desired: &[Desired],
http: &ScriptedHttp,
fs: &MemFs,
ffmpeg: &StubFfmpeg,
clock: &RecordingClock,
opts: &ExecOptions,
) -> ExecOutcome {
let mut playlists = BTreeMap::new();
run_full(
plan,
manifest,
albums,
&mut playlists,
desired,
http,
fs,
ffmpeg,
clock,
opts,
)
}
#[allow(clippy::too_many_arguments)]
fn run_full(
plan: &Plan,
manifest: &mut Manifest,
albums: &mut BTreeMap<String, AlbumArt>,
playlists: &mut BTreeMap<String, PlaylistState>,
desired: &[Desired],
http: &ScriptedHttp,
fs: &MemFs,
ffmpeg: &StubFfmpeg,
clock: &RecordingClock,
opts: &ExecOptions,
) -> ExecOutcome {
let mut client = SunoClient::new(ClerkAuth::new("eyJtoken"), RecordingClock::new());
pollster::block_on(execute(
plan,
manifest,
albums,
playlists,
desired,
Ports {
client: &mut client,
http,
fs,
ffmpeg,
clock,
},
opts,
))
}
fn small_poll() -> ExecOptions {
ExecOptions {
max_retries: 3,
wav_poll_attempts: 2,
wav_poll_interval: Duration::from_secs(5),
concurrency: 4,
}
}
#[test]
fn download_mp3_writes_tagged_file_and_records_manifest() {
let c = art_clip("a");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new()
.route("a.mp3", Reply::ok(b"mp3-body".to_vec()))
.route("a/large.jpg", Reply::ok(b"art-bytes".to_vec()));
let fs = MemFs::new();
let ffmpeg = StubFfmpeg::flac();
let clock = RecordingClock::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&ffmpeg,
&clock,
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 1);
assert_eq!(outcome.failed(), 0);
assert_eq!(outcome.status, RunStatus::Completed);
let written = fs.read_file("a.mp3").unwrap();
assert_eq!(&written[..3], b"ID3");
assert!(written.ends_with(b"mp3-body"));
let entry = manifest.get("a").unwrap();
assert_eq!(entry.path, "a.mp3");
assert_eq!(entry.format, AudioFormat::Mp3);
assert_eq!(entry.meta_hash, "m");
assert_eq!(entry.art_hash, "art");
assert_eq!(entry.size, written.len() as u64);
assert!(!entry.preserve);
}
#[test]
fn download_mp3_uses_cdn_fallback_when_audio_url_empty() {
let mut c = clip("a");
c.audio_url = String::new();
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new().route("cdn1.suno.ai/a.mp3", Reply::ok(b"body".to_vec()));
let fs = MemFs::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 1);
assert_eq!(http.count("cdn1.suno.ai/a.mp3"), 1);
}
#[test]
fn download_flac_renders_transcodes_and_records() {
let c = clip("b");
let d = desired(c.clone(), AudioFormat::Flac);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Flac,
}],
};
let http = ScriptedHttp::new()
.with_auth()
.route(
"/wav_file/",
Reply::json(r#"{"wav_file_url": "https://cdn1.suno.ai/b.wav"}"#),
)
.route("b.wav", Reply::ok(b"wav-bytes".to_vec()));
let fs = MemFs::new();
let clock = RecordingClock::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&clock,
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 1);
assert_eq!(outcome.failed(), 0);
let written = fs.read_file("b.flac").unwrap();
assert_eq!(&written[..4], b"fLaC");
assert_eq!(manifest.get("b").unwrap().format, AudioFormat::Flac);
assert_eq!(http.count("/convert_wav/"), 0);
assert!(clock.sleeps().is_empty());
}
#[test]
fn download_flac_requests_render_then_polls_until_ready() {
let c = clip("c");
let d = desired(c.clone(), AudioFormat::Flac);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Flac,
}],
};
let http = ScriptedHttp::new()
.with_auth()
.route_seq(
"/wav_file/",
vec![
Reply::json("{}"),
Reply::json(r#"{"wav_file_url": "https://cdn1.suno.ai/c.wav"}"#),
],
)
.route("/convert_wav/", Reply::status(200))
.route("c.wav", Reply::ok(b"wav".to_vec()));
let clock = RecordingClock::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs_new(),
&StubFfmpeg::flac(),
&clock,
&small_poll(),
);
assert_eq!(outcome.downloaded, 1);
assert_eq!(http.count("/convert_wav/"), 1);
assert_eq!(clock.sleeps(), vec![Duration::from_secs(5)]);
}
#[test]
fn download_flac_unavailable_render_is_a_nonfatal_failure() {
let c = clip("d");
let d = desired(c.clone(), AudioFormat::Flac);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Flac,
}],
};
let http = ScriptedHttp::new()
.with_auth()
.route("/wav_file/", Reply::json("{}"))
.route("/convert_wav/", Reply::status(200));
let fs = MemFs::new();
let clock = RecordingClock::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&clock,
&small_poll(),
);
assert_eq!(outcome.downloaded, 0);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.failures[0].clip_id, "d");
assert_eq!(outcome.status, RunStatus::Completed);
assert!(!fs.exists("d.flac"));
assert_eq!(clock.sleeps().len(), 2);
}
#[test]
fn flac_transcode_failure_is_recorded_and_skipped() {
let c = clip("t");
let d = desired(c.clone(), AudioFormat::Flac);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Flac,
}],
};
let http = ScriptedHttp::new()
.with_auth()
.route(
"/wav_file/",
Reply::json(r#"{"wav_file_url": "https://cdn1.suno.ai/t.wav"}"#),
)
.route("t.wav", Reply::ok(b"wav".to_vec()));
let fs = MemFs::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::failing(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 0);
assert_eq!(outcome.failed(), 1);
assert!(!fs.exists("t.flac"));
assert!(manifest.get("t").is_none());
}
#[test]
fn cover_falls_back_when_large_image_is_missing() {
let c = art_clip("e");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new()
.route("e.mp3", Reply::ok(b"body".to_vec()))
.route("e/large.jpg", Reply::status(404))
.route("e/small.jpg", Reply::ok(b"the-art".to_vec()));
let fs = MemFs::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 1);
let calls = http.calls();
let large = calls
.iter()
.position(|u| u.contains("e/large.jpg"))
.unwrap();
let small = calls
.iter()
.position(|u| u.contains("e/small.jpg"))
.unwrap();
assert!(large < small, "large art tried before small");
}
#[test]
fn failed_write_leaves_the_prior_file_intact() {
let c = clip("f");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new().route("f.mp3", Reply::ok(b"new-body".to_vec()));
let fs = MemFs::new()
.with_file("f.mp3", b"OLD-CONTENT".to_vec())
.fail_write("f.mp3");
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 0);
assert_eq!(outcome.failed(), 1);
assert_eq!(fs.read_file("f.mp3").unwrap(), b"OLD-CONTENT");
assert!(manifest.get("f").is_none());
}
#[test]
fn size_mismatch_after_write_is_a_failure() {
let c = clip("g");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new().route("g.mp3", Reply::ok(b"body".to_vec()));
let fs = MemFs::new().corrupt_write("g.mp3");
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 0);
assert_eq!(outcome.failed(), 1);
assert!(outcome.failures[0].reason.contains("expected"));
assert!(manifest.get("g").is_none());
}
#[test]
fn transient_failure_is_retried_then_skipped() {
let c = clip("h");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new().route("h.mp3", Reply::status(500));
let fs = MemFs::new();
let clock = RecordingClock::new();
let opts = ExecOptions {
max_retries: 2,
..ExecOptions::default()
};
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&clock,
&opts,
);
assert_eq!(outcome.downloaded, 0);
assert_eq!(outcome.failed(), 1);
assert_eq!(http.count("h.mp3"), 3);
assert_eq!(clock.sleeps().len(), 2);
}
#[test]
fn truncated_download_is_retried_then_succeeds() {
let c = clip("i");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new().route_seq(
"i.mp3",
vec![
Reply::ok(b"short".to_vec()).with_content_length(999),
Reply::ok(b"good-body".to_vec()),
],
);
let fs = MemFs::new();
let clock = RecordingClock::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&clock,
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 1);
assert_eq!(http.count("i.mp3"), 2);
assert_eq!(clock.sleeps().len(), 1);
}
#[test]
fn rate_limit_backs_off_using_retry_after() {
let c = clip("j");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new().route_seq(
"j.mp3",
vec![
Reply::status(429).with_retry_after(7),
Reply::ok(b"body".to_vec()),
],
);
let fs = MemFs::new();
let clock = RecordingClock::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&clock,
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 1);
assert_eq!(clock.sleeps(), vec![Duration::from_secs(7)]);
}
#[test]
fn auth_failure_aborts_the_run() {
let c1 = clip("k1");
let c2 = clip("k2");
let d1 = desired(c1.clone(), AudioFormat::Flac);
let d2 = desired(c2.clone(), AudioFormat::Flac);
let plan = Plan {
actions: vec![
Action::Download {
clip: c1.clone(),
lineage: LineageContext::own_root(&c1),
path: d1.path.clone(),
format: AudioFormat::Flac,
},
Action::Download {
clip: c2.clone(),
lineage: LineageContext::own_root(&c2),
path: d2.path.clone(),
format: AudioFormat::Flac,
},
],
};
let http = ScriptedHttp::new()
.with_auth()
.route("/wav_file/", Reply::status(401));
let fs = MemFs::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d1, d2],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&small_poll(),
);
assert_eq!(outcome.status, RunStatus::AuthAborted);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.failures[0].clip_id, "k1");
assert_eq!(outcome.downloaded, 0);
}
#[test]
fn disk_full_primary_write_aborts_the_run() {
let c1 = clip("d1");
let c2 = clip("d2");
let d1 = desired(c1.clone(), AudioFormat::Mp3);
let d2 = desired(c2.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![
Action::Download {
clip: c1.clone(),
lineage: LineageContext::own_root(&c1),
path: d1.path.clone(),
format: AudioFormat::Mp3,
},
Action::Download {
clip: c2.clone(),
lineage: LineageContext::own_root(&c2),
path: d2.path.clone(),
format: AudioFormat::Mp3,
},
],
};
let http = ScriptedHttp::new()
.route("d1.mp3", Reply::ok(b"body-1".to_vec()))
.route("d2.mp3", Reply::ok(b"body-2".to_vec()));
let fs = MemFs::new().fail_write_out_of_space("d1.mp3");
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d1, d2],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.status, RunStatus::DiskFull);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.failures[0].clip_id, "d1");
assert!(outcome.failures[0].reason.contains("disk full"));
assert_eq!(outcome.downloaded, 0);
assert_eq!(http.count("d2.mp3"), 0);
assert!(!fs.exists("d2.mp3"));
}
#[test]
fn disk_full_flac_transcode_aborts_the_run() {
let c1 = clip("d1");
let c2 = clip("d2");
let d1 = desired(c1.clone(), AudioFormat::Flac);
let d2 = desired(c2.clone(), AudioFormat::Flac);
let plan = Plan {
actions: vec![
Action::Download {
clip: c1.clone(),
lineage: LineageContext::own_root(&c1),
path: d1.path.clone(),
format: AudioFormat::Flac,
},
Action::Download {
clip: c2.clone(),
lineage: LineageContext::own_root(&c2),
path: d2.path.clone(),
format: AudioFormat::Flac,
},
],
};
let http = ScriptedHttp::new()
.with_auth()
.route(
"/wav_file/",
Reply::json(r#"{"wav_file_url": "https://cdn1.suno.ai/d1.wav"}"#),
)
.route(".wav", Reply::ok(b"wav".to_vec()));
let fs = MemFs::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d1, d2],
&http,
&fs,
&StubFfmpeg::out_of_space(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.status, RunStatus::DiskFull);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.failures[0].clip_id, "d1");
assert!(outcome.failures[0].reason.contains("disk full"));
assert_eq!(outcome.downloaded, 0);
}
#[test]
fn disk_full_artifact_write_aborts_the_run() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "a/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/a/large.jpg".to_owned(),
hash: "h1".to_owned(),
owner_id: "a".to_owned(),
content: None,
}],
};
let http = ScriptedHttp::new().route("a/large.jpg", Reply::ok(b"jpg-bytes".to_vec()));
let fs = MemFs::new().fail_write_out_of_space("a/cover.jpg");
let outcome = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.status, RunStatus::DiskFull);
assert_eq!(outcome.failed(), 1);
assert!(outcome.failures[0].reason.contains("disk full"));
assert_eq!(outcome.artifacts_written, 0);
assert_eq!(manifest.get("a").unwrap().cover_jpg, None);
}
#[test]
fn disk_full_leaves_the_failed_clips_manifest_entry_unchanged() {
let c = clip("m");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new().route("m.mp3", Reply::ok(b"new-body".to_vec()));
let fs = MemFs::new()
.with_file("m.mp3", b"OLD-CONTENT".to_vec())
.fail_write_out_of_space("m.mp3");
let mut manifest = Manifest::new();
let before = entry("m.mp3", AudioFormat::Mp3);
manifest.insert("m", before.clone());
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.status, RunStatus::DiskFull);
assert_eq!(manifest.get("m"), Some(&before));
assert_eq!(fs.read_file("m.mp3").unwrap(), b"OLD-CONTENT");
}
#[test]
fn cdn_download_rejection_skips_the_clip_without_aborting() {
let c1 = clip("k1");
let c2 = clip("k2");
let d1 = desired(c1.clone(), AudioFormat::Mp3);
let d2 = desired(c2.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![
Action::Download {
clip: c1.clone(),
lineage: LineageContext::own_root(&c1),
path: d1.path.clone(),
format: AudioFormat::Mp3,
},
Action::Download {
clip: c2.clone(),
lineage: LineageContext::own_root(&c2),
path: d2.path.clone(),
format: AudioFormat::Mp3,
},
],
};
let http = ScriptedHttp::new()
.route("k1.mp3", Reply::status(403))
.route("k2.mp3", Reply::ok(b"body".to_vec()));
let fs = MemFs::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d1, d2],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_ne!(outcome.status, RunStatus::AuthAborted);
assert_eq!(outcome.downloaded, 1);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.failures[0].clip_id, "k1");
}
#[test]
fn one_clip_failure_does_not_abort_the_run() {
let c1 = clip("l1");
let c2 = clip("l2");
let d1 = desired(c1.clone(), AudioFormat::Mp3);
let d2 = desired(c2.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![
Action::Download {
clip: c1.clone(),
lineage: LineageContext::own_root(&c1),
path: d1.path.clone(),
format: AudioFormat::Mp3,
},
Action::Download {
clip: c2.clone(),
lineage: LineageContext::own_root(&c2),
path: d2.path.clone(),
format: AudioFormat::Mp3,
},
],
};
let http = ScriptedHttp::new()
.route("l1.mp3", Reply::status(404))
.route("l2.mp3", Reply::ok(b"body".to_vec()));
let fs = MemFs::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d1, d2],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.status, RunStatus::Completed);
assert_eq!(outcome.downloaded, 1);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.failures[0].clip_id, "l1");
assert!(fs.exists("l2.mp3"));
assert!(manifest.get("l2").is_some());
assert!(manifest.get("l1").is_none());
}
#[test]
fn preserve_is_set_for_copy_held_and_private_clips() {
let mut mirror = desired(clip("m1"), AudioFormat::Mp3);
mirror.modes = vec![SourceMode::Mirror];
let mut copy_held = desired(clip("m2"), AudioFormat::Mp3);
copy_held.modes = vec![SourceMode::Mirror, SourceMode::Copy];
let mut private = desired(clip("m3"), AudioFormat::Mp3);
private.private = true;
let plan = Plan {
actions: vec![
Action::Download {
clip: mirror.clip.clone(),
lineage: LineageContext::own_root(&mirror.clip),
path: mirror.path.clone(),
format: AudioFormat::Mp3,
},
Action::Download {
clip: copy_held.clip.clone(),
lineage: LineageContext::own_root(©_held.clip),
path: copy_held.path.clone(),
format: AudioFormat::Mp3,
},
Action::Download {
clip: private.clip.clone(),
lineage: LineageContext::own_root(&private.clip),
path: private.path.clone(),
format: AudioFormat::Mp3,
},
],
};
let http = ScriptedHttp::new()
.route("m1.mp3", Reply::ok(b"a".to_vec()))
.route("m2.mp3", Reply::ok(b"b".to_vec()))
.route("m3.mp3", Reply::ok(b"c".to_vec()));
let fs = MemFs::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[mirror, copy_held, private],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.downloaded, 3);
assert!(!manifest.get("m1").unwrap().preserve);
assert!(manifest.get("m2").unwrap().preserve);
assert!(manifest.get("m3").unwrap().preserve);
}
#[test]
fn reformat_writes_new_format_and_removes_old_file() {
let c = clip("n");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Reformat {
clip: c.clone(),
path: "n.mp3".to_owned(),
from_path: "n.flac".to_owned(),
from: AudioFormat::Flac,
to: AudioFormat::Mp3,
}],
};
let http = ScriptedHttp::new().route("n.mp3", Reply::ok(b"body".to_vec()));
let fs = MemFs::new().with_file("n.flac", b"OLD-FLAC".to_vec());
let mut manifest = Manifest::new();
manifest.insert("n", entry("n.flac", AudioFormat::Flac));
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.reformatted, 1);
assert!(fs.exists("n.mp3"));
assert!(!fs.exists("n.flac"));
let updated = manifest.get("n").unwrap();
assert_eq!(updated.path, "n.mp3");
assert_eq!(updated.format, AudioFormat::Mp3);
assert_eq!(updated.meta_hash, "m");
}
#[test]
fn retag_rewrites_file_and_updates_hashes() {
let c = clip("o");
let mut d = desired(c.clone(), AudioFormat::Mp3);
d.meta_hash = "new".to_owned();
d.art_hash = "new-art".to_owned();
let existing = tag_mp3(
b"audio",
&TrackMetadata::from_clip(&c, &LineageContext::own_root(&c)),
None,
)
.unwrap();
let fs = MemFs::new().with_file("o.mp3", existing.clone());
let mut manifest = Manifest::new();
let mut start = entry("o.mp3", AudioFormat::Mp3);
start.size = existing.len() as u64;
manifest.insert("o", start);
let plan = Plan {
actions: vec![Action::Retag {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: "o.mp3".to_owned(),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[d],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.retagged, 1);
let updated = manifest.get("o").unwrap();
assert_eq!(updated.meta_hash, "new");
assert_eq!(updated.art_hash, "new-art");
assert_eq!(&fs.read_file("o.mp3").unwrap()[..3], b"ID3");
}
#[test]
fn rename_moves_file_and_updates_manifest_path() {
let c = clip("p");
let mut d = desired(c.clone(), AudioFormat::Mp3);
d.path = "new/p.mp3".to_owned();
let fs = MemFs::new().with_file("old/p.mp3", b"DATA".to_vec());
let mut manifest = Manifest::new();
manifest.insert("p", entry("old/p.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![Action::Rename {
from: "old/p.mp3".to_owned(),
to: "new/p.mp3".to_owned(),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[d],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.renamed, 1);
assert!(fs.exists("new/p.mp3"));
assert!(!fs.exists("old/p.mp3"));
assert_eq!(manifest.get("p").unwrap().path, "new/p.mp3");
}
#[test]
fn disk_full_rename_aborts_the_run() {
let c = clip("p");
let mut d = desired(c.clone(), AudioFormat::Mp3);
d.path = "new/p.mp3".to_owned();
let fs = MemFs::new()
.with_file("old/p.mp3", b"DATA".to_vec())
.fail_rename_out_of_space("new/p.mp3");
let mut manifest = Manifest::new();
manifest.insert("p", entry("old/p.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![Action::Rename {
from: "old/p.mp3".to_owned(),
to: "new/p.mp3".to_owned(),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[d],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.status, RunStatus::DiskFull);
assert_eq!(outcome.renamed, 0);
assert_eq!(outcome.failed(), 1);
assert!(outcome.failures[0].reason.contains("disk full"));
assert!(fs.exists("old/p.mp3"));
assert!(!fs.exists("new/p.mp3"));
assert_eq!(manifest.get("p").unwrap().path, "old/p.mp3");
}
#[test]
fn delete_removes_file_and_manifest_entry() {
let fs = MemFs::new().with_file("q.mp3", b"DATA".to_vec());
let mut manifest = Manifest::new();
manifest.insert("q", entry("q.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![Action::Delete {
path: "q.mp3".to_owned(),
clip_id: "q".to_owned(),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.deleted, 1);
assert!(!fs.exists("q.mp3"));
assert!(manifest.get("q").is_none());
}
#[test]
fn failed_delete_keeps_the_manifest_entry() {
let fs = MemFs::new()
.with_file("s.mp3", b"DATA".to_vec())
.fail_remove("s.mp3");
let mut manifest = Manifest::new();
manifest.insert("s", entry("s.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![Action::Delete {
path: "s.mp3".to_owned(),
clip_id: "s".to_owned(),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.deleted, 0);
assert_eq!(outcome.failed(), 1);
assert!(manifest.get("s").is_some());
assert!(fs.exists("s.mp3"));
}
#[test]
fn skip_is_a_noop() {
let mut manifest = Manifest::new();
let plan = Plan {
actions: vec![Action::Skip {
clip_id: "r".to_owned(),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&MemFs::new(),
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.skipped, 1);
assert_eq!(outcome.failed(), 0);
}
#[test]
fn header_helpers_parse_or_ignore() {
let resp = HttpResponse {
status: 200,
headers: vec![("Content-Length".to_owned(), "42".to_owned())],
body: Vec::new(),
};
assert_eq!(content_length(&resp), Some(42));
let bare = HttpResponse {
status: 200,
headers: Vec::new(),
body: Vec::new(),
};
assert_eq!(content_length(&bare), None);
}
#[test]
fn preserve_rule_covers_copy_and_private() {
let base = desired(clip("x"), AudioFormat::Mp3);
assert!(!preserve_for(&base));
let mut copy_held = base.clone();
copy_held.modes = vec![SourceMode::Copy];
assert!(preserve_for(©_held));
let mut private = base.clone();
private.private = true;
assert!(preserve_for(&private));
}
fn fs_new() -> MemFs {
MemFs::new()
}
#[test]
fn skip_sets_preserve_when_a_clip_becomes_copy_held() {
let c = clip("s1");
let mut d = desired(c.clone(), AudioFormat::Mp3);
d.modes = vec![SourceMode::Copy];
let plan = Plan {
actions: vec![Action::Skip {
clip_id: "s1".to_owned(),
}],
};
let mut manifest = Manifest::new();
manifest.insert("s1".to_owned(), entry("s1.mp3", AudioFormat::Mp3));
assert!(!manifest.get("s1").unwrap().preserve);
let outcome = run(
&plan,
&mut manifest,
&[d],
&ScriptedHttp::new(),
&fs_new(),
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.skipped, 1);
assert!(
manifest.get("s1").unwrap().preserve,
"a copy-held skip must mark the entry preserved"
);
}
#[test]
fn skip_clears_stale_preserve_when_a_clip_returns_to_mirror_only() {
let c = clip("s2");
let d = desired(c.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![Action::Skip {
clip_id: "s2".to_owned(),
}],
};
let mut manifest = Manifest::new();
let mut stale = entry("s2.mp3", AudioFormat::Mp3);
stale.preserve = true;
manifest.insert("s2".to_owned(), stale);
run(
&plan,
&mut manifest,
&[d],
&ScriptedHttp::new(),
&fs_new(),
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert!(
!manifest.get("s2").unwrap().preserve,
"a mirror-only skip must clear a stale preserve marker"
);
}
#[test]
fn flac_render_retries_a_rate_limited_wav_lookup() {
let c = clip("rl");
let d = desired(c.clone(), AudioFormat::Flac);
let plan = Plan {
actions: vec![Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format: AudioFormat::Flac,
}],
};
let http = ScriptedHttp::new()
.with_auth()
.route_seq(
"/wav_file/",
vec![
Reply::status(429),
Reply::json(r#"{"wav_file_url": "https://cdn1.suno.ai/rl.wav"}"#),
],
)
.route("rl.wav", Reply::ok(b"wav".to_vec()));
let clock = RecordingClock::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[d],
&http,
&fs_new(),
&StubFfmpeg::flac(),
&clock,
&small_poll(),
);
assert_eq!(outcome.downloaded, 1);
assert_eq!(outcome.failed(), 0);
assert_eq!(http.count("/convert_wav/"), 0);
assert_eq!(clock.sleeps(), vec![Duration::from_secs(1)]);
}
#[test]
fn write_artifact_fetches_writes_and_updates_manifest() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "a/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/a/large.jpg".to_owned(),
hash: "h1".to_owned(),
owner_id: "a".to_owned(),
content: None,
}],
};
let http = ScriptedHttp::new().route("a/large.jpg", Reply::ok(b"jpg-bytes".to_vec()));
let fs = MemFs::new();
let outcome = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_written, 1);
assert_eq!(outcome.failed(), 0);
assert_eq!(outcome.status, RunStatus::Completed);
assert_eq!(fs.read_file("a/cover.jpg").unwrap(), b"jpg-bytes");
assert_eq!(
manifest.get("a").unwrap().cover_jpg,
Some(ArtifactState {
path: "a/cover.jpg".to_owned(),
hash: "h1".to_owned(),
})
);
}
#[test]
fn write_text_sidecar_records_slot_with_no_network_fetch() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::DetailsTxt,
path: "a.details.txt".to_owned(),
source_url: String::new(),
hash: "dh".to_owned(),
owner_id: "a".to_owned(),
content: Some("Title: A\n".to_owned()),
}],
};
let http = ScriptedHttp::new();
let fs = MemFs::new();
let outcome = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_written, 1);
assert_eq!(outcome.failed(), 0);
assert_eq!(fs.read_file("a.details.txt").unwrap(), b"Title: A\n");
assert_eq!(
manifest.get("a").unwrap().details_txt,
Some(ArtifactState {
path: "a.details.txt".to_owned(),
hash: "dh".to_owned(),
})
);
}
#[test]
fn write_lyrics_sidecar_relocation_removes_old_file() {
let mut manifest = Manifest::new();
let mut e = entry("old/a.flac", AudioFormat::Flac);
e.lyrics_txt = Some(ArtifactState {
path: "old/a.lyrics.txt".to_owned(),
hash: "lh".to_owned(),
});
manifest.insert("a", e);
let fs = MemFs::new()
.with_file("old/a.flac", b"AUDIO".to_vec())
.with_file("old/a.lyrics.txt", b"old words\n".to_vec());
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::LyricsTxt,
path: "new/a.lyrics.txt".to_owned(),
source_url: String::new(),
hash: "lh".to_owned(),
owner_id: "a".to_owned(),
content: Some("new words\n".to_owned()),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.failed(), 0);
assert_eq!(fs.read_file("new/a.lyrics.txt").unwrap(), b"new words\n");
assert!(!fs.exists("old/a.lyrics.txt"));
assert_eq!(
manifest.get("a").unwrap().lyrics_txt.as_ref().unwrap().path,
"new/a.lyrics.txt"
);
}
#[test]
fn sidecar_path_swap_never_deletes_a_file_written_this_run() {
let mut manifest = Manifest::new();
let mut a = entry("a.flac", AudioFormat::Flac);
a.lyrics_txt = Some(ArtifactState {
path: "x.lyrics.txt".to_owned(),
hash: "ah".to_owned(),
});
manifest.insert("a", a);
let mut b = entry("b.flac", AudioFormat::Flac);
b.lyrics_txt = Some(ArtifactState {
path: "y.lyrics.txt".to_owned(),
hash: "bh".to_owned(),
});
manifest.insert("b", b);
let fs = MemFs::new()
.with_file("a.flac", b"A".to_vec())
.with_file("b.flac", b"B".to_vec())
.with_file("x.lyrics.txt", b"A words\n".to_vec())
.with_file("y.lyrics.txt", b"B words\n".to_vec());
let plan = Plan {
actions: vec![
Action::WriteArtifact {
kind: ArtifactKind::LyricsTxt,
path: "y.lyrics.txt".to_owned(),
source_url: String::new(),
hash: "ah".to_owned(),
owner_id: "a".to_owned(),
content: Some("A words\n".to_owned()),
},
Action::WriteArtifact {
kind: ArtifactKind::LyricsTxt,
path: "x.lyrics.txt".to_owned(),
source_url: String::new(),
hash: "bh".to_owned(),
owner_id: "b".to_owned(),
content: Some("B words\n".to_owned()),
},
],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.failed(), 0);
assert_eq!(fs.read_file("y.lyrics.txt").unwrap(), b"A words\n");
assert_eq!(fs.read_file("x.lyrics.txt").unwrap(), b"B words\n");
assert_eq!(
manifest.get("a").unwrap().lyrics_txt.as_ref().unwrap().path,
"y.lyrics.txt"
);
assert_eq!(
manifest.get("b").unwrap().lyrics_txt.as_ref().unwrap().path,
"x.lyrics.txt"
);
}
#[test]
fn old_sidecar_kept_when_another_clip_still_references_it() {
let mut manifest = Manifest::new();
let mut a = entry("a.flac", AudioFormat::Flac);
a.lyrics_txt = Some(ArtifactState {
path: "y.lyrics.txt".to_owned(),
hash: "ah".to_owned(),
});
manifest.insert("a", a);
let mut b = entry("b.flac", AudioFormat::Flac);
b.lyrics_txt = Some(ArtifactState {
path: "y.lyrics.txt".to_owned(),
hash: "bh".to_owned(),
});
manifest.insert("b", b);
let fs = MemFs::new()
.with_file("a.flac", b"A".to_vec())
.with_file("b.flac", b"B".to_vec())
.with_file("y.lyrics.txt", b"A words\n".to_vec());
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::LyricsTxt,
path: "x.lyrics.txt".to_owned(),
source_url: String::new(),
hash: "bh".to_owned(),
owner_id: "b".to_owned(),
content: Some("B words\n".to_owned()),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.failed(), 0);
assert!(
fs.exists("y.lyrics.txt"),
"A's live sidecar must not be deleted"
);
assert_eq!(fs.read_file("x.lyrics.txt").unwrap(), b"B words\n");
}
#[test]
fn shared_old_path_is_reclaimed_when_every_referencing_clip_moves_away() {
let mut manifest = Manifest::new();
let mut a = entry("a.flac", AudioFormat::Flac);
a.lyrics_txt = Some(ArtifactState {
path: "s.lyrics.txt".to_owned(),
hash: "ah".to_owned(),
});
manifest.insert("a", a);
let mut b = entry("b.flac", AudioFormat::Flac);
b.lyrics_txt = Some(ArtifactState {
path: "s.lyrics.txt".to_owned(),
hash: "bh".to_owned(),
});
manifest.insert("b", b);
let fs = MemFs::new()
.with_file("a.flac", b"A".to_vec())
.with_file("b.flac", b"B".to_vec())
.with_file("s.lyrics.txt", b"shared\n".to_vec());
let plan = Plan {
actions: vec![
Action::WriteArtifact {
kind: ArtifactKind::LyricsTxt,
path: "pa.lyrics.txt".to_owned(),
source_url: String::new(),
hash: "ah".to_owned(),
owner_id: "a".to_owned(),
content: Some("A words\n".to_owned()),
},
Action::WriteArtifact {
kind: ArtifactKind::LyricsTxt,
path: "pb.lyrics.txt".to_owned(),
source_url: String::new(),
hash: "bh".to_owned(),
owner_id: "b".to_owned(),
content: Some("B words\n".to_owned()),
},
],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.failed(), 0);
assert_eq!(fs.read_file("pa.lyrics.txt").unwrap(), b"A words\n");
assert_eq!(fs.read_file("pb.lyrics.txt").unwrap(), b"B words\n");
assert!(
!fs.exists("s.lyrics.txt"),
"the vacated shared path must be reclaimed, not orphaned"
);
}
#[test]
fn write_text_sidecar_skipped_when_owner_audio_absent() {
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::DetailsTxt,
path: "gone.details.txt".to_owned(),
source_url: String::new(),
hash: "dh".to_owned(),
owner_id: "gone".to_owned(),
content: Some("Title: Gone\n".to_owned()),
}],
};
let fs = MemFs::new();
let mut manifest = Manifest::new();
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_written, 0);
assert_eq!(outcome.skipped, 1);
assert!(!fs.exists("gone.details.txt"));
assert!(manifest.get("gone").is_none());
}
#[test]
fn delete_artifact_removes_file_and_clears_slot() {
let fs = MemFs::new().with_file("a/cover.jpg", b"jpg".to_vec());
let mut manifest = Manifest::new();
let mut e = entry("a.mp3", AudioFormat::Mp3);
e.cover_jpg = Some(ArtifactState {
path: "a/cover.jpg".to_owned(),
hash: "h1".to_owned(),
});
manifest.insert("a", e);
let plan = Plan {
actions: vec![Action::DeleteArtifact {
kind: ArtifactKind::CoverJpg,
path: "a/cover.jpg".to_owned(),
owner_id: "a".to_owned(),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_deleted, 1);
assert!(!fs.exists("a/cover.jpg"));
assert_eq!(manifest.get("a").unwrap().cover_jpg, None);
}
#[test]
fn delete_artifact_tolerates_already_absent_file() {
let mut manifest = Manifest::new();
let mut e = entry("a.mp3", AudioFormat::Mp3);
e.cover_jpg = Some(ArtifactState {
path: "a/cover.jpg".to_owned(),
hash: "h1".to_owned(),
});
manifest.insert("a", e);
let plan = Plan {
actions: vec![Action::DeleteArtifact {
kind: ArtifactKind::CoverJpg,
path: "a/cover.jpg".to_owned(),
owner_id: "a".to_owned(),
}],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&MemFs::new(),
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_deleted, 1);
assert_eq!(outcome.failed(), 0);
assert_eq!(manifest.get("a").unwrap().cover_jpg, None);
}
#[test]
fn write_artifact_http_failure_is_a_per_clip_failure_not_a_run_abort() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.mp3", AudioFormat::Mp3));
manifest.insert("b", entry("b.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![
Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "a/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/a/large.jpg".to_owned(),
hash: "h1".to_owned(),
owner_id: "a".to_owned(),
content: None,
},
Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "b/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/b/large.jpg".to_owned(),
hash: "h2".to_owned(),
owner_id: "b".to_owned(),
content: None,
},
],
};
let http = ScriptedHttp::new()
.route("a/large.jpg", Reply::status(404))
.route("b/large.jpg", Reply::ok(b"jpg-b".to_vec()));
let fs = MemFs::new();
let outcome = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.status, RunStatus::Completed);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.failures[0].clip_id, "a");
assert_eq!(outcome.artifacts_written, 1);
assert!(!fs.exists("a/cover.jpg"));
assert_eq!(manifest.get("a").unwrap().cover_jpg, None);
assert_eq!(fs.read_file("b/cover.jpg").unwrap(), b"jpg-b");
assert!(manifest.get("b").unwrap().cover_jpg.is_some());
}
#[test]
fn co_delete_executes_audio_delete_then_artifact_delete() {
let fs = MemFs::new()
.with_file("gone.mp3", b"DATA".to_vec())
.with_file("gone/cover.jpg", b"jpg".to_vec());
let mut manifest = Manifest::new();
let mut e = entry("gone.mp3", AudioFormat::Mp3);
e.cover_jpg = Some(ArtifactState {
path: "gone/cover.jpg".to_owned(),
hash: "h1".to_owned(),
});
manifest.insert("gone", e);
let plan = Plan {
actions: vec![
Action::Delete {
path: "gone.mp3".to_owned(),
clip_id: "gone".to_owned(),
},
Action::DeleteArtifact {
kind: ArtifactKind::CoverJpg,
path: "gone/cover.jpg".to_owned(),
owner_id: "gone".to_owned(),
},
],
};
let outcome = run(
&plan,
&mut manifest,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.deleted, 1);
assert_eq!(outcome.artifacts_deleted, 1);
assert_eq!(outcome.failed(), 0);
assert!(!fs.exists("gone.mp3"));
assert!(!fs.exists("gone/cover.jpg"));
assert!(manifest.get("gone").is_none());
}
#[test]
fn write_artifact_is_skipped_when_the_owner_audio_is_absent() {
let ca = clip("a");
let plan = Plan {
actions: vec![
Action::Download {
clip: ca.clone(),
lineage: LineageContext::own_root(&ca),
path: "a.mp3".to_owned(),
format: AudioFormat::Mp3,
},
Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "a/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/a/large.jpg".to_owned(),
hash: "h1".to_owned(),
owner_id: "a".to_owned(),
content: None,
},
Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "b/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/b/large.jpg".to_owned(),
hash: "h2".to_owned(),
owner_id: "b".to_owned(),
content: None,
},
],
};
let http = ScriptedHttp::new()
.route("a.mp3", Reply::status(404))
.route("a/large.jpg", Reply::ok(b"jpg-a".to_vec()))
.route("b/large.jpg", Reply::ok(b"jpg-b".to_vec()));
let fs = MemFs::new();
let mut manifest = Manifest::new();
manifest.insert("b", entry("b.mp3", AudioFormat::Mp3));
let outcome = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.status, RunStatus::Completed);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.failures[0].clip_id, "a");
assert_eq!(outcome.skipped, 1);
assert_eq!(http.count("a/large.jpg"), 0);
assert!(!fs.exists("a/cover.jpg"));
assert!(manifest.get("a").is_none());
assert_eq!(outcome.artifacts_written, 1);
assert_eq!(fs.read_file("b/cover.jpg").unwrap(), b"jpg-b");
assert!(manifest.get("b").unwrap().cover_jpg.is_some());
}
#[test]
fn write_artifact_transcodes_animated_cover_to_webp() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::CoverWebp,
path: "a/cover.webp".to_owned(),
source_url: "https://cdn.suno.ai/a/video.mp4".to_owned(),
hash: "v1".to_owned(),
owner_id: "a".to_owned(),
content: None,
}],
};
let http = ScriptedHttp::new().route("a/video.mp4", Reply::ok(b"mp4-bytes".to_vec()));
let fs = MemFs::new();
let ffmpeg = StubFfmpeg::webp();
let outcome = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&ffmpeg,
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_written, 1);
assert_eq!(outcome.failed(), 0);
assert_eq!(outcome.status, RunStatus::Completed);
assert_eq!(http.count("a/video.mp4"), 1);
let written = fs.read_file("a/cover.webp").unwrap();
assert_ne!(written, b"mp4-bytes");
assert!(written.starts_with(b"RIFF"));
assert_eq!(
manifest.get("a").unwrap().cover_webp,
Some(ArtifactState {
path: "a/cover.webp".to_owned(),
hash: "v1".to_owned(),
})
);
}
#[test]
fn write_artifact_webp_transcode_failure_is_per_clip() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.mp3", AudioFormat::Mp3));
manifest.insert("b", entry("b.mp3", AudioFormat::Mp3));
let plan = Plan {
actions: vec![
Action::WriteArtifact {
kind: ArtifactKind::CoverWebp,
path: "a/cover.webp".to_owned(),
source_url: "https://cdn.suno.ai/a/video.mp4".to_owned(),
hash: "v1".to_owned(),
owner_id: "a".to_owned(),
content: None,
},
Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "b/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/b/large.jpg".to_owned(),
hash: "h1".to_owned(),
owner_id: "b".to_owned(),
content: None,
},
],
};
let http = ScriptedHttp::new()
.route("a/video.mp4", Reply::ok(b"mp4-bytes".to_vec()))
.route("b/large.jpg", Reply::ok(b"jpg-b".to_vec()));
let fs = MemFs::new();
let outcome = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::failing(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.status, RunStatus::Completed);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.failures[0].clip_id, "a");
assert!(!fs.exists("a/cover.webp"));
assert_eq!(manifest.get("a").unwrap().cover_webp, None);
assert_eq!(outcome.artifacts_written, 1);
assert_eq!(fs.read_file("b/cover.jpg").unwrap(), b"jpg-b");
assert!(manifest.get("b").unwrap().cover_jpg.is_some());
}
#[test]
fn folder_jpg_write_records_album_state_and_skips_manifest() {
let mut manifest = Manifest::new();
let mut albums: BTreeMap<String, AlbumArt> = BTreeMap::new();
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::FolderJpg,
path: "creator/album/folder.jpg".to_owned(),
source_url: "https://art.suno.ai/root/large.jpg".to_owned(),
hash: "jh".to_owned(),
owner_id: "root".to_owned(),
content: None,
}],
};
let http = ScriptedHttp::new().route("root/large.jpg", Reply::ok(b"folder-jpg".to_vec()));
let fs = MemFs::new();
let outcome = run_with_albums(
&plan,
&mut manifest,
&mut albums,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_written, 1);
assert_eq!(outcome.status, RunStatus::Completed);
assert_eq!(
fs.read_file("creator/album/folder.jpg").unwrap(),
b"folder-jpg"
);
assert_eq!(
albums.get("root").unwrap().folder_jpg,
Some(ArtifactState {
path: "creator/album/folder.jpg".to_owned(),
hash: "jh".to_owned(),
})
);
assert!(manifest.get("root").is_none());
}
#[test]
fn folder_webp_write_transcodes_and_records_album_state() {
let mut manifest = Manifest::new();
let mut albums: BTreeMap<String, AlbumArt> = BTreeMap::new();
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::FolderWebp,
path: "creator/album/cover.webp".to_owned(),
source_url: "https://cdn.suno.ai/root/video.mp4".to_owned(),
hash: "wh".to_owned(),
owner_id: "root".to_owned(),
content: None,
}],
};
let http = ScriptedHttp::new().route("root/video.mp4", Reply::ok(b"mp4-bytes".to_vec()));
let fs = MemFs::new();
let outcome = run_with_albums(
&plan,
&mut manifest,
&mut albums,
&[],
&http,
&fs,
&StubFfmpeg::webp(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_written, 1);
assert_eq!(outcome.failed(), 0);
let written = fs.read_file("creator/album/cover.webp").unwrap();
assert_ne!(written, b"mp4-bytes");
assert!(written.starts_with(b"RIFF"));
assert_eq!(
albums.get("root").unwrap().folder_webp,
Some(ArtifactState {
path: "creator/album/cover.webp".to_owned(),
hash: "wh".to_owned(),
})
);
}
#[test]
fn folder_art_delete_clears_album_state() {
let fs = MemFs::new().with_file("creator/album/folder.jpg", b"jpg".to_vec());
let mut manifest = Manifest::new();
let mut albums: BTreeMap<String, AlbumArt> = BTreeMap::new();
albums.insert(
"root".to_owned(),
AlbumArt {
folder_jpg: Some(ArtifactState {
path: "creator/album/folder.jpg".to_owned(),
hash: "jh".to_owned(),
}),
folder_webp: None,
},
);
let plan = Plan {
actions: vec![Action::DeleteArtifact {
kind: ArtifactKind::FolderJpg,
path: "creator/album/folder.jpg".to_owned(),
owner_id: "root".to_owned(),
}],
};
let outcome = run_with_albums(
&plan,
&mut manifest,
&mut albums,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_deleted, 1);
assert!(!fs.exists("creator/album/folder.jpg"));
assert!(!albums.contains_key("root"));
}
#[test]
fn playlist_write_uses_inline_content_and_records_state() {
let mut manifest = Manifest::new();
let mut albums: BTreeMap<String, AlbumArt> = BTreeMap::new();
let mut playlists: BTreeMap<String, PlaylistState> = BTreeMap::new();
let body = "#EXTM3U\n#PLAYLIST:Road Trip\n#EXTINF:60,One\nA/One.flac\n";
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::Playlist,
path: "Road Trip.m3u8".to_owned(),
source_url: String::new(),
hash: "ph1".to_owned(),
owner_id: "pl1".to_owned(),
content: Some(body.to_owned()),
}],
};
let fs = MemFs::new();
let outcome = run_full(
&plan,
&mut manifest,
&mut albums,
&mut playlists,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_written, 1);
assert_eq!(outcome.failed(), 0);
assert_eq!(fs.read_file("Road Trip.m3u8").unwrap(), body.as_bytes());
assert_eq!(
playlists.get("pl1"),
Some(&PlaylistState {
name: "Road Trip".to_owned(),
path: "Road Trip.m3u8".to_owned(),
hash: "ph1".to_owned(),
})
);
}
#[test]
fn playlist_delete_removes_file_and_clears_state() {
let fs = MemFs::new().with_file("Old.m3u8", b"#EXTM3U\n".to_vec());
let mut manifest = Manifest::new();
let mut albums: BTreeMap<String, AlbumArt> = BTreeMap::new();
let mut playlists: BTreeMap<String, PlaylistState> = BTreeMap::new();
playlists.insert(
"pl1".to_owned(),
PlaylistState {
name: "Old".to_owned(),
path: "Old.m3u8".to_owned(),
hash: "ph1".to_owned(),
},
);
let plan = Plan {
actions: vec![Action::DeleteArtifact {
kind: ArtifactKind::Playlist,
path: "Old.m3u8".to_owned(),
owner_id: "pl1".to_owned(),
}],
};
let outcome = run_full(
&plan,
&mut manifest,
&mut albums,
&mut playlists,
&[],
&ScriptedHttp::new(),
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.artifacts_deleted, 1);
assert!(!fs.exists("Old.m3u8"));
assert!(
!playlists.contains_key("pl1"),
"the playlist row is cleared on delete"
);
}
#[test]
fn rename_move_relocates_cover_and_prunes_old_album() {
let mut manifest = Manifest::new();
let mut e = entry("Creator/AlbumA/song.flac", AudioFormat::Flac);
e.cover_jpg = Some(ArtifactState {
path: "Creator/AlbumA/cover.jpg".to_owned(),
hash: "h1".to_owned(),
});
manifest.insert("a", e);
let fs = MemFs::new()
.with_file("Creator/AlbumA/song.flac", b"AUDIO".to_vec())
.with_file("Creator/AlbumA/cover.jpg", b"old-jpg".to_vec());
let plan = Plan {
actions: vec![
Action::Rename {
from: "Creator/AlbumA/song.flac".to_owned(),
to: "Creator/AlbumB/song.flac".to_owned(),
},
Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "Creator/AlbumB/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/a/large.jpg".to_owned(),
hash: "h1".to_owned(),
owner_id: "a".to_owned(),
content: None,
},
],
};
let http = ScriptedHttp::new().route("a/large.jpg", Reply::ok(b"new-jpg".to_vec()));
let outcome = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.failed(), 0);
assert!(fs.exists("Creator/AlbumB/song.flac"));
assert_eq!(
fs.read_file("Creator/AlbumB/cover.jpg").unwrap(),
b"new-jpg"
);
assert!(!fs.exists("Creator/AlbumA/cover.jpg"));
assert!(!fs.exists("Creator/AlbumA/song.flac"));
assert_eq!(
manifest.get("a").unwrap().cover_jpg.as_ref().unwrap().path,
"Creator/AlbumB/cover.jpg"
);
assert!(!fs.has_dir("Creator/AlbumA"));
assert!(fs.has_dir("Creator/AlbumB"));
}
#[test]
fn rename_move_relocates_folder_art_and_prunes_old_album() {
let mut manifest = Manifest::new();
let mut albums: BTreeMap<String, AlbumArt> = BTreeMap::new();
albums.insert(
"root".to_owned(),
AlbumArt {
folder_jpg: Some(ArtifactState {
path: "Creator/AlbumA/folder.jpg".to_owned(),
hash: "jh".to_owned(),
}),
folder_webp: None,
},
);
let fs = MemFs::new().with_file("Creator/AlbumA/folder.jpg", b"old-folder".to_vec());
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::FolderJpg,
path: "Creator/AlbumB/folder.jpg".to_owned(),
source_url: "https://art.suno.ai/root/large.jpg".to_owned(),
hash: "jh".to_owned(),
owner_id: "root".to_owned(),
content: None,
}],
};
let http = ScriptedHttp::new().route("root/large.jpg", Reply::ok(b"new-folder".to_vec()));
let outcome = run_with_albums(
&plan,
&mut manifest,
&mut albums,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(outcome.failed(), 0);
assert_eq!(
fs.read_file("Creator/AlbumB/folder.jpg").unwrap(),
b"new-folder"
);
assert!(!fs.exists("Creator/AlbumA/folder.jpg"));
assert_eq!(
albums
.get("root")
.unwrap()
.folder_jpg
.as_ref()
.unwrap()
.path,
"Creator/AlbumB/folder.jpg"
);
assert!(!fs.has_dir("Creator/AlbumA"));
assert!(fs.has_dir("Creator/AlbumB"));
}
#[test]
fn prune_empty_dirs_removes_only_empty_dirs() {
let fs = MemFs::new()
.with_file("keep/full/song.flac", b"x".to_vec())
.with_file("hidden/.suno-manifest.json", b"{}".to_vec())
.with_dir("empty/leaf")
.with_dir("nested/a/b/c");
fs.prune_empty_dirs("").unwrap();
for gone in [
"empty",
"empty/leaf",
"nested",
"nested/a",
"nested/a/b",
"nested/a/b/c",
] {
assert!(!fs.has_dir(gone), "empty dir {gone} should be pruned");
}
assert!(fs.has_dir("keep"));
assert!(fs.has_dir("keep/full"));
assert!(fs.has_dir("hidden"));
assert!(fs.exists("keep/full/song.flac"));
assert!(fs.exists("hidden/.suno-manifest.json"));
}
#[test]
fn prune_empty_dirs_never_removes_the_named_root() {
let fs = MemFs::new().with_dir("empty/leaf");
fs.prune_empty_dirs("empty").unwrap();
assert!(fs.has_dir("empty"), "the named root is never removed");
assert!(!fs.has_dir("empty/leaf"));
}
#[test]
fn old_sidecar_remove_failure_is_per_clip_and_converges_next_run() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac);
e.cover_jpg = Some(ArtifactState {
path: "AlbumA/cover.jpg".to_owned(),
hash: "h1".to_owned(),
});
manifest.insert("a", e);
let fs = MemFs::new()
.with_file("a.flac", b"AUDIO".to_vec())
.with_file("AlbumA/cover.jpg", b"old".to_vec());
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "AlbumB/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/a/large.jpg".to_owned(),
hash: "h1".to_owned(),
owner_id: "a".to_owned(),
content: None,
}],
};
let http = ScriptedHttp::new().route("a/large.jpg", Reply::ok(b"new".to_vec()));
fs.arm_fail_remove("AlbumA/cover.jpg");
let first = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(
first.status,
RunStatus::Completed,
"a remove failure never aborts the run"
);
assert_eq!(first.failed(), 1);
assert!(fs.exists("AlbumB/cover.jpg"));
assert!(fs.exists("AlbumA/cover.jpg"));
assert_eq!(
manifest.get("a").unwrap().cover_jpg.as_ref().unwrap().path,
"AlbumA/cover.jpg"
);
assert!(fs.has_dir("AlbumA"), "the orphan keeps its directory alive");
fs.disarm_fail_remove("AlbumA/cover.jpg");
let second = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(second.failed(), 0);
assert!(fs.exists("AlbumB/cover.jpg"));
assert!(!fs.exists("AlbumA/cover.jpg"), "no orphan persists");
assert_eq!(
manifest.get("a").unwrap().cover_jpg.as_ref().unwrap().path,
"AlbumB/cover.jpg"
);
assert!(!fs.has_dir("AlbumA"), "the emptied directory is pruned");
}
#[test]
fn same_path_artifact_rewrite_does_no_remove_and_prunes_nothing() {
let mut manifest = Manifest::new();
let mut e = entry("Album/a.mp3", AudioFormat::Mp3);
e.cover_jpg = Some(ArtifactState {
path: "Album/cover.jpg".to_owned(),
hash: "h1".to_owned(),
});
manifest.insert("a", e);
let fs = MemFs::new()
.with_file("Album/a.mp3", b"AUDIO".to_vec())
.with_file("Album/cover.jpg", b"old".to_vec());
fs.arm_fail_remove("Album/cover.jpg");
let plan = Plan {
actions: vec![Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "Album/cover.jpg".to_owned(),
source_url: "https://art.suno.ai/a/large.jpg".to_owned(),
hash: "h2".to_owned(),
owner_id: "a".to_owned(),
content: None,
}],
};
let http = ScriptedHttp::new().route("a/large.jpg", Reply::ok(b"new".to_vec()));
let outcome = run(
&plan,
&mut manifest,
&[],
&http,
&fs,
&StubFfmpeg::flac(),
&RecordingClock::new(),
&ExecOptions::default(),
);
assert_eq!(
outcome.failed(),
0,
"no remove is attempted, so the armed failure never fires"
);
assert_eq!(outcome.artifacts_written, 1);
assert_eq!(fs.read_file("Album/cover.jpg").unwrap(), b"new");
assert_eq!(
manifest.get("a").unwrap().cover_jpg.as_ref().unwrap().hash,
"h2"
);
assert!(fs.has_dir("Album"));
}
mod concurrency {
use super::*;
use crate::ffmpeg::FfmpegError;
use crate::fs::{FileStat, FsError};
use crate::http::{HttpRequest, TransportError};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
#[derive(Default)]
struct YieldOnce {
yielded: bool,
}
impl Future for YieldOnce {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.yielded {
Poll::Ready(())
} else {
self.yielded = true;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
struct GatedHttp {
inner: ScriptedHttp,
inflight: Arc<AtomicUsize>,
peak: Arc<AtomicUsize>,
}
impl GatedHttp {
fn new(inner: ScriptedHttp) -> Self {
Self {
inner,
inflight: Arc::new(AtomicUsize::new(0)),
peak: Arc::new(AtomicUsize::new(0)),
}
}
fn peak(&self) -> usize {
self.peak.load(Ordering::SeqCst)
}
}
impl Http for GatedHttp {
async fn send(&self, request: HttpRequest) -> Result<HttpResponse, TransportError> {
let now = self.inflight.fetch_add(1, Ordering::SeqCst) + 1;
self.peak.fetch_max(now, Ordering::SeqCst);
YieldOnce::default().await;
let out = self.inner.send(request).await;
self.inflight.fetch_sub(1, Ordering::SeqCst);
out
}
}
fn download(id: &str, format: AudioFormat) -> (Clip, Desired, Action) {
let c = clip(id);
let d = desired(c.clone(), format);
let action = Action::Download {
clip: c.clone(),
lineage: LineageContext::own_root(&c),
path: d.path.clone(),
format,
};
(c, d, action)
}
fn opts_with(concurrency: u32) -> ExecOptions {
ExecOptions {
concurrency,
..small_poll()
}
}
#[test]
fn concurrency_never_exceeds_the_configured_bound() {
let count = 6;
let concurrency = 3;
let mut scripted = ScriptedHttp::new().with_auth();
let mut actions = Vec::new();
let mut desireds = Vec::new();
for i in 0..count {
let id = format!("c{i}");
scripted = scripted.route(&format!("{id}.mp3"), Reply::ok(b"mp3-body".to_vec()));
let (_c, d, action) = download(&id, AudioFormat::Mp3);
actions.push(action);
desireds.push(d);
}
let http = GatedHttp::new(scripted);
let fs = MemFs::new();
let plan = Plan { actions };
let mut manifest = Manifest::new();
let outcome = run_gated_fs(
&plan,
&mut manifest,
&desireds,
&http,
&fs,
&opts_with(concurrency),
);
assert_eq!(outcome.downloaded, count);
assert!(
http.peak() <= concurrency as usize,
"peak {} exceeded the bound {concurrency}",
http.peak()
);
assert_eq!(
http.peak(),
concurrency as usize,
"expected the run to saturate the bound"
);
}
fn run_gated_fs(
plan: &Plan,
manifest: &mut Manifest,
desired: &[Desired],
http: &GatedHttp,
fs: &MemFs,
opts: &ExecOptions,
) -> ExecOutcome {
let ffmpeg = StubFfmpeg::flac();
let clock = RecordingClock::new();
let mut albums = BTreeMap::new();
let mut playlists = BTreeMap::new();
let mut client = SunoClient::new(ClerkAuth::new("eyJtoken"), RecordingClock::new());
pollster::block_on(execute(
plan,
manifest,
&mut albums,
&mut playlists,
desired,
Ports {
client: &mut client,
http,
fs,
ffmpeg: &ffmpeg,
clock: &clock,
},
opts,
))
}
#[test]
fn a_failing_clip_does_not_abort_the_others() {
let mut scripted = ScriptedHttp::new().with_auth();
scripted = scripted
.route("ok1.mp3", Reply::ok(b"one".to_vec()))
.route("bad.mp3", Reply::status(404))
.route("ok2.mp3", Reply::ok(b"two".to_vec()));
let (_a, d1, a1) = download("ok1", AudioFormat::Mp3);
let (_b, d2, a2) = download("bad", AudioFormat::Mp3);
let (_c, d3, a3) = download("ok2", AudioFormat::Mp3);
let http = GatedHttp::new(scripted);
let fs = MemFs::new();
let plan = Plan {
actions: vec![a1, a2, a3],
};
let mut manifest = Manifest::new();
let outcome = run_gated_fs(
&plan,
&mut manifest,
&[d1, d2, d3],
&http,
&fs,
&opts_with(3),
);
assert_eq!(outcome.downloaded, 2);
assert_eq!(outcome.failed(), 1);
assert_eq!(outcome.status, RunStatus::Completed);
assert_eq!(outcome.failures[0].clip_id, "bad");
assert!(manifest.get("ok1").is_some());
assert!(manifest.get("ok2").is_some());
assert!(manifest.get("bad").is_none());
}
#[test]
fn outcome_is_identical_across_concurrency_levels() {
fn build() -> (Plan, Vec<Desired>) {
let mut actions = Vec::new();
let mut desireds = Vec::new();
for id in ["a", "b", "c", "d"] {
let (_c, d, action) = download(id, AudioFormat::Mp3);
actions.push(action);
desireds.push(d);
}
let (_e, de, ae) = download("fail", AudioFormat::Mp3);
actions.insert(2, ae);
desireds.push(de);
actions.push(Action::Skip {
clip_id: "gone".to_owned(),
});
actions.push(Action::Delete {
path: "old.mp3".to_owned(),
clip_id: "old".to_owned(),
});
(Plan { actions }, desireds)
}
fn http() -> ScriptedHttp {
ScriptedHttp::new()
.with_auth()
.route("a.mp3", Reply::ok(b"a".to_vec()))
.route("b.mp3", Reply::ok(b"b".to_vec()))
.route("c.mp3", Reply::ok(b"c".to_vec()))
.route("d.mp3", Reply::ok(b"d".to_vec()))
.route("fail.mp3", Reply::status(404))
}
fn seed_manifest() -> Manifest {
let mut m = Manifest::new();
m.insert("old".to_owned(), entry("old.mp3", AudioFormat::Mp3));
m
}
let (plan, desireds) = build();
let mut m1 = seed_manifest();
let fs1 = MemFs::new().with_file("old.mp3", b"x".to_vec());
let out1 = run_gated_fs(
&plan,
&mut m1,
&desireds,
&GatedHttp::new(http()),
&fs1,
&opts_with(1),
);
let mut m8 = seed_manifest();
let fs8 = MemFs::new().with_file("old.mp3", b"x".to_vec());
let out8 = run_gated_fs(
&plan,
&mut m8,
&desireds,
&GatedHttp::new(http()),
&fs8,
&opts_with(8),
);
assert_eq!(out1, out8, "outcome must not depend on concurrency");
assert_eq!(m1, m8, "final manifest must not depend on concurrency");
assert_eq!(out8.downloaded, 4);
assert_eq!(out8.deleted, 1);
assert_eq!(out8.skipped, 1);
assert_eq!(out8.failed(), 1);
}
#[test]
fn a_systemic_disk_full_aborts_promptly() {
let count = 8;
let concurrency = 2;
let mut scripted = ScriptedHttp::new().with_auth();
let mut actions = Vec::new();
let mut desireds = Vec::new();
for i in 0..count {
let id = format!("d{i}");
scripted = scripted.route(&format!("{id}.mp3"), Reply::ok(b"mp3-body".to_vec()));
let (_c, d, action) = download(&id, AudioFormat::Mp3);
actions.push(action);
desireds.push(d);
}
let fs = MemFs::new().fail_write_out_of_space("d0.mp3");
let http = GatedHttp::new(scripted);
let plan = Plan { actions };
let mut manifest = Manifest::new();
let outcome = run_gated_fs(
&plan,
&mut manifest,
&desireds,
&http,
&fs,
&opts_with(concurrency),
);
assert_eq!(outcome.status, RunStatus::DiskFull);
assert!(
outcome.downloaded < count,
"a systemic abort must stop remaining work, downloaded {}",
outcome.downloaded
);
}
#[test]
fn limiter_records_a_rate_limit_under_concurrent_calls() {
let scripted = ScriptedHttp::new()
.with_auth()
.route_seq(
"/gen/x/wav_file/",
vec![
Reply::status(429),
Reply::json(r#"{"wav_file_url": "https://cdn1.suno.ai/x.wav"}"#),
],
)
.route(
"/gen/y/wav_file/",
Reply::json(r#"{"wav_file_url": "https://cdn1.suno.ai/y.wav"}"#),
)
.route(
"/gen/z/wav_file/",
Reply::json(r#"{"wav_file_url": "https://cdn1.suno.ai/z.wav"}"#),
)
.route("x.wav", Reply::ok(b"wav-x".to_vec()))
.route("y.wav", Reply::ok(b"wav-y".to_vec()))
.route("z.wav", Reply::ok(b"wav-z".to_vec()));
let mut actions = Vec::new();
let mut desireds = Vec::new();
for id in ["x", "y", "z"] {
let (_c, d, action) = download(id, AudioFormat::Flac);
actions.push(action);
desireds.push(d);
}
let plan = Plan { actions };
let fs = MemFs::new();
let ffmpeg = StubFfmpeg::flac();
let clock = RecordingClock::new();
let mut albums = BTreeMap::new();
let mut playlists = BTreeMap::new();
let mut manifest = Manifest::new();
let mut client = SunoClient::new(ClerkAuth::new("eyJtoken"), RecordingClock::new());
let outcome = pollster::block_on(execute(
&plan,
&mut manifest,
&mut albums,
&mut playlists,
&desireds,
Ports {
client: &mut client,
http: &scripted,
fs: &fs,
ffmpeg: &ffmpeg,
clock: &clock,
},
&opts_with(3),
));
assert_eq!(outcome.downloaded, 3);
assert_eq!(outcome.failed(), 0);
assert!(
(client.limiter_rate() - 1.0).abs() < 1e-9,
"one 429 must halve the rate to 1.0, got {}",
client.limiter_rate()
);
}
#[test]
fn a_download_is_committed_in_plan_order_around_a_rename() {
let c_new = clip("new");
let mut d_new = desired(c_new.clone(), AudioFormat::Mp3);
d_new.path = "shared.mp3".to_owned();
let plan = Plan {
actions: vec![
Action::Rename {
from: "shared.mp3".to_owned(),
to: "moved.mp3".to_owned(),
},
Action::Download {
clip: c_new.clone(),
lineage: LineageContext::own_root(&c_new),
path: "shared.mp3".to_owned(),
format: AudioFormat::Mp3,
},
],
};
let scripted = ScriptedHttp::new()
.with_auth()
.route("new.mp3", Reply::ok(b"NEW-BODY".to_vec()));
let http = GatedHttp::new(scripted);
let fs = MemFs::new().with_file("shared.mp3", b"ORIGINAL".to_vec());
let mut manifest = Manifest::new();
manifest.insert("orig", entry("shared.mp3", AudioFormat::Mp3));
let outcome = run_gated_fs(&plan, &mut manifest, &[d_new], &http, &fs, &opts_with(4));
assert_eq!(outcome.renamed, 1);
assert_eq!(outcome.downloaded, 1);
assert_eq!(
fs.read_file("moved.mp3").as_deref(),
Some(&b"ORIGINAL"[..]),
"the rename must carry the original bytes, untouched by the download"
);
let landed = fs.read_file("shared.mp3").expect("new download must land");
assert_ne!(
landed, b"ORIGINAL",
"the new download must replace the moved original, not corrupt it"
);
assert_eq!(manifest.get("orig").unwrap().path, "moved.mp3");
assert_eq!(manifest.get("new").unwrap().path, "shared.mp3");
}
#[test]
fn an_aborted_reformat_leaves_the_old_file_and_manifest_consistent() {
let boom = clip("boom");
let mut d_boom = desired(boom.clone(), AudioFormat::Mp3);
d_boom.path = "boom.mp3".to_owned();
let reformer = clip("r");
let d_reformer = desired(reformer.clone(), AudioFormat::Mp3);
let plan = Plan {
actions: vec![
Action::Download {
clip: boom.clone(),
lineage: LineageContext::own_root(&boom),
path: "boom.mp3".to_owned(),
format: AudioFormat::Mp3,
},
Action::Reformat {
clip: reformer.clone(),
path: "r_new.mp3".to_owned(),
from_path: "r_old.flac".to_owned(),
from: AudioFormat::Flac,
to: AudioFormat::Mp3,
},
],
};
let scripted = ScriptedHttp::new()
.with_auth()
.route("boom.mp3", Reply::ok(b"boom-body".to_vec()))
.route("r.mp3", Reply::ok(b"reformatted".to_vec()));
let http = GatedHttp::new(scripted);
let fs = MemFs::new()
.with_file("r_old.flac", b"OLD-FLAC".to_vec())
.fail_write_out_of_space("boom.mp3");
let mut manifest = Manifest::new();
manifest.insert("r", entry("r_old.flac", AudioFormat::Flac));
let outcome = run_gated_fs(
&plan,
&mut manifest,
&[d_boom, d_reformer],
&http,
&fs,
&opts_with(4),
);
assert_eq!(outcome.status, RunStatus::DiskFull);
assert!(
fs.exists("r_old.flac"),
"the old file must survive the abort"
);
assert!(
!fs.exists("r_new.mp3"),
"no reformatted file may be written"
);
let still = manifest.get("r").expect("the manifest must still track r");
assert_eq!(
still.path, "r_old.flac",
"the manifest must still point at the surviving old file"
);
assert_eq!(still.format, AudioFormat::Flac);
}
#[test]
fn a_systemic_abort_leaves_no_untracked_destination_files() {
let mut scripted = ScriptedHttp::new().with_auth();
let mut actions = Vec::new();
let mut desireds = Vec::new();
for id in ["a0", "a1", "boom", "a3", "a4"] {
scripted = scripted.route(&format!("{id}.mp3"), Reply::ok(b"body".to_vec()));
let (_c, d, action) = download(id, AudioFormat::Mp3);
actions.push(action);
desireds.push(d);
}
let http = GatedHttp::new(scripted);
let fs = MemFs::new().fail_write_out_of_space("boom.mp3");
let plan = Plan { actions };
let mut manifest = Manifest::new();
let outcome = run_gated_fs(&plan, &mut manifest, &desireds, &http, &fs, &opts_with(2));
assert_eq!(outcome.status, RunStatus::DiskFull);
let tracked: std::collections::BTreeSet<String> = manifest
.entries
.values()
.map(|entry| entry.path.clone())
.collect();
for path in fs.paths() {
assert!(
tracked.contains(&path),
"found an untracked destination file: {path}"
);
}
assert!(
!fs.exists("a3.mp3"),
"uncommitted renders must not be on disk"
);
assert!(
!fs.exists("a4.mp3"),
"uncommitted renders must not be on disk"
);
}
struct CountingFfmpeg {
inner: StubFfmpeg,
held: Arc<AtomicUsize>,
peak: Arc<AtomicUsize>,
}
impl Ffmpeg for CountingFfmpeg {
fn wav_to_flac(
&self,
wav: &[u8],
) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send {
let fut = self.inner.wav_to_flac(wav);
let held = self.held.clone();
let peak = self.peak.clone();
async move {
let out = fut.await;
if out.is_ok() {
let now = held.fetch_add(1, Ordering::SeqCst) + 1;
peak.fetch_max(now, Ordering::SeqCst);
}
out
}
}
fn mp4_to_webp(
&self,
mp4: &[u8],
settings: WebpEncodeSettings,
) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send {
self.inner.mp4_to_webp(mp4, settings)
}
}
struct CountingFs {
inner: MemFs,
held: Arc<AtomicUsize>,
}
impl Filesystem for CountingFs {
fn write_atomic(&self, path: &str, bytes: &[u8]) -> Result<(), FsError> {
let out = self.inner.write_atomic(path, bytes);
self.held.fetch_sub(1, Ordering::SeqCst);
out
}
fn rename(&self, from: &str, to: &str) -> Result<(), FsError> {
self.inner.rename(from, to)
}
fn remove(&self, path: &str) -> Result<(), FsError> {
self.inner.remove(path)
}
fn prune_empty_dirs(&self, root: &str) -> Result<(), FsError> {
self.inner.prune_empty_dirs(root)
}
fn read(&self, path: &str) -> Result<Vec<u8>, FsError> {
self.inner.read(path)
}
fn metadata(&self, path: &str) -> Option<FileStat> {
self.inner.metadata(path)
}
}
#[test]
fn rendered_payloads_in_memory_stay_bounded_by_concurrency() {
let count = 12;
let concurrency = 3;
let mut scripted = ScriptedHttp::new().with_auth();
let mut actions = Vec::new();
let mut desireds = Vec::new();
for i in 0..count {
let id = format!("f{i}");
scripted = scripted
.route(
&format!("/gen/{id}/wav_file/"),
Reply::json(&format!(
r#"{{"wav_file_url": "https://cdn1.suno.ai/{id}.wav"}}"#
)),
)
.route(&format!("{id}.wav"), Reply::ok(b"wav-body".to_vec()));
let (_c, d, action) = download(&id, AudioFormat::Flac);
actions.push(action);
desireds.push(d);
}
let http = GatedHttp::new(scripted);
let held = Arc::new(AtomicUsize::new(0));
let peak = Arc::new(AtomicUsize::new(0));
let ffmpeg = CountingFfmpeg {
inner: StubFfmpeg::flac(),
held: held.clone(),
peak: peak.clone(),
};
let fs = CountingFs {
inner: MemFs::new(),
held: held.clone(),
};
let clock = RecordingClock::new();
let mut albums = BTreeMap::new();
let mut playlists = BTreeMap::new();
let mut manifest = Manifest::new();
let mut client = SunoClient::new(ClerkAuth::new("eyJtoken"), RecordingClock::new());
let plan = Plan { actions };
let outcome = pollster::block_on(execute(
&plan,
&mut manifest,
&mut albums,
&mut playlists,
&desireds,
Ports {
client: &mut client,
http: &http,
fs: &fs,
ffmpeg: &ffmpeg,
clock: &clock,
},
&opts_with(concurrency),
));
assert_eq!(outcome.downloaded, count as usize);
assert_eq!(
held.load(Ordering::SeqCst),
0,
"every payload must be committed"
);
assert!(
peak.load(Ordering::SeqCst) <= concurrency as usize + 1,
"peak live payloads {} exceeded the bound {}",
peak.load(Ordering::SeqCst),
concurrency + 1
);
assert!(
peak.load(Ordering::SeqCst) >= 2,
"the render should genuinely overlap, peak was {}",
peak.load(Ordering::SeqCst)
);
}
}
}