use super::*;
pub(super) async fn assemble(
ctx: &RunCtx<'_>,
areas: &[suno_core::AreaListing],
clips: &[suno_core::Clip],
store: &suno_core::LineageStore,
selection: &ResolvedSelection,
force_copy: bool,
graph_changed: bool,
) -> std::result::Result<Assembled, ExitCode> {
let settings = ctx.settings;
let args = ctx.args;
let verbosity = ctx.verbosity;
let sources = source_statuses(areas, force_copy);
let can_delete = deletion_allowed(&sources);
let library_authoritative = library_authoritative(areas, force_copy);
let colliding_albums = store.colliding_root_titles();
let modes_by_id = build_modes_by_id(areas, force_copy);
let since = match args.since.as_deref().map(RecencySpec::parse).transpose() {
Ok(since) => since,
Err(message) => {
eprint_t!("error: {message}");
return Err(ExitCode::Config);
}
};
let truncate = narrows_downloads(can_delete, library_authoritative);
if is_narrowed(args.limit, args.since.as_deref()) && !truncate && verbosity >= -1 {
let deletes = if can_delete {
"; the library is still mirrored and deletions apply"
} else {
""
};
eprint_t!(
"note: --limit/--since do not narrow this run (an authoritative full library is listed){deletes}"
);
}
let params = SelectParams {
limit: if truncate { args.limit } else { None },
since: if truncate { since } else { None },
min_newest: settings.min_newest as usize,
now: wallclock::now_secs(),
last_run: last_run::read_last_run(ctx.dest),
};
let selected = select(clips, ¶ms);
let mut contexts: HashMap<String, LineageContext> = selected
.iter()
.map(|clip| (clip.id.clone(), store.context_for(clip)))
.collect();
let leads = resolve_lead_ids(&selected, &settings.lead_tracks);
if verbosity >= -1 {
for entry in &leads.unmatched {
eprint_t!("warning: lead_tracks entry '{entry}' matched no selected clip; ignoring");
}
for entry in &leads.ambiguous {
eprint_t!(
"warning: lead_tracks entry '{entry}' matched more than one clip; use a longer id"
);
}
}
for (id, assignment) in assign_track_numbers(
&selected,
&contexts,
&leads.resolved,
settings.number_singletons,
) {
if let Some(context) = contexts.get_mut(&id) {
context.track = assignment.track;
context.track_total = assignment.total;
}
}
let mut desired = build_desired(
&selected,
settings.format,
&modes_by_id,
&contexts,
&colliding_albums,
ArtifactToggles {
animated_covers: settings.animated_covers,
details: settings.details_sidecar,
lyrics: settings.lyrics_sidecar,
lrc: settings.lrc_sidecar,
video: settings.video_mp4,
webp: settings.animated_cover_webp,
},
&NamingConfig {
template: settings.naming_template.clone(),
character_set: settings.character_set,
..NamingConfig::default()
},
);
let stems_by_id = stems::list_existing_stems(
settings.download_stems,
&selected,
ctx.client,
ctx.http,
settings.concurrency,
)
.await;
if settings.download_stems {
for d in &mut desired {
let base = stems::strip_format_ext(&d.path, settings.format);
d.stems = stems_by_id
.get(&d.clip.id)
.map(|stems| clip_stems(base, stems, settings.stem_format, settings.character_set));
}
}
let albums_desired = if library_authoritative {
album_desired(
&desired,
settings.animated_covers,
settings.raw_animated_cover,
settings.animated_cover_webp,
)
} else {
Vec::new()
};
let mut protected_playlists: BTreeSet<String> = BTreeSet::new();
let (playlist_desired, playlists_enumerated) =
if selection.is_plain_library() && library_authoritative {
areas::fetch_playlist_desired(
ctx.client,
ctx.http,
&desired,
&mut protected_playlists,
verbosity,
settings.concurrency,
)
.await
} else {
build_scoped_playlist_desired(
areas,
&desired,
store,
&mut protected_playlists,
force_copy,
!truncate,
)
};
let stored_playlists: BTreeMap<String, PlaylistState> = store
.playlists
.iter()
.filter(|(id, _)| !protected_playlists.contains(id.as_str()))
.map(|(id, state)| (id.clone(), state.clone()))
.collect();
Ok(Assembled {
desired,
albums_desired,
playlist_desired,
stored_playlists,
sources,
library_authoritative,
playlists_enumerated,
graph_changed,
})
}