use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::model::Clip;
const ZERO_UUID: &str = "00000000-0000-0000-0000-000000000000";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeType {
Cover,
Remaster,
SpeedEdit,
Edit,
Extend,
SectionReplace,
Stitch,
Derived,
Uploaded,
}
impl EdgeType {
pub fn label(self) -> &'static str {
match self {
EdgeType::Cover => "Cover of",
EdgeType::Remaster => "Remaster of",
EdgeType::SpeedEdit => "Speed-edited from",
EdgeType::Edit => "Edited from",
EdgeType::Extend => "Extended from",
EdgeType::SectionReplace => "Section replaced from",
EdgeType::Stitch => "Stitched from",
EdgeType::Derived => "Derived from",
EdgeType::Uploaded => "Uploaded",
}
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum EdgeRole {
#[default]
Primary,
Secondary,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Edge {
pub parent_id: String,
pub edge_type: EdgeType,
pub role: EdgeRole,
pub ordinal: u32,
pub source_field: &'static str,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResolveStatus {
External,
Unresolved,
Cycle,
#[default]
#[serde(other)]
Resolved,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootInfo {
pub root_id: String,
pub root_title: String,
pub status: ResolveStatus,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Resolution {
pub roots: HashMap<String, RootInfo>,
pub gap_filled: Vec<Clip>,
pub bridges: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineageContext {
pub root_id: String,
pub root_title: String,
pub root_date: String,
pub parent_id: String,
pub edge_type: Option<EdgeType>,
pub status: ResolveStatus,
pub track: u32,
pub track_total: u32,
}
impl LineageContext {
pub fn for_clip(clip: &Clip, resolution: &Resolution) -> LineageContext {
let (root_id, root_title, status) = match resolution.roots.get(&clip.id) {
Some(info) => (info.root_id.clone(), info.root_title.clone(), info.status),
None => (clip.id.clone(), clip.title.clone(), ResolveStatus::Resolved),
};
let (parent_id, edge_type) = match immediate_parent(clip) {
Some((id, edge)) => (id, Some(edge)),
None => (String::new(), None),
};
LineageContext {
root_id,
root_title,
root_date: clip.created_at.clone(),
parent_id,
edge_type,
status,
track: 0,
track_total: 0,
}
}
pub fn own_root(clip: &Clip) -> LineageContext {
LineageContext {
root_id: clip.id.clone(),
root_title: clip.title.clone(),
root_date: clip.created_at.clone(),
parent_id: String::new(),
edge_type: None,
status: ResolveStatus::Resolved,
track: 0,
track_total: 0,
}
}
pub fn album(&self, own_title: &str) -> String {
let root_title = self.root_title.trim();
if !root_title.is_empty() && self.root_title != own_title {
self.root_title.clone()
} else {
own_title.to_owned()
}
}
pub fn year(&self, own_created_at: &str) -> String {
let root_year = year_of(&self.root_date);
if root_year.is_empty() {
year_of(own_created_at)
} else {
root_year
}
}
}
fn year_of(created_at: &str) -> String {
created_at.chars().take(4).collect()
}
pub fn edge_type(clip: &Clip) -> Option<EdgeType> {
let task = clip.task.as_str();
let clip_type = clip.clip_type.as_str();
if task == "infill" || task == "fixed_infill" {
Some(EdgeType::SectionReplace)
} else if task == "extend" {
Some(EdgeType::Extend)
} else if clip_type == "concat" {
Some(EdgeType::Stitch)
} else if clip_type == "edit_speed" {
Some(EdgeType::SpeedEdit)
} else if task == "cover" {
Some(EdgeType::Cover)
} else if clip_type == "upsample" || task == "upsample" {
Some(EdgeType::Remaster)
} else if clip_type == "edit_v3_export" {
Some(EdgeType::Edit)
} else if normalise_id(&clip.edited_clip_id).is_some() {
Some(EdgeType::Derived)
} else {
None
}
}
pub fn immediate_parent(clip: &Clip) -> Option<(String, EdgeType)> {
primary_parent(clip).map(|(id, edge, _field)| (id, edge))
}
pub fn lineage_edges(clip: &Clip) -> Vec<Edge> {
let Some(edge_type) = edge_type(clip) else {
return Vec::new();
};
let mut edges = Vec::new();
if let Some((parent_id, _edge, source_field)) = primary_parent(clip) {
edges.push(Edge {
parent_id,
edge_type,
role: EdgeRole::Primary,
ordinal: 0,
source_field,
});
}
match edge_type {
EdgeType::Stitch => {
for (ordinal, entry) in clip.concat_history.iter().enumerate().skip(1) {
if let Some(id) = normalise_id(&entry.id) {
edges.push(Edge {
parent_id: id,
edge_type,
role: EdgeRole::Secondary,
ordinal: ordinal as u32,
source_field: "concat_history",
});
}
}
}
EdgeType::SectionReplace => {
if let Some(future) = normalise_id(&clip.override_future_clip_id)
&& edges
.first()
.is_none_or(|primary| primary.parent_id != future)
{
edges.push(Edge {
parent_id: future,
edge_type,
role: EdgeRole::Secondary,
ordinal: 1,
source_field: "override_future_clip_id",
});
}
}
_ => {}
}
edges
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttributionEdge {
pub parent_id: String,
pub edge_slug: String,
pub role: EdgeRole,
pub ordinal: u32,
pub source_field: &'static str,
pub same_owner: bool,
}
pub fn attribution_edges(clip: &Clip) -> Vec<AttributionEdge> {
let mut edges = Vec::new();
for root in &clip.clip_roots {
let Some(parent_id) = normalise_id(&root.id) else {
continue;
};
let ordinal = edges.len() as u32;
edges.push(AttributionEdge {
parent_id,
edge_slug: clip.clip_attribution_type.clone(),
role: EdgeRole::Secondary,
ordinal,
source_field: "clip_roots",
same_owner: same_owner(clip, root),
});
}
edges
}
fn same_owner(clip: &Clip, root: &crate::model::ClipRoot) -> bool {
let clip_handle = clip.handle.trim();
let root_handle = root.handle.trim();
!clip_handle.is_empty() && !root_handle.is_empty() && clip_handle == root_handle
}
fn primary_parent(clip: &Clip) -> Option<(String, EdgeType, &'static str)> {
let edge = edge_type(clip)?;
let history_head = clip.history.first().map_or("", |entry| entry.id.as_str());
let concat_head = clip
.concat_history
.first()
.map_or("", |entry| entry.id.as_str());
let candidates: Vec<(&str, &'static str)> = match edge {
EdgeType::SectionReplace => vec![
(
clip.override_history_clip_id.as_str(),
"override_history_clip_id",
),
(
clip.override_future_clip_id.as_str(),
"override_future_clip_id",
),
(history_head, "history"),
(clip.edited_clip_id.as_str(), "edited_clip_id"),
],
EdgeType::Extend => vec![
(history_head, "history"),
(clip.edited_clip_id.as_str(), "edited_clip_id"),
],
EdgeType::Stitch => vec![
(concat_head, "concat_history"),
(clip.edited_clip_id.as_str(), "edited_clip_id"),
],
EdgeType::SpeedEdit => vec![
(clip.speed_clip_id.as_str(), "speed_clip_id"),
(clip.edited_clip_id.as_str(), "edited_clip_id"),
],
EdgeType::Cover => vec![
(clip.cover_clip_id.as_str(), "cover_clip_id"),
(clip.edited_clip_id.as_str(), "edited_clip_id"),
],
EdgeType::Remaster => vec![
(clip.upsample_clip_id.as_str(), "upsample_clip_id"),
(clip.remaster_clip_id.as_str(), "remaster_clip_id"),
(clip.edited_clip_id.as_str(), "edited_clip_id"),
],
EdgeType::Edit | EdgeType::Derived => {
vec![(clip.edited_clip_id.as_str(), "edited_clip_id")]
}
EdgeType::Uploaded => vec![],
};
candidates
.into_iter()
.find_map(|(value, field)| normalise_id(value).map(|id| (id, edge, field)))
}
fn normalise_id(id: &str) -> Option<String> {
let id = id.strip_prefix("m_").unwrap_or(id);
if id.is_empty() || id == ZERO_UUID {
None
} else {
Some(id.to_string())
}
}
#[cfg(test)]
mod tests;