use std::collections::{HashMap, HashSet};
use crate::client::SunoClient;
use crate::clock::Clock;
use crate::error::Result;
use crate::http::Http;
use crate::lineage::{Resolution, ResolveStatus, RootInfo, attribution_edges, immediate_parent};
use crate::model::Clip;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolveOpts {
pub max_gap_fills: u32,
pub concurrency: u32,
}
impl Default for ResolveOpts {
fn default() -> Self {
Self {
max_gap_fills: 200,
concurrency: 4,
}
}
}
pub async fn resolve_roots(
clips: &[Clip],
archived_parents: &HashMap<String, String>,
client: &SunoClient<impl Clock>,
http: &impl Http,
opts: ResolveOpts,
) -> Result<Resolution> {
let mut resolver = Resolver::new(clips, opts, archived_parents);
resolver.run(client, http).await?;
Ok(resolver.into_resolution(clips))
}
enum Walk {
Resolved,
Blocked(String),
}
struct Resolver<'a> {
index: HashMap<String, Clip>,
archived_parents: &'a HashMap<String, String>,
gap_filled: HashSet<String>,
bridges: HashMap<String, String>,
external: HashSet<String>,
seeded: HashSet<String>,
memo: HashMap<String, RootInfo>,
targets: Vec<String>,
budget: u32,
concurrency: u32,
}
impl<'a> Resolver<'a> {
fn new(
clips: &[Clip],
opts: ResolveOpts,
archived_parents: &'a HashMap<String, String>,
) -> Self {
let index = clips
.iter()
.map(|clip| (clip.id.clone(), clip.clone()))
.collect();
let targets = clips.iter().map(|clip| clip.id.clone()).collect();
Self {
index,
archived_parents,
gap_filled: HashSet::new(),
bridges: HashMap::new(),
external: HashSet::new(),
seeded: HashSet::new(),
memo: HashMap::new(),
targets,
budget: opts.max_gap_fills,
concurrency: opts.concurrency,
}
}
async fn run(&mut self, client: &SunoClient<impl Clock>, http: &impl Http) -> Result<()> {
let targets = self.targets.clone();
loop {
let mut frontier: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let mut blocked: Vec<(String, String)> = Vec::new();
for target in &targets {
if self.memo.contains_key(target) {
continue;
}
if let Walk::Blocked(missing) = self.walk(target) {
if seen.insert(missing.clone()) {
frontier.push(missing.clone());
}
blocked.push((target.clone(), missing));
}
}
if blocked.is_empty() {
break;
}
if self.budget == 0 || !self.gap_fill(client, http, &frontier).await? {
self.finalise_external(&blocked);
break;
}
}
Ok(())
}
fn walk(&mut self, start: &str) -> Walk {
if self.memo.contains_key(start) {
return Walk::Resolved;
}
let mut chain: Vec<String> = Vec::new();
let mut visited: HashSet<String> = HashSet::new();
let mut current = start.to_string();
loop {
if let Some(info) = self.memo.get(¤t).cloned() {
self.assign(&chain, &info);
return Walk::Resolved;
}
if visited.contains(¤t) {
let cycle_start = chain.iter().position(|id| *id == current).unwrap_or(0);
let root = chain[cycle_start..]
.iter()
.min()
.cloned()
.unwrap_or_else(|| current.clone());
let info = self.terminal(&root, ResolveStatus::Cycle);
self.assign(&chain, &info);
self.memo.insert(current, info);
return Walk::Resolved;
}
let parent_id = if let Some(clip) = self.index.get(¤t) {
immediate_parent(clip).map(|(id, _edge)| id)
} else if let Some(parent) = self.archived_parents.get(¤t) {
Some(parent.clone())
} else {
return Walk::Blocked(current);
};
let Some(parent_id) = parent_id else {
let info = RootInfo {
root_id: current.clone(),
root_title: self.title_of(¤t),
status: ResolveStatus::Resolved,
};
self.assign(&chain, &info);
self.memo.insert(current, info);
return Walk::Resolved;
};
visited.insert(current.clone());
chain.push(current);
if self.index.contains_key(&parent_id) || self.archived_parents.contains_key(&parent_id)
{
current = parent_id;
} else if let Some(bridged) = self.bridges.get(&parent_id).cloned() {
visited.insert(parent_id);
current = bridged;
} else if self.external.contains(&parent_id) {
let info = self.terminal(&parent_id, ResolveStatus::External);
self.assign(&chain, &info);
self.memo.insert(parent_id, info);
return Walk::Resolved;
} else {
return Walk::Blocked(parent_id);
}
}
}
async fn gap_fill(
&mut self,
client: &SunoClient<impl Clock>,
http: &impl Http,
frontier: &[String],
) -> Result<bool> {
let mut want: Vec<String> = frontier
.iter()
.filter(|id| !self.known(id))
.cloned()
.collect();
want.sort();
want.dedup();
let mut seeds: Vec<String> = self
.clip_root_seeds()
.into_iter()
.filter(|id| !self.known(id) && !self.seeded.contains(id) && !want.contains(id))
.collect();
seeds.sort();
seeds.dedup();
if want.is_empty() && seeds.is_empty() {
return Ok(false);
}
let frontier_take = (self.budget as usize).min(want.len());
let frontier_batch: Vec<String> = want.into_iter().take(frontier_take).collect();
self.budget -= frontier_batch.len() as u32;
let seed_take = (self.budget as usize).min(seeds.len());
let seed_batch: Vec<String> = seeds.into_iter().take(seed_take).collect();
self.budget -= seed_batch.len() as u32;
for id in &seed_batch {
self.seeded.insert(id.clone());
}
let all: Vec<&str> = frontier_batch
.iter()
.chain(seed_batch.iter())
.map(String::as_str)
.collect();
let fetched = client
.get_clips_by_ids(http, &all, self.concurrency as usize)
.await?;
let mut returned: HashSet<String> = HashSet::new();
let mut progressed = false;
for clip in fetched {
returned.insert(clip.id.clone());
if self.insert_ancestor(clip) {
progressed = true;
}
}
for id in &frontier_batch {
if returned.contains(id) {
continue;
}
match client.get_clip_parent(http, id).await? {
Some(parent) => {
let parent_id = parent.id.clone();
self.insert_ancestor(parent);
self.bridges.insert(id.clone(), parent_id);
progressed = true;
}
None => {
self.external.insert(id.clone());
progressed = true;
}
}
}
Ok(progressed)
}
fn clip_root_seeds(&self) -> Vec<String> {
let mut seeds = Vec::new();
for clip in self.index.values() {
for edge in attribution_edges(clip) {
if edge.same_owner {
seeds.push(edge.parent_id);
}
}
}
seeds
}
fn insert_ancestor(&mut self, clip: Clip) -> bool {
if clip.id.is_empty() || self.index.contains_key(&clip.id) {
return false;
}
self.gap_filled.insert(clip.id.clone());
self.index.insert(clip.id.clone(), clip);
true
}
fn known(&self, id: &str) -> bool {
self.index.contains_key(id)
|| self.archived_parents.contains_key(id)
|| self.bridges.contains_key(id)
|| self.external.contains(id)
}
fn finalise_external(&mut self, blocked: &[(String, String)]) {
for (target, missing) in blocked {
if self.memo.contains_key(target) {
continue;
}
let info = self.terminal(missing, ResolveStatus::External);
self.memo.insert(target.clone(), info);
}
}
fn terminal(&self, id: &str, status: ResolveStatus) -> RootInfo {
RootInfo {
root_id: id.to_string(),
root_title: self.title_of(id),
status,
}
}
fn title_of(&self, id: &str) -> String {
self.index
.get(id)
.map_or_else(String::new, |clip| clip.title.clone())
}
fn assign(&mut self, chain: &[String], info: &RootInfo) {
for id in chain {
self.memo.insert(id.clone(), info.clone());
}
}
fn into_resolution(self, clips: &[Clip]) -> Resolution {
let mut roots = HashMap::with_capacity(clips.len());
for clip in clips {
let info = self
.memo
.get(&clip.id)
.cloned()
.unwrap_or_else(|| RootInfo {
root_id: clip.id.clone(),
root_title: clip.title.clone(),
status: ResolveStatus::Unresolved,
});
roots.insert(clip.id.clone(), info);
}
let mut gap_filled: Vec<Clip> = self
.gap_filled
.iter()
.filter_map(|id| self.index.get(id).cloned())
.collect();
gap_filled.sort_by(|a, b| a.id.cmp(&b.id));
let mut bridges: Vec<(String, String)> = self
.bridges
.iter()
.map(|(child, parent)| (child.clone(), parent.clone()))
.collect();
bridges.sort();
Resolution {
roots,
gap_filled,
bridges,
}
}
}
#[cfg(test)]
mod tests;